From d662bc27078bb53c17c63b2f9dee5b6fb2da4159 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 25 Jul 2025 15:39:59 +0200 Subject: [PATCH] UI(Reviews): move 'loading allowed' to FeatureReviews --- langs/en.json | 3 + src/Logic/State/UserRelatedState.ts | 4 +- src/Logic/Web/MangroveReviews.ts | 59 +++++++++---------- .../ThemeViewState/WithUserRelatedState.ts | 33 ++++++++++- src/UI/Reviews/AllReviews.svelte | 5 +- src/UI/Reviews/ReviewForm.svelte | 17 +++--- src/UI/Reviews/ReviewPrivacyShield.svelte | 22 +++---- src/UI/Reviews/ReviewsOverview.svelte | 6 +- src/UI/Reviews/SingleReview.svelte | 17 ++++-- src/UI/ThemeViewGUI.svelte | 2 - 10 files changed, 105 insertions(+), 63 deletions(-) diff --git a/langs/en.json b/langs/en.json index b227d75bd..a00ce8cfe 100644 --- a/langs/en.json +++ b/langs/en.json @@ -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.", diff --git a/src/Logic/State/UserRelatedState.ts b/src/Logic/State/UserRelatedState.ts index 2695928c1..6f3b60b13 100644 --- a/src/Logic/State/UserRelatedState.ts +++ b/src/Logic/State/UserRelatedState.ts @@ -369,7 +369,7 @@ export default class UserRelatedState { private static initUserSettingsState(): LayerConfig { try { - return new LayerConfig(usersettings, "userinformationpanel") + return new LayerConfig(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 diff --git a/src/Logic/Web/MangroveReviews.ts b/src/Logic/Web/MangroveReviews.ts index e71fb7b5a..38ee66321 100644 --- a/src/Logic/Web/MangroveReviews.ts +++ b/src/Logic/Web/MangroveReviews.ts @@ -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 + readonly loadingAllowed: UIEventSource + removeReviewLocally(review: Review): void + + deleteReview(review: Review & { signature: string }): Promise + + createReview(review: Omit): Promise +} export class MangroveIdentity { private readonly keypair: UIEventSource = new UIEventSource( @@ -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 = {} public readonly subjectUri: Store public readonly average: Store + public readonly loadingAllowed: UIEventSource private readonly _reviews: UIEventSource< (Review & { kid: string; signature: string; madeByLoggedInUser: Store })[] > = new UIEventSource(undefined) - public readonly reviews: Store<(Review & { signature: string, madeByLoggedInUser: Store })[]> = + public readonly reviews: Store<(Review & { kid: string, signature: string, madeByLoggedInUser: Store })[]> = this._reviews private readonly _lat: number private readonly _lon: number @@ -158,23 +171,21 @@ export default class FeatureReviews { private readonly _name: Store private readonly _identity: MangroveIdentity private readonly _testmode: Store - public readonly loadingAllowed: UIEventSource private readonly _reportError: (msg: string, extra: string) => Promise private constructor( feature: Feature, tagsSource: UIEventSource>, mangroveIdentity: MangroveIdentity, + loadingAllowed?: UIEventSource, options?: Readonly<{ nameKey?: "name" | string fallbackName?: string - uncertaintyRadius?: number + uncertaintyRadius?: number, }>, testmode?: Store, - loadingAllowed?: UIEventSource, reportError?: (msg: string, extra: string) => Promise ) { - 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>, 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) ) diff --git a/src/Models/ThemeViewState/WithUserRelatedState.ts b/src/Models/ThemeViewState/WithUserRelatedState.ts index d7c5a7f5b..c4c259dbc 100644 --- a/src/Models/ThemeViewState/WithUserRelatedState.ts +++ b/src/Models/ThemeViewState/WithUserRelatedState.ts @@ -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 + + readonly overlayLayerStates: ReadonlyMap< string, { readonly isDisplayed: UIEventSource } @@ -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{ + 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 + } + } diff --git a/src/UI/Reviews/AllReviews.svelte b/src/UI/Reviews/AllReviews.svelte index 2a0267edc..e98fc083b 100644 --- a/src/UI/Reviews/AllReviews.svelte +++ b/src/UI/Reviews/AllReviews.svelte @@ -23,6 +23,7 @@ let average = reviews.average let allReviews: Store< (Review & { + kid: string, signature: string, madeByLoggedInUser: Store })[] @@ -32,7 +33,7 @@ let subject: Store = reviews.subjectUri - +
{#if $allReviews?.length > 1} @@ -43,7 +44,7 @@
{:else if $allReviews.length > 0} {#each $allReviews as review} - + {/each} {:else}
diff --git a/src/UI/Reviews/ReviewForm.svelte b/src/UI/Reviews/ReviewForm.svelte index 450228554..37fc7d55a 100644 --- a/src/UI/Reviews/ReviewForm.svelte +++ b/src/UI/Reviews/ReviewForm.svelte @@ -1,5 +1,6 @@ - + {#if uploadFailed}
@@ -188,7 +191,7 @@ {/if} - {#if $debug || $test} + {#if $debug || $isTesting} {$subject} {/if}
diff --git a/src/UI/Reviews/ReviewPrivacyShield.svelte b/src/UI/Reviews/ReviewPrivacyShield.svelte index 7b725b10b..13ca75a8f 100644 --- a/src/UI/Reviews/ReviewPrivacyShield.svelte +++ b/src/UI/Reviews/ReviewPrivacyShield.svelte @@ -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 export let hiddenIfNotAllowed: boolean = false - let allowed = reviews?.loadingAllowed ?? new ImmutableStore(true) + let t = Translations.t.reviews + -{#if $allowed} +{#if $loadingAllowed} -{:else if !hiddenIfNotAllowed && $allowed !== null} +{:else if !hiddenIfNotAllowed && $loadingAllowed !== null}
- Reviews are disabled due to your privacy settings. -
{/if} diff --git a/src/UI/Reviews/ReviewsOverview.svelte b/src/UI/Reviews/ReviewsOverview.svelte index 4df5530c2..84fd6d639 100644 --- a/src/UI/Reviews/ReviewsOverview.svelte +++ b/src/UI/Reviews/ReviewsOverview.svelte @@ -1,5 +1,4 @@