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)",
|
"affiliated_reviewer_warning": "(Affiliated review)",
|
||||||
"attribution": "By Mangrove.reviews",
|
"attribution": "By Mangrove.reviews",
|
||||||
"averageRating": "Average rating of {n} stars",
|
"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": "I am affiliated with this object",
|
||||||
"i_am_affiliated_explanation": "Check if you are an owner, creator, employee, …",
|
"i_am_affiliated_explanation": "Check if you are an owner, creator, employee, …",
|
||||||
"no_reviews_yet": "There are no reviews yet. Be the first one!",
|
"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",
|
"jspdf": "^2.5.2",
|
||||||
"latlon2country": "^1.2.7",
|
"latlon2country": "^1.2.7",
|
||||||
"libphonenumber-js": "^1.11.19",
|
"libphonenumber-js": "^1.11.19",
|
||||||
"mangrove-reviews-typescript": "^1.3.1",
|
"mangrove-reviews-typescript": "^1.4.6",
|
||||||
"maplibre-gl": "^5.1.0",
|
"maplibre-gl": "^5.1.0",
|
||||||
"marked": "^12.0.2",
|
"marked": "^12.0.2",
|
||||||
"monaco-editor": "^0.46.0",
|
"monaco-editor": "^0.46.0",
|
||||||
|
@ -19525,9 +19525,10 @@
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/mangrove-reviews-typescript": {
|
"node_modules/mangrove-reviews-typescript": {
|
||||||
"version": "1.3.1",
|
"version": "1.4.6",
|
||||||
"resolved": "https://registry.npmjs.org/mangrove-reviews-typescript/-/mangrove-reviews-typescript-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/mangrove-reviews-typescript/-/mangrove-reviews-typescript-1.4.6.tgz",
|
||||||
"integrity": "sha512-xgU4uvJ0zQTRMoA4exje509FS19jN3u/buGBSUGh1TcTQBKWBGSmaT0cnHzcH9lhg6J8LCTsgf3Mv07Y87O9/w==",
|
"integrity": "sha512-ZUM6Dath7OQEBz2oL8MKC+kMAEjIsoY0mPgAzXBcDLdKafFFOx2j6YCrMVu5RAsBhZLT77isFmbv1Ty5jKxMag==",
|
||||||
|
"license": "GPL-2.0-only",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.2.3",
|
"axios": "^1.2.3",
|
||||||
"jose": "^4.11.2",
|
"jose": "^4.11.2",
|
||||||
|
|
|
@ -225,7 +225,7 @@
|
||||||
"jspdf": "^2.5.2",
|
"jspdf": "^2.5.2",
|
||||||
"latlon2country": "^1.2.7",
|
"latlon2country": "^1.2.7",
|
||||||
"libphonenumber-js": "^1.11.19",
|
"libphonenumber-js": "^1.11.19",
|
||||||
"mangrove-reviews-typescript": "^1.3.1",
|
"mangrove-reviews-typescript": "^1.4.6",
|
||||||
"maplibre-gl": "^5.1.0",
|
"maplibre-gl": "^5.1.0",
|
||||||
"marked": "^12.0.2",
|
"marked": "^12.0.2",
|
||||||
"monaco-editor": "^0.46.0",
|
"monaco-editor": "^0.46.0",
|
||||||
|
|
|
@ -150,7 +150,7 @@ export default class FeatureReviews {
|
||||||
private readonly _reviews: UIEventSource<
|
private readonly _reviews: UIEventSource<
|
||||||
(Review & { kid: string; signature: string; madeByLoggedInUser: Store<boolean> })[]
|
(Review & { kid: string; signature: string; madeByLoggedInUser: Store<boolean> })[]
|
||||||
> = new UIEventSource([])
|
> = new UIEventSource([])
|
||||||
public readonly reviews: Store<(Review & { madeByLoggedInUser: Store<boolean> })[]> =
|
public readonly reviews: Store<(Review & { signature: string, madeByLoggedInUser: Store<boolean> })[]> =
|
||||||
this._reviews
|
this._reviews
|
||||||
private readonly _lat: number
|
private readonly _lat: number
|
||||||
private readonly _lon: number
|
private readonly _lon: number
|
||||||
|
@ -361,9 +361,10 @@ export default class FeatureReviews {
|
||||||
const keypair: CryptoKeyPair = await this._identity.getKeypair()
|
const keypair: CryptoKeyPair = await this._identity.getKeypair()
|
||||||
const jwt = await MangroveReviews.signReview(keypair, r)
|
const jwt = await MangroveReviews.signReview(keypair, r)
|
||||||
const kid = await MangroveReviews.publicToPem(keypair.publicKey)
|
const kid = await MangroveReviews.publicToPem(keypair.publicKey)
|
||||||
|
|
||||||
if (!this._testmode.data) {
|
if (!this._testmode.data) {
|
||||||
try {
|
try {
|
||||||
await MangroveReviews.submitReview(jwt)
|
await MangroveReviews.signAndSubmitReview(keypair, r)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await this._reportError(
|
await this._reportError(
|
||||||
e,
|
e,
|
||||||
|
@ -375,10 +376,11 @@ export default class FeatureReviews {
|
||||||
console.log("Testmode enabled - not uploading review")
|
console.log("Testmode enabled - not uploading review")
|
||||||
await Utils.waitFor(1000)
|
await Utils.waitFor(1000)
|
||||||
}
|
}
|
||||||
|
console.log("JWT IS", jwt)
|
||||||
const reviewWithKid = {
|
const reviewWithKid = {
|
||||||
...r,
|
...r,
|
||||||
kid,
|
kid,
|
||||||
signature: jwt,
|
signature: "",
|
||||||
madeByLoggedInUser: new ImmutableStore(true),
|
madeByLoggedInUser: new ImmutableStore(true),
|
||||||
}
|
}
|
||||||
this._reviews.data.push(reviewWithKid)
|
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
|
* Gets an URI which represents the item in a mangrove-compatible way
|
||||||
*
|
*
|
||||||
|
|
|
@ -8,15 +8,21 @@
|
||||||
import Tr from "../Base/Tr.svelte"
|
import Tr from "../Base/Tr.svelte"
|
||||||
import ReviewPrivacyShield from "./ReviewPrivacyShield.svelte"
|
import ReviewPrivacyShield from "./ReviewPrivacyShield.svelte"
|
||||||
import ThemeViewState from "../../Models/ThemeViewState"
|
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
|
* An element showing all reviews
|
||||||
*/
|
*/
|
||||||
export let reviews: FeatureReviews
|
export let reviews: FeatureReviews
|
||||||
export let state: ThemeViewState
|
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 average = reviews.average
|
||||||
let allReviews: Store<
|
let allReviews: Store<
|
||||||
(Review & {
|
(Review & {
|
||||||
|
signature: string,
|
||||||
madeByLoggedInUser: Store<boolean>
|
madeByLoggedInUser: Store<boolean>
|
||||||
})[]
|
})[]
|
||||||
> = reviews.reviews.map((r) => Utils.NoNull(r))
|
> = reviews.reviews.map((r) => Utils.NoNull(r))
|
||||||
|
@ -26,13 +32,13 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ReviewPrivacyShield {reviews} guistate={state.guistate}>
|
<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}
|
{#if $allReviews.length > 1}
|
||||||
<StarsBar score={$average} />
|
<StarsBar score={$average} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if $allReviews.length > 0}
|
{#if $allReviews.length > 0}
|
||||||
{#each $allReviews as review}
|
{#each $allReviews as review}
|
||||||
<SingleReview {review} />
|
<SingleReview {review} {state} {tags} {feature} {layer} {reviews}/>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="subtle m-2 italic">
|
<div class="subtle m-2 italic">
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import FeatureReviews from "../../Logic/Web/MangroveReviews"
|
import FeatureReviews from "../../Logic/Web/MangroveReviews"
|
||||||
import StarsBar from "./StarsBar.svelte"
|
import StarsBar from "./StarsBar.svelte"
|
||||||
import SpecialTranslation from "../Popup/TagRendering/SpecialTranslation.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 type { Feature } from "geojson"
|
||||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../i18n/Translations"
|
||||||
|
@ -10,7 +10,7 @@
|
||||||
import Tr from "../Base/Tr.svelte"
|
import Tr from "../Base/Tr.svelte"
|
||||||
import If from "../Base/If.svelte"
|
import If from "../Base/If.svelte"
|
||||||
import Loading from "../Base/Loading.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 { placeholder } from "../../Utils/placeholder"
|
||||||
import { ExclamationTriangle } from "@babeard/svelte-heroicons/solid/ExclamationTriangle"
|
import { ExclamationTriangle } from "@babeard/svelte-heroicons/solid/ExclamationTriangle"
|
||||||
import ReviewPrivacyShield from "./ReviewPrivacyShield.svelte"
|
import ReviewPrivacyShield from "./ReviewPrivacyShield.svelte"
|
||||||
|
@ -26,11 +26,12 @@
|
||||||
* This is multi-stepped.
|
* This is multi-stepped.
|
||||||
*/
|
*/
|
||||||
export let reviews: FeatureReviews
|
export let reviews: FeatureReviews
|
||||||
|
export let editReview: Review & { signature: string } = undefined
|
||||||
let score = 0
|
let subject: Store<string> = editReview !== undefined ? new ImmutableStore(editReview.sub) : reviews.subjectUri
|
||||||
let confirmedScore = undefined
|
let score = editReview?.rating ?? 0
|
||||||
let isAffiliated = new UIEventSource(false)
|
let confirmedScore = editReview?.rating
|
||||||
let opinion = new UIEventSource<string>(undefined)
|
let isAffiliated = new UIEventSource(editReview?.metadata?.is_affiliated ?? false)
|
||||||
|
let opinion = new UIEventSource<string>(editReview?.opinion)
|
||||||
|
|
||||||
const t = Translations.t.reviews
|
const t = Translations.t.reviews
|
||||||
|
|
||||||
|
@ -47,6 +48,7 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
let uploadFailed: string = undefined
|
let uploadFailed: string = undefined
|
||||||
|
let isTesting = state?.featureSwitchIsTesting
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
if (hasError.data) {
|
if (hasError.data) {
|
||||||
|
@ -62,18 +64,42 @@
|
||||||
opinion: opinion.data,
|
opinion: opinion.data,
|
||||||
metadata: { nickname, is_affiliated: isAffiliated.data },
|
metadata: { nickname, is_affiliated: isAffiliated.data },
|
||||||
}
|
}
|
||||||
try {
|
if (!isTesting?.data) {
|
||||||
await reviews.createReview(review)
|
try {
|
||||||
} catch (e) {
|
if (editReview) {
|
||||||
console.error("Could not create review due to", e)
|
const payload = {
|
||||||
uploadFailed = "" + e
|
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"
|
_state = "done"
|
||||||
}
|
}
|
||||||
|
|
||||||
let test = state.featureSwitches.featureSwitchIsTesting
|
let test = state.featureSwitches.featureSwitchIsTesting
|
||||||
let debug = state.featureSwitches.featureSwitchIsDebugging
|
let debug = state.featureSwitches.featureSwitchIsDebugging
|
||||||
let subject = reviews.subjectUri
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ReviewPrivacyShield hiddenIfNotAllowed {reviews} guistate={state.guistate}>
|
<ReviewPrivacyShield hiddenIfNotAllowed {reviews} guistate={state.guistate}>
|
||||||
|
@ -156,7 +182,7 @@
|
||||||
<Tr slot="else" t={t.reviewing_as_anonymous} />
|
<Tr slot="else" t={t.reviewing_as_anonymous} />
|
||||||
</If>
|
</If>
|
||||||
<button class="primary" class:disabled={$hasError !== undefined} on:click={save}>
|
<button class="primary" class:disabled={$hasError !== undefined} on:click={save}>
|
||||||
<Tr t={t.save} />
|
<Tr t={ editReview === undefined ? t.save : t.edit} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,12 @@
|
||||||
*/
|
*/
|
||||||
import FeatureReviews from "../../Logic/Web/MangroveReviews"
|
import FeatureReviews from "../../Logic/Web/MangroveReviews"
|
||||||
import { MenuState } from "../../Models/MenuState"
|
import { MenuState } from "../../Models/MenuState"
|
||||||
|
import { ImmutableStore } from "../../Logic/UIEventSource"
|
||||||
|
|
||||||
export let guistate: MenuState
|
export let guistate: MenuState
|
||||||
export let reviews: FeatureReviews
|
export let reviews: FeatureReviews
|
||||||
export let hiddenIfNotAllowed: boolean = false
|
export let hiddenIfNotAllowed: boolean = false
|
||||||
let allowed = reviews.loadingAllowed
|
let allowed = reviews?.loadingAllowed ?? new ImmutableStore(true)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $allowed}
|
{#if $allowed}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Review } from "mangrove-reviews-typescript"
|
import { MangroveReviews, Review } from "mangrove-reviews-typescript"
|
||||||
import { ImmutableStore, Store } 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"
|
||||||
import Tr from "../Base/Tr.svelte"
|
import Tr from "../Base/Tr.svelte"
|
||||||
|
@ -9,8 +9,22 @@
|
||||||
import Markdown from "../Base/Markdown.svelte"
|
import Markdown from "../Base/Markdown.svelte"
|
||||||
import AccordionSingle from "../Flowbite/AccordionSingle.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 { 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 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 & {
|
export let review: Review & {
|
||||||
kid: string
|
kid: string
|
||||||
signature: string
|
signature: string
|
||||||
|
@ -35,8 +49,61 @@
|
||||||
|
|
||||||
state?.guistate?.closeAll()
|
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>
|
</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
|
<div
|
||||||
class={"low-interaction flex flex-col rounded-lg p-1 px-2" +
|
class={"low-interaction flex flex-col rounded-lg p-1 px-2" +
|
||||||
($byLoggedInUser ? "border-interactive" : "")}
|
($byLoggedInUser ? "border-interactive" : "")}
|
||||||
|
@ -79,10 +146,26 @@
|
||||||
{date}
|
{date}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{#if review.opinion}
|
{#if review.opinion}
|
||||||
<div class="disable-links">
|
<div class="disable-links" class:strikethrough={$deleted}>
|
||||||
<Markdown src={review.opinion} />
|
<Markdown src={review.opinion} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -99,4 +182,9 @@
|
||||||
{#if review.metadata.is_affiliated}
|
{#if review.metadata.is_affiliated}
|
||||||
<Tr t={Translations.t.reviews.affiliated_reviewer_warning} />
|
<Tr t={Translations.t.reviews.affiliated_reviewer_warning} />
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if $isDebugging}
|
||||||
|
<div class="subtle">
|
||||||
|
Maresi: {(review.signature)}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue