forked from MapComplete/MapComplete
Feature(reviews): support deleting and editing reviews, fix #2129
This commit is contained in:
parent
3ee9ee9d88
commit
06ff00f7f7
8 changed files with 165 additions and 29 deletions
|
@ -802,6 +802,11 @@
|
|||
"affiliated_reviewer_warning": "(Affiliated review)",
|
||||
"attribution": "By Mangrove.reviews",
|
||||
"averageRating": "Average rating of {n} stars",
|
||||
"delete": "Delete review",
|
||||
"deleteConfirm": "Permantently delete this review",
|
||||
"deleteText": "This cannot be undone",
|
||||
"deleteTitle": "Delete this review?",
|
||||
"edit": "Edit review",
|
||||
"i_am_affiliated": "I am affiliated with this object",
|
||||
"i_am_affiliated_explanation": "Check if you are an owner, creator, employee, …",
|
||||
"no_reviews_yet": "There are no reviews yet. Be the first one!",
|
||||
|
|
9
package-lock.json
generated
9
package-lock.json
generated
|
@ -61,7 +61,7 @@
|
|||
"jspdf": "^2.5.2",
|
||||
"latlon2country": "^1.2.7",
|
||||
"libphonenumber-js": "^1.11.19",
|
||||
"mangrove-reviews-typescript": "^1.3.1",
|
||||
"mangrove-reviews-typescript": "^1.4.6",
|
||||
"maplibre-gl": "^5.1.0",
|
||||
"marked": "^12.0.2",
|
||||
"monaco-editor": "^0.46.0",
|
||||
|
@ -19525,9 +19525,10 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/mangrove-reviews-typescript": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/mangrove-reviews-typescript/-/mangrove-reviews-typescript-1.3.1.tgz",
|
||||
"integrity": "sha512-xgU4uvJ0zQTRMoA4exje509FS19jN3u/buGBSUGh1TcTQBKWBGSmaT0cnHzcH9lhg6J8LCTsgf3Mv07Y87O9/w==",
|
||||
"version": "1.4.6",
|
||||
"resolved": "https://registry.npmjs.org/mangrove-reviews-typescript/-/mangrove-reviews-typescript-1.4.6.tgz",
|
||||
"integrity": "sha512-ZUM6Dath7OQEBz2oL8MKC+kMAEjIsoY0mPgAzXBcDLdKafFFOx2j6YCrMVu5RAsBhZLT77isFmbv1Ty5jKxMag==",
|
||||
"license": "GPL-2.0-only",
|
||||
"dependencies": {
|
||||
"axios": "^1.2.3",
|
||||
"jose": "^4.11.2",
|
||||
|
|
|
@ -225,7 +225,7 @@
|
|||
"jspdf": "^2.5.2",
|
||||
"latlon2country": "^1.2.7",
|
||||
"libphonenumber-js": "^1.11.19",
|
||||
"mangrove-reviews-typescript": "^1.3.1",
|
||||
"mangrove-reviews-typescript": "^1.4.6",
|
||||
"maplibre-gl": "^5.1.0",
|
||||
"marked": "^12.0.2",
|
||||
"monaco-editor": "^0.46.0",
|
||||
|
|
|
@ -150,7 +150,7 @@ export default class FeatureReviews {
|
|||
private readonly _reviews: UIEventSource<
|
||||
(Review & { kid: string; signature: string; madeByLoggedInUser: Store<boolean> })[]
|
||||
> = new UIEventSource([])
|
||||
public readonly reviews: Store<(Review & { madeByLoggedInUser: Store<boolean> })[]> =
|
||||
public readonly reviews: Store<(Review & { signature: string, madeByLoggedInUser: Store<boolean> })[]> =
|
||||
this._reviews
|
||||
private readonly _lat: number
|
||||
private readonly _lon: number
|
||||
|
@ -361,9 +361,10 @@ export default class FeatureReviews {
|
|||
const keypair: CryptoKeyPair = await this._identity.getKeypair()
|
||||
const jwt = await MangroveReviews.signReview(keypair, r)
|
||||
const kid = await MangroveReviews.publicToPem(keypair.publicKey)
|
||||
|
||||
if (!this._testmode.data) {
|
||||
try {
|
||||
await MangroveReviews.submitReview(jwt)
|
||||
await MangroveReviews.signAndSubmitReview(keypair, r)
|
||||
} catch (e) {
|
||||
await this._reportError(
|
||||
e,
|
||||
|
@ -375,10 +376,11 @@ export default class FeatureReviews {
|
|||
console.log("Testmode enabled - not uploading review")
|
||||
await Utils.waitFor(1000)
|
||||
}
|
||||
console.log("JWT IS", jwt)
|
||||
const reviewWithKid = {
|
||||
...r,
|
||||
kid,
|
||||
signature: jwt,
|
||||
signature: "",
|
||||
madeByLoggedInUser: new ImmutableStore(true),
|
||||
}
|
||||
this._reviews.data.push(reviewWithKid)
|
||||
|
@ -448,6 +450,13 @@ export default class FeatureReviews {
|
|||
}
|
||||
}
|
||||
|
||||
public async deleteReview(review: Review & {signature: string}){
|
||||
await MangroveReviews.deleteReview(await this._identity.getKeypair(), review)
|
||||
this._reviews.set(
|
||||
this._reviews.data.filter(r => r !== review)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an URI which represents the item in a mangrove-compatible way
|
||||
*
|
||||
|
|
|
@ -8,15 +8,21 @@
|
|||
import Tr from "../Base/Tr.svelte"
|
||||
import ReviewPrivacyShield from "./ReviewPrivacyShield.svelte"
|
||||
import ThemeViewState from "../../Models/ThemeViewState"
|
||||
import type { Store } from "../../Logic/UIEventSource"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Feature } from "geojson"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
/**
|
||||
* An element showing all reviews
|
||||
*/
|
||||
export let reviews: FeatureReviews
|
||||
export let state: ThemeViewState
|
||||
export let tags: UIEventSource<Record<string, string>> = undefined
|
||||
export let feature: Feature = undefined
|
||||
export let layer: LayerConfig =undefined
|
||||
let average = reviews.average
|
||||
let allReviews: Store<
|
||||
(Review & {
|
||||
signature: string,
|
||||
madeByLoggedInUser: Store<boolean>
|
||||
})[]
|
||||
> = reviews.reviews.map((r) => Utils.NoNull(r))
|
||||
|
@ -26,13 +32,13 @@
|
|||
</script>
|
||||
|
||||
<ReviewPrivacyShield {reviews} guistate={state.guistate}>
|
||||
<div class="border-2 border-dashed border-gray-300 p-2">
|
||||
<div class="border-2 border-dashed border-gray-300 p-2 flex flex-col gap-y-2">
|
||||
{#if $allReviews.length > 1}
|
||||
<StarsBar score={$average} />
|
||||
{/if}
|
||||
{#if $allReviews.length > 0}
|
||||
{#each $allReviews as review}
|
||||
<SingleReview {review} />
|
||||
<SingleReview {review} {state} {tags} {feature} {layer} {reviews}/>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="subtle m-2 italic">
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import FeatureReviews from "../../Logic/Web/MangroveReviews"
|
||||
import StarsBar from "./StarsBar.svelte"
|
||||
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.svelte"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import type { Feature } from "geojson"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
@ -10,7 +10,7 @@
|
|||
import Tr from "../Base/Tr.svelte"
|
||||
import If from "../Base/If.svelte"
|
||||
import Loading from "../Base/Loading.svelte"
|
||||
import { Review } from "mangrove-reviews-typescript"
|
||||
import { MangroveReviews, Review } from "mangrove-reviews-typescript"
|
||||
import { placeholder } from "../../Utils/placeholder"
|
||||
import { ExclamationTriangle } from "@babeard/svelte-heroicons/solid/ExclamationTriangle"
|
||||
import ReviewPrivacyShield from "./ReviewPrivacyShield.svelte"
|
||||
|
@ -26,11 +26,12 @@
|
|||
* This is multi-stepped.
|
||||
*/
|
||||
export let reviews: FeatureReviews
|
||||
|
||||
let score = 0
|
||||
let confirmedScore = undefined
|
||||
let isAffiliated = new UIEventSource(false)
|
||||
let opinion = new UIEventSource<string>(undefined)
|
||||
export let editReview: Review & { signature: string } = undefined
|
||||
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)
|
||||
let opinion = new UIEventSource<string>(editReview?.opinion)
|
||||
|
||||
const t = Translations.t.reviews
|
||||
|
||||
|
@ -47,6 +48,7 @@
|
|||
})
|
||||
|
||||
let uploadFailed: string = undefined
|
||||
let isTesting = state?.featureSwitchIsTesting
|
||||
|
||||
async function save() {
|
||||
if (hasError.data) {
|
||||
|
@ -62,18 +64,42 @@
|
|||
opinion: opinion.data,
|
||||
metadata: { nickname, is_affiliated: isAffiliated.data },
|
||||
}
|
||||
try {
|
||||
await reviews.createReview(review)
|
||||
} catch (e) {
|
||||
console.error("Could not create review due to", e)
|
||||
uploadFailed = "" + e
|
||||
if (!isTesting?.data) {
|
||||
try {
|
||||
if (editReview) {
|
||||
const payload = {
|
||||
opinion: opinion.data,
|
||||
rating: confirmedScore,
|
||||
iat: Math.floor(new Date().getTime()),
|
||||
sub: editReview.sub,
|
||||
metadata: {
|
||||
nickname,
|
||||
},
|
||||
}
|
||||
if (editReview.images) {
|
||||
review.images = editReview.images
|
||||
}
|
||||
if (isAffiliated.data) {
|
||||
review.metadata.is_affiliated = isAffiliated.data
|
||||
}
|
||||
await reviews.createReview(payload)
|
||||
await reviews.deleteReview(editReview)
|
||||
} else {
|
||||
await reviews.createReview(review)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Could not create review due to", e)
|
||||
uploadFailed = "" + e
|
||||
}
|
||||
} else {
|
||||
console.log("Not actually saving, testing mode is on")
|
||||
}
|
||||
|
||||
_state = "done"
|
||||
}
|
||||
|
||||
let test = state.featureSwitches.featureSwitchIsTesting
|
||||
let debug = state.featureSwitches.featureSwitchIsDebugging
|
||||
let subject = reviews.subjectUri
|
||||
</script>
|
||||
|
||||
<ReviewPrivacyShield hiddenIfNotAllowed {reviews} guistate={state.guistate}>
|
||||
|
@ -156,7 +182,7 @@
|
|||
<Tr slot="else" t={t.reviewing_as_anonymous} />
|
||||
</If>
|
||||
<button class="primary" class:disabled={$hasError !== undefined} on:click={save}>
|
||||
<Tr t={t.save} />
|
||||
<Tr t={ editReview === undefined ? t.save : t.edit} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -4,11 +4,12 @@
|
|||
*/
|
||||
import FeatureReviews from "../../Logic/Web/MangroveReviews"
|
||||
import { MenuState } from "../../Models/MenuState"
|
||||
import { ImmutableStore } from "../../Logic/UIEventSource"
|
||||
|
||||
export let guistate: MenuState
|
||||
export let reviews: FeatureReviews
|
||||
export let hiddenIfNotAllowed: boolean = false
|
||||
let allowed = reviews.loadingAllowed
|
||||
let allowed = reviews?.loadingAllowed ?? new ImmutableStore(true)
|
||||
</script>
|
||||
|
||||
{#if $allowed}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { Review } from "mangrove-reviews-typescript"
|
||||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||
import { MangroveReviews, Review } from "mangrove-reviews-typescript"
|
||||
import { ImmutableStore, Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import StarsBar from "./StarsBar.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
|
@ -9,8 +9,22 @@
|
|||
import Markdown from "../Base/Markdown.svelte"
|
||||
import AccordionSingle from "../Flowbite/AccordionSingle.svelte"
|
||||
import AttributedImage from "../Image/AttributedImage.svelte"
|
||||
import DotMenu from "../Base/DotMenu.svelte"
|
||||
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||
import Popup from "../Base/Popup.svelte"
|
||||
import NextButton from "../Base/NextButton.svelte"
|
||||
import { PencilIcon } from "@babeard/svelte-heroicons/solid"
|
||||
import ReviewForm from "./ReviewForm.svelte"
|
||||
import type { Feature } from "geojson"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import FeatureReviews from "../../Logic/Web/MangroveReviews"
|
||||
|
||||
export let state: ThemeViewState = 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 review: Review & {
|
||||
kid: string
|
||||
signature: string
|
||||
|
@ -35,8 +49,61 @@
|
|||
|
||||
state?.guistate?.closeAll()
|
||||
}
|
||||
|
||||
|
||||
let isTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false)
|
||||
|
||||
function report() {
|
||||
MangroveReviews.reportAbuseReview(identity, review, "This review was reported by a MapComplete user")
|
||||
}
|
||||
|
||||
let deleted = new UIEventSource(false)
|
||||
|
||||
async function deleteR() {
|
||||
const identity: MangroveIdentity = await state?.userRelatedState?.mangroveIdentity.getKeypair()
|
||||
console.log("Deleting review...", identity)
|
||||
if (!isTesting.data) {
|
||||
|
||||
try {
|
||||
reviews.deleteReview(review)
|
||||
} catch (e) {
|
||||
console.log("Could not delete review... ", e)
|
||||
}
|
||||
} else {
|
||||
console.log("Not really deleting the review, testing")
|
||||
}
|
||||
showDelete.setData(false)
|
||||
deleted.setData(true)
|
||||
}
|
||||
|
||||
let showDelete = new UIEventSource(false)
|
||||
let showEdit = new UIEventSource(false)
|
||||
let isDebugging = state?.featureSwitches?.featureSwitchIsDebugging
|
||||
const t = Translations.t.reviews
|
||||
</script>
|
||||
|
||||
<Popup shown={showDelete}>
|
||||
<svelte:fragment slot="header">
|
||||
<Tr t={t.deleteTitle} />
|
||||
</svelte:fragment>
|
||||
|
||||
<Tr t={t.deleteText} />
|
||||
|
||||
<NextButton clss="primary" on:click={() => deleteR()}>
|
||||
<TrashIcon class="w-12 text-red-600" />
|
||||
<Tr t={t.deleteConfirm} />
|
||||
</NextButton>
|
||||
</Popup>
|
||||
|
||||
<Popup shown={showEdit}>
|
||||
<svelte:fragment slot="header">
|
||||
<Tr t={t.edit} />
|
||||
</svelte:fragment>
|
||||
|
||||
<ReviewForm {state} editReview={review} {reviews} />
|
||||
|
||||
</Popup>
|
||||
|
||||
<div
|
||||
class={"low-interaction flex flex-col rounded-lg p-1 px-2" +
|
||||
($byLoggedInUser ? "border-interactive" : "")}
|
||||
|
@ -79,10 +146,26 @@
|
|||
{date}
|
||||
</a>
|
||||
</div>
|
||||
{#if review.signature}
|
||||
<div class="self-start">
|
||||
<DotMenu>
|
||||
{#if byLoggedInUser}
|
||||
<button on:click={() => showEdit.set(true)}>
|
||||
<PencilIcon />
|
||||
<Tr t={t.edit} />
|
||||
</button>
|
||||
<button on:click={() => showDelete.set(true)}>
|
||||
<TrashIcon />
|
||||
<Tr t={t.delete} />
|
||||
</button>
|
||||
{/if}
|
||||
</DotMenu>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if review.opinion}
|
||||
<div class="disable-links">
|
||||
<div class="disable-links" class:strikethrough={$deleted}>
|
||||
<Markdown src={review.opinion} />
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -99,4 +182,9 @@
|
|||
{#if review.metadata.is_affiliated}
|
||||
<Tr t={Translations.t.reviews.affiliated_reviewer_warning} />
|
||||
{/if}
|
||||
{#if $isDebugging}
|
||||
<div class="subtle">
|
||||
Maresi: {(review.signature)}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue