Feature(reviews): fix #2457, add report functionality with reason

This commit is contained in:
Pieter Vander Vennet 2025-07-13 15:58:05 +02:00
parent dc62db7dcf
commit 7ce320075d
3 changed files with 89 additions and 10 deletions

View file

@ -817,6 +817,9 @@
"question_opinion": "How was your experience?", "question_opinion": "How was your experience?",
"rate": "Rate {n} stars", "rate": "Rate {n} stars",
"rated": "Rated {n} stars", "rated": "Rated {n} stars",
"reportReason": "Why should this report be reported?",
"reportReview": "Report this review as inappropriate",
"reportReviewTitle": "Report this review as inappropriate?",
"reviewPlaceholder": "Describe your experience…", "reviewPlaceholder": "Describe your experience…",
"reviewing_as": "Reviewing as {nickname}", "reviewing_as": "Reviewing as {nickname}",
"reviewing_as_anonymous": "Reviewing as anonymous", "reviewing_as_anonymous": "Reviewing as anonymous",

View file

@ -452,9 +452,14 @@ export default class FeatureReviews {
public async deleteReview(review: Review & {signature: string}){ public async deleteReview(review: Review & {signature: string}){
await MangroveReviews.deleteReview(await this._identity.getKeypair(), review) await MangroveReviews.deleteReview(await this._identity.getKeypair(), review)
this.removeReviewLocally(review)
}
public removeReviewLocally(review: Review){
this._reviews.set( this._reviews.set(
this._reviews.data.filter(r => r !== review) this._reviews.data.filter(r => r !== review)
) )
} }
/** /**

View file

@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { MangroveReviews, Review } from "mangrove-reviews-typescript" import { MangroveReviews, Review } from "mangrove-reviews-typescript"
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource" import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
import StarsBar from "./StarsBar.svelte" import StarsBar from "./StarsBar.svelte"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
@ -7,7 +8,6 @@
import { ariaLabel } from "../../Utils/ariaLabel" import { ariaLabel } from "../../Utils/ariaLabel"
import ThemeViewState from "../../Models/ThemeViewState" import ThemeViewState from "../../Models/ThemeViewState"
import Markdown from "../Base/Markdown.svelte" import Markdown from "../Base/Markdown.svelte"
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
import AttributedImage from "../Image/AttributedImage.svelte" import AttributedImage from "../Image/AttributedImage.svelte"
import DotMenu from "../Base/DotMenu.svelte" import DotMenu from "../Base/DotMenu.svelte"
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid" import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
@ -17,7 +17,11 @@
import ReviewForm from "./ReviewForm.svelte" import ReviewForm from "./ReviewForm.svelte"
import type { Feature } from "geojson" import type { Feature } from "geojson"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import FeatureReviews from "../../Logic/Web/MangroveReviews" import FeatureReviews, { MangroveIdentity } 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"
export let state: ThemeViewState = undefined export let state: ThemeViewState = undefined
export let tags: UIEventSource<Record<string, string>> = undefined export let tags: UIEventSource<Record<string, string>> = undefined
@ -53,21 +57,39 @@
let isTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false) let isTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false)
function report() { async function report() {
MangroveReviews.reportAbuseReview(identity, review, "This review was reported by a MapComplete user") let reason = reportReason.data
if (!reason) {
reason = "This review was reported by a MapComplete user"
}
const identity: CryptoKeyPair = await state?.userRelatedState?.mangroveIdentity.getKeypair()
if (!isTesting.data) {
try {
await MangroveReviews.reportAbuseReview(identity, review, reason)
} catch (e) {
console.error("Could not report review due to", e)
await state.reportError(e)
}
} else {
console.log("Not actually reporting, test mode")
}
showReport.set(false)
reviews.removeReviewLocally(review)
} }
let deleted = new UIEventSource(false) let deleted = new UIEventSource(false)
async function deleteR() { async function deleteR() {
const identity: MangroveIdentity = await state?.userRelatedState?.mangroveIdentity.getKeypair() const identity: CryptoKeyPair = await state?.userRelatedState?.mangroveIdentity.getKeypair()
console.log("Deleting review...", identity) console.log("Deleting review...", identity)
if (!isTesting.data) { if (!isTesting.data) {
try { try {
reviews.deleteReview(review) await reviews.deleteReview(review)
} catch (e) { } catch (e) {
console.log("Could not delete review... ", e) console.log("Could not delete review... ", e)
await state.reportError(e)
} }
} else { } else {
console.log("Not really deleting the review, testing") console.log("Not really deleting the review, testing")
@ -76,8 +98,10 @@
deleted.setData(true) deleted.setData(true)
} }
let reportReason = new UIEventSource("")
let showDelete = new UIEventSource(false) let showDelete = new UIEventSource(false)
let showEdit = new UIEventSource(false) let showEdit = new UIEventSource(false)
let showReport = new UIEventSource(false)
let isDebugging = state?.featureSwitches?.featureSwitchIsDebugging let isDebugging = state?.featureSwitches?.featureSwitchIsDebugging
const t = Translations.t.reviews const t = Translations.t.reviews
</script> </script>
@ -87,20 +111,62 @@
<Tr t={t.deleteTitle} /> <Tr t={t.deleteTitle} />
</svelte:fragment> </svelte:fragment>
<div class="flex gap-x-4 items-center m-8">
<Delete_icon class="w-16" />
<div class="p-4 low-interaction border-interactive">
<StarsBar starSize="w-4 h-4 md:w-6 md:h-6" readonly={true} score={review.rating} />
<div class="disable-links">
<Markdown src={review.opinion} />
</div>
</div>
</div>
<Tr t={t.deleteText} /> <Tr t={t.deleteText} />
<NextButton clss="primary" on:click={() => deleteR()}> <NextButton clss="primary float-right" on:click={() => deleteR()}>
<TrashIcon class="w-12 text-red-600" /> <TrashIcon class="w-12 text-red-600" />
<Tr t={t.deleteConfirm} /> <Tr t={t.deleteConfirm} />
</NextButton> </NextButton>
</Popup> </Popup>
<Popup shown={showReport}>
<svelte:fragment slot="header">
<Tr t={t.reportReviewTitle} />
</svelte:fragment>
<div class="flex gap-x-4 items-center m-8">
<ShieldExclamation class="w-16" />
<div class="p-4 low-interaction border-interactive">
<StarsBar starSize="w-4 h-4 md:w-6 md:h-6" readonly={true} score={review.rating} />
<div class="disable-links">
<Markdown src={review.opinion} />
</div>
</div>
</div>
<h3>
<Tr t={t.reportReason} />
</h3>
<div class="w-full">
<ValidatedInput type="text" value={reportReason} />
</div>
<Tr t={t.deleteText} />
<NextButton clss="primary float-right" on:click={() => {report()}}>
<TrashIcon class="w-12 text-red-600" />
<Tr t={t.reportReview} />
</NextButton>
</Popup>
<Popup shown={showEdit}> <Popup shown={showEdit}>
<svelte:fragment slot="header"> <svelte:fragment slot="header">
<Tr t={t.edit} /> <Tr t={t.edit} />
</svelte:fragment> </svelte:fragment>
<ReviewForm {state} editReview={review} {reviews} /> <ReviewForm {tags} {feature} {layer} {state} editReview={review} {reviews} />
</Popup> </Popup>
@ -149,7 +215,7 @@
{#if review.signature} {#if review.signature}
<div class="self-start"> <div class="self-start">
<DotMenu> <DotMenu>
{#if byLoggedInUser} {#if $byLoggedInUser}
<button on:click={() => showEdit.set(true)}> <button on:click={() => showEdit.set(true)}>
<PencilIcon /> <PencilIcon />
<Tr t={t.edit} /> <Tr t={t.edit} />
@ -158,6 +224,11 @@
<TrashIcon /> <TrashIcon />
<Tr t={t.delete} /> <Tr t={t.delete} />
</button> </button>
{:else}
<button on:click={() => showReport.set(true)}>
<ShieldExclamation />
<Tr t={t.reportReview} />
</button>
{/if} {/if}
</DotMenu> </DotMenu>
</div> </div>