forked from MapComplete/MapComplete
UI(Reviews): move 'loading allowed' to FeatureReviews
This commit is contained in:
parent
ce3a049d9d
commit
d662bc2707
10 changed files with 105 additions and 63 deletions
|
@ -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.",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue