UI(Reviews): move 'loading allowed' to FeatureReviews

This commit is contained in:
Pieter Vander Vennet 2025-07-25 15:39:59 +02:00
parent ce3a049d9d
commit d662bc2707
10 changed files with 105 additions and 63 deletions

View file

@ -810,9 +810,12 @@
"deleteConfirm": "Permantently delete this review",
"deleteText": "This cannot be undone",
"deleteTitle": "Delete this review?",
"disabledForPrivacy": "Reviews are disabled due to your privacy settings.",
"edit": "Edit review",
"editPrivacySettings": "Edit your privacy settings",
"i_am_affiliated": "I am affiliated with this object",
"i_am_affiliated_explanation": "Check if you are an owner, creator, employee, …",
"loadOnce": "Load reviews once",
"no_reviews_yet": "There are no reviews yet. Be the first one!",
"non_place_review": "One review is not about a place and is not shown here.",
"non_place_reviews": "{n} reviews are not about a place and are not shown here.",

View file

@ -369,7 +369,7 @@ export default class UserRelatedState {
private static initUserSettingsState(): LayerConfig {
try {
return new LayerConfig(<LayerConfigJson>usersettings, "userinformationpanel")
return new LayerConfig(<LayerConfigJson><any>usersettings, "userinformationpanel")
} catch (e) {
return undefined
}
@ -655,7 +655,7 @@ export default class UserRelatedState {
for (const key in featureSwitches) {
if (featureSwitches[key].addCallbackAndRun) {
featureSwitches[key].addCallbackAndRun((v) => {
featureSwitches[key].addCallbackAndRun((v: string) => {
const oldV = amendedPrefs.data["__" + key]
if (oldV === v) {
return

View file

@ -4,6 +4,17 @@ import { Utils } from "../../Utils"
import { Feature, Position } from "geojson"
import { GeoOperations } from "../GeoOperations"
import { SpecialVisualizationState } from "../../UI/SpecialVisualization"
import { WithUserRelatedState } from "../../Models/ThemeViewState/WithUserRelatedState"
export interface ReviewCollection {
readonly subjectUri?: Store<string>
readonly loadingAllowed: UIEventSource<boolean>
removeReviewLocally(review: Review): void
deleteReview(review: Review & { signature: string }): Promise<void>
createReview(review: Omit<Review, "sub">): Promise<void>
}
export class MangroveIdentity {
private readonly keypair: UIEventSource<CryptoKeyPair> = new UIEventSource<CryptoKeyPair>(
@ -134,12 +145,13 @@ export class MangroveIdentity {
}
}
/**
* Tracks all reviews of a given feature, allows to create a new review (and inserts this into the list)
*
* This object will start fetching the reviews as soon as it is constructed
*/
export default class FeatureReviews {
export default class FeatureReviews implements ReviewCollection {
/**
* See https://gitlab.com/open-reviews/mangrove/-/blob/master/servers/reviewer/src/review.rs#L269 and https://source.mapcomplete.org/MapComplete/MapComplete/issues/1775
*/
@ -147,10 +159,11 @@ export default class FeatureReviews {
private static readonly _featureReviewsCache: Record<string, FeatureReviews> = {}
public readonly subjectUri: Store<string>
public readonly average: Store<number | null>
public readonly loadingAllowed: UIEventSource<boolean>
private readonly _reviews: UIEventSource<
(Review & { kid: string; signature: string; madeByLoggedInUser: Store<boolean> })[]
> = new UIEventSource(undefined)
public readonly reviews: Store<(Review & { signature: string, madeByLoggedInUser: Store<boolean> })[]> =
public readonly reviews: Store<(Review & { kid: string, signature: string, madeByLoggedInUser: Store<boolean> })[]> =
this._reviews
private readonly _lat: number
private readonly _lon: number
@ -158,23 +171,21 @@ export default class FeatureReviews {
private readonly _name: Store<string>
private readonly _identity: MangroveIdentity
private readonly _testmode: Store<boolean>
public readonly loadingAllowed: UIEventSource<boolean | null>
private readonly _reportError: (msg: string, extra: string) => Promise<void>
private constructor(
feature: Feature,
tagsSource: UIEventSource<Record<string, string>>,
mangroveIdentity: MangroveIdentity,
loadingAllowed?: UIEventSource<boolean>,
options?: Readonly<{
nameKey?: "name" | string
fallbackName?: string
uncertaintyRadius?: number
uncertaintyRadius?: number,
}>,
testmode?: Store<boolean>,
loadingAllowed?: UIEventSource<boolean | null>,
reportError?: (msg: string, extra: string) => Promise<void>
) {
this.loadingAllowed = loadingAllowed
this._reportError = reportError
const centerLonLat = GeoOperations.centerpointCoordinates(feature)
;[this._lon, this._lat] = centerLonLat
@ -221,14 +232,17 @@ export default class FeatureReviews {
})
this.subjectUri = this.ConstructSubjectUri()
this.loadingAllowed = loadingAllowed
this.subjectUri.mapD(
async (sub) => {
if (!loadingAllowed.data) {
return
}
const reviews = await MangroveReviews.getReviews({ sub })
console.debug("Got reviews for", feature, reviews, sub)
this.addReviews(reviews.reviews, this._name.data)
},
[this._name]
[this._name, loadingAllowed]
)
/* We also construct all subject queries _without_ encoding the name to work around a previous bug
* See https://github.com/giggls/opencampsitemap/issues/30
@ -267,6 +281,7 @@ export default class FeatureReviews {
})
}
/**
* Construct a featureReviewsFor or fetches it from the cache
*
@ -288,7 +303,7 @@ export default class FeatureReviews {
tagsSource: UIEventSource<Record<string, string>>,
mangroveIdentity: MangroveIdentity,
options: { nameKey: string; fallbackName: string },
state?: SpecialVisualizationState
state?: SpecialVisualizationState & WithUserRelatedState,
): FeatureReviews {
const key =
feature.properties.id +
@ -300,34 +315,14 @@ export default class FeatureReviews {
if (cached !== undefined) {
return cached
}
const themeIsSensitive = state?.theme?.enableMorePrivacy ?? false
const settings =
state?.osmConnection?.getPreference<"always" | "yes" | "ask" | "hidden">(
"reviews-allowed"
) ?? new ImmutableStore("yes")
const loadingAllowed = new UIEventSource(false)
settings.addCallbackAndRun((s) => {
if (s === "hidden") {
loadingAllowed.set(null)
return
}
if (s === "always") {
loadingAllowed.set(true)
return
}
if (themeIsSensitive || s === "ask") {
loadingAllowed.set(false)
return
}
loadingAllowed.set(true)
})
const featureReviews = new FeatureReviews(
feature,
tagsSource,
mangroveIdentity,
new UIEventSource(state?.loadReviews?.data ?? true),
options,
state?.featureSwitchIsTesting,
loadingAllowed,
(msg, extra) => state?.reportError(msg, extra)
)
FeatureReviews._featureReviewsCache[key] = featureReviews
@ -458,7 +453,7 @@ export default class FeatureReviews {
this.removeReviewLocally(review)
}
public removeReviewLocally(review: Review){
public removeReviewLocally(review: Review): void {
this._reviews.set(
this._reviews.data?.filter(r => r !== review)
)

View file

@ -4,7 +4,7 @@ import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import UserRelatedState from "../../Logic/State/UserRelatedState"
import { QueryParameters } from "../../Logic/Web/QueryParameters"
import FeatureSwitchState from "../../Logic/State/FeatureSwitchState"
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import LayerConfig from "../ThemeConfig/LayerConfig"
import { LastClickFeatureSource } from "../../Logic/FeatureSource/Sources/LastClickFeatureSource"
import { GeocodingUtils } from "../../Logic/Search/GeocodingProvider"
@ -18,6 +18,9 @@ export class WithUserRelatedState {
readonly osmConnection: OsmConnection
readonly userRelatedState: UserRelatedState
readonly loadReviews: Store<boolean>
readonly overlayLayerStates: ReadonlyMap<
string,
{ readonly isDisplayed: UIEventSource<boolean> }
@ -62,6 +65,7 @@ export class WithUserRelatedState {
this.featureSwitches,
rasterLayer
)
this.loadReviews = this.constructIsLoadingReviewsAllowed()
if (!this.theme.official) {
// Add custom themes to the "visited custom themes"
@ -113,4 +117,31 @@ export class WithUserRelatedState {
}
return this.theme.getMatchingLayer(properties)
}
private constructIsLoadingReviewsAllowed() : UIEventSource<boolean>{
const loadingAllowed = new UIEventSource(false)
const themeIsSensitive = this.theme?.enableMorePrivacy ?? false
const settings =
this?.osmConnection?.getPreference<"always" | "yes" | "ask" | "hidden">(
"reviews-allowed",
) ?? new ImmutableStore("yes")
settings.addCallbackAndRun((s) => {
if (s === "hidden") {
loadingAllowed.set(null)
return
}
if (s === "always") {
loadingAllowed.set(true)
return
}
if (themeIsSensitive || s === "ask") {
loadingAllowed.set(false)
return
}
loadingAllowed.set(true)
})
return loadingAllowed
}
}

View file

@ -23,6 +23,7 @@
let average = reviews.average
let allReviews: Store<
(Review & {
kid: string,
signature: string,
madeByLoggedInUser: Store<boolean>
})[]
@ -32,7 +33,7 @@
let subject: Store<string> = reviews.subjectUri
</script>
<ReviewPrivacyShield {reviews} guistate={state.guistate}>
<ReviewPrivacyShield loadingAllowed={reviews.loadingAllowed} guistate={state.guistate}>
<div class="border-2 border-dashed border-gray-300 p-2 flex flex-col gap-y-2">
{#if $allReviews?.length > 1}
<StarsBar score={$average} />
@ -43,7 +44,7 @@
</div>
{:else if $allReviews.length > 0}
{#each $allReviews as review}
<SingleReview {review} {state} {tags} {feature} {layer} {reviews}/>
<SingleReview {review} {state} {tags} {feature} {layer} {reviews} showMenu={false}/>
{/each}
{:else}
<div class="subtle m-2 italic flex justify-center">

View file

@ -1,5 +1,6 @@
<script lang="ts">
import FeatureReviews from "../../Logic/Web/MangroveReviews"
import type { ReviewCollection } from "../../Logic/Web/MangroveReviews"
import StarsBar from "./StarsBar.svelte"
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
@ -10,7 +11,7 @@
import Tr from "../Base/Tr.svelte"
import If from "../Base/If.svelte"
import Loading from "../Base/Loading.svelte"
import { MangroveReviews, Review } from "mangrove-reviews-typescript"
import { Review } from "mangrove-reviews-typescript"
import { placeholder } from "../../Utils/placeholder"
import { ExclamationTriangle } from "@babeard/svelte-heroicons/solid/ExclamationTriangle"
import ReviewPrivacyShield from "./ReviewPrivacyShield.svelte"
@ -25,9 +26,9 @@
* The form to create a new review.
* This is multi-stepped.
*/
export let reviews: FeatureReviews
export let reviews: ReviewCollection
export let editReview: Review & { signature: string } = undefined
let subject: Store<string> = editReview !== undefined ? new ImmutableStore(editReview.sub) : reviews.subjectUri
export let subject: Store<string> = editReview !== undefined ? new ImmutableStore(editReview.sub) : reviews.subjectUri
let score = editReview?.rating ?? 0
let confirmedScore = editReview?.rating
let isAffiliated = new UIEventSource(editReview?.metadata?.is_affiliated ?? false)
@ -49,7 +50,11 @@
let uploadFailed: string = undefined
let isTesting = state?.featureSwitchIsTesting
let debug = state.featureSwitches.featureSwitchIsDebugging
/**
* only for this feature, copy upstream value to start with
*/
async function save() {
if (hasError.data) {
return
@ -98,11 +103,9 @@
_state = "done"
}
let test = state.featureSwitches.featureSwitchIsTesting
let debug = state.featureSwitches.featureSwitchIsDebugging
</script>
<ReviewPrivacyShield hiddenIfNotAllowed {reviews} guistate={state.guistate}>
<ReviewPrivacyShield hiddenIfNotAllowed loadingAllowed={reviews.loadingAllowed} guistate={state.guistate}>
{#if uploadFailed}
<div class="alert flex">
<ExclamationTriangle class="h-6 w-6" />
@ -188,7 +191,7 @@
<Tr cls="subtle mt-4" t={t.tos} />
{/if}
{#if $debug || $test}
{#if $debug || $isTesting}
<span class="subtle self-end">{$subject}</span>
{/if}
</div>

View file

@ -2,29 +2,31 @@
/**
* Due to privacy, we cannot load reviews unless allowed in the privacy policy
*/
import FeatureReviews from "../../Logic/Web/MangroveReviews"
import { MenuState } from "../../Models/MenuState"
import { ImmutableStore } from "../../Logic/UIEventSource"
import { UIEventSource } from "../../Logic/UIEventSource"
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
export let guistate: MenuState
export let reviews: FeatureReviews
export let loadingAllowed : UIEventSource<boolean>
export let hiddenIfNotAllowed: boolean = false
let allowed = reviews?.loadingAllowed ?? new ImmutableStore(true)
let t = Translations.t.reviews
</script>
{#if $allowed}
{#if $loadingAllowed}
<slot />
{:else if !hiddenIfNotAllowed && $allowed !== null}
{:else if !hiddenIfNotAllowed && $loadingAllowed !== null}
<div class="low-interaction mx-1 flex flex-col rounded">
Reviews are disabled due to your privacy settings.
<button on:click={() => reviews.loadingAllowed.set(true)} class="primary">
Load reviews once
<Tr t={t.disabledForPrivacy}/>
<button on:click={() => {loadingAllowed.set(true)}} class="primary">
<Tr t={t.loadOnce}/>
</button>
<button
class="as-link self-end"
on:click={() => guistate.openUsersettings("mangrove-reviews-allowed")}
>
Edit your privacy settings
<Tr t={t.editPrivacySettings}/>
</button>
</div>
{/if}

View file

@ -1,5 +1,4 @@
<script lang="ts">
import type { SpecialVisualizationState } from "../SpecialVisualization"
import Translations from "../i18n/Translations"
import Tr from "../Base/Tr.svelte"
import LoginToggle from "../Base/LoginToggle.svelte"
@ -7,11 +6,14 @@
import SingleReview from "./SingleReview.svelte"
import Mangrove_logo from "../../assets/svg/Mangrove_logo.svelte"
import Loading from "../Base/Loading.svelte"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { MangroveIdentity } from "../../Logic/Web/MangroveReviews"
import ThemeViewState from "../../Models/ThemeViewState"
/**
* A panel showing all the reviews by the logged-in user
*/
export let state: SpecialVisualizationState
export let state: ThemeViewState & {osmConnection: OsmConnection, userRelatedState: { mangroveIdentity: MangroveIdentity }}
let allReviews = state.userRelatedState.mangroveIdentity.getAllReviews()
let reviews = state.userRelatedState.mangroveIdentity.getGeoReviews()
let kid = state.userRelatedState.mangroveIdentity.getKeyId()

View file

@ -17,17 +17,24 @@
import ReviewForm from "./ReviewForm.svelte"
import type { Feature } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import FeatureReviews, { MangroveIdentity } from "../../Logic/Web/MangroveReviews"
import { MangroveIdentity } from "../../Logic/Web/MangroveReviews"
import type { ReviewCollection } from "../../Logic/Web/MangroveReviews"
import ShieldExclamation from "@babeard/svelte-heroicons/solid/ShieldExclamation"
import Delete_icon from "../../assets/svg/Delete_icon.svelte"
import ValidatedInput from "../InputElement/ValidatedInput.svelte"
import type { MapProperties } from "../../Models/MapProperties"
export let state: ThemeViewState = undefined
export let state: ThemeViewState & {
mapProperties?: MapProperties,
userRelatedState?: { mangroveIdentity: MangroveIdentity }
} = undefined
export let tags: UIEventSource<Record<string, string>> = undefined
export let feature: Feature = undefined
export let layer: LayerConfig = undefined
export let reviews: FeatureReviews
export let reviews: ReviewCollection
export let showMenu: boolean
export let review: Review & {
kid: string
@ -212,7 +219,7 @@
{date}
</a>
</div>
{#if review.signature}
{#if review.signature && showMenu !== undefined}
<div class="self-start">
<DotMenu>
{#if $byLoggedInUser}

View file

@ -40,7 +40,6 @@
import DrawerLeft from "./Base/DrawerLeft.svelte"
import DrawerRight from "./Base/DrawerRight.svelte"
import SearchResults from "./Search/SearchResults.svelte"
import Hash from "../Logic/Web/Hash"
import Searchbar from "./Base/Searchbar.svelte"
import ChevronRight from "@babeard/svelte-heroicons/mini/ChevronRight"
import { Drawer } from "flowbite-svelte"
@ -67,7 +66,6 @@
let selectedElement: UIEventSource<Feature> = new UIEventSource<Feature>(undefined)
let compass = Orientation.singleton.alpha
let compassLoaded = Orientation.singleton.gotMeasurement
let hash = Hash.hash
let addNewFeatureMode = state.userRelatedState.addNewFeatureMode
let gpsAvailable = state.geolocation.geolocationState.gpsAvailable
let gpsButtonAriaLabel = state.geolocation.geolocationState.gpsStateExplanation