Reviews: hopefully fix #1782, add review overview
This commit is contained in:
parent
8be41571fa
commit
592adfdf2a
11 changed files with 172 additions and 32 deletions
|
@ -671,6 +671,7 @@
|
|||
"reviewPlaceholder": "Describe your experience…",
|
||||
"reviewing_as": "Reviewing as {nickname}",
|
||||
"reviewing_as_anonymous": "Reviewing as anonymous",
|
||||
"reviews_bug": "Expected more reviews? Some reviews are not displayed due to a bug.",
|
||||
"save": "Save review",
|
||||
"saved": "Review saved. Thanks for sharing!",
|
||||
"saving_review": "Saving…",
|
||||
|
@ -678,7 +679,9 @@
|
|||
"title_singular": "One review",
|
||||
"too_long": "At most {max} characters are allowed. Your review has {amount} characters.",
|
||||
"tos": "If you create a review, you agree to <a href='https://mangrove.reviews/terms' target='_blank'>the TOS and privacy policy of Mangrove.reviews</a>",
|
||||
"write_a_comment": "Leave a review…"
|
||||
"write_a_comment": "Leave a review…",
|
||||
"your_reviews": "Your previous reviews",
|
||||
"your_reviews_empty": "We couldn't find any of your previous reviews"
|
||||
},
|
||||
"split": {
|
||||
"cancel": "Cancel",
|
||||
|
|
|
@ -73,7 +73,6 @@ export default class UserRelatedState {
|
|||
|
||||
constructor(
|
||||
osmConnection: OsmConnection,
|
||||
availableLanguages?: string[],
|
||||
layout?: LayoutConfig,
|
||||
featureSwitches?: FeatureSwitchState,
|
||||
mapProperties?: MapProperties
|
||||
|
@ -365,6 +364,11 @@ export default class UserRelatedState {
|
|||
[translationMode]
|
||||
)
|
||||
|
||||
this.mangroveIdentity.getKeyId().addCallbackAndRun(kid => {
|
||||
amendedPrefs.data["mangrove_kid"] = kid
|
||||
amendedPrefs.ping()
|
||||
})
|
||||
|
||||
const usersettingMetaTagging = new ThemeMetaTagging()
|
||||
osmConnection.userDetails.addCallback((userDetails) => {
|
||||
for (const k in userDetails) {
|
||||
|
|
|
@ -306,7 +306,8 @@ export abstract class Store<T> implements Readable<T> {
|
|||
|
||||
export class ImmutableStore<T> extends Store<T> {
|
||||
public readonly data: T
|
||||
|
||||
static FALSE = new ImmutableStore<boolean>(false)
|
||||
static TRUE = new ImmutableStore<boolean>(true)
|
||||
constructor(data: T) {
|
||||
super()
|
||||
this.data = data
|
||||
|
|
|
@ -5,10 +5,12 @@ import { Feature, Position } from "geojson"
|
|||
import { GeoOperations } from "../GeoOperations"
|
||||
|
||||
export class MangroveIdentity {
|
||||
public readonly keypair: Store<CryptoKeyPair>
|
||||
public readonly key_id: Store<string>
|
||||
private readonly keypair: Store<CryptoKeyPair>
|
||||
private readonly mangroveIdentity: UIEventSource<string>
|
||||
private readonly key_id: Store<string>
|
||||
|
||||
constructor(mangroveIdentity: UIEventSource<string>) {
|
||||
this.mangroveIdentity = mangroveIdentity
|
||||
const key_id = new UIEventSource<string>(undefined)
|
||||
this.key_id = key_id
|
||||
const keypairEventSource = new UIEventSource<CryptoKeyPair>(undefined)
|
||||
|
@ -23,13 +25,7 @@ export class MangroveIdentity {
|
|||
key_id.setData(pem)
|
||||
})
|
||||
|
||||
try {
|
||||
if (!Utils.runningFromConsole && (mangroveIdentity.data ?? "") === "") {
|
||||
MangroveIdentity.CreateIdentity(mangroveIdentity).then((_) => {})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Could not create identity: ", e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -44,8 +40,61 @@ export class MangroveIdentity {
|
|||
// Identity has been loaded via osmPreferences by now - we don't overwrite
|
||||
return
|
||||
}
|
||||
console.log("Creating a new Mangrove identity!")
|
||||
identity.setData(JSON.stringify(jwk))
|
||||
}
|
||||
|
||||
/**
|
||||
* Only called to create a review.
|
||||
*/
|
||||
async getKeypair(): Promise<CryptoKeyPair> {
|
||||
if(this.keypair.data ?? "" === ""){
|
||||
// We want to create a review, but it seems like no key has been setup at this moment
|
||||
// We create the key
|
||||
try {
|
||||
if (!Utils.runningFromConsole && (this.mangroveIdentity.data ?? "") === "") {
|
||||
await MangroveIdentity.CreateIdentity(this.mangroveIdentity)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Could not create identity: ", e)
|
||||
}
|
||||
}
|
||||
return this.keypair.data
|
||||
}
|
||||
|
||||
getKeyId(): Store<string> {
|
||||
return this.key_id
|
||||
}
|
||||
|
||||
private allReviewsById : UIEventSource<(Review & {kid: string, signature: string})[]>= undefined
|
||||
|
||||
|
||||
/**
|
||||
* Gets all reviews that are made for the current identity.
|
||||
*/
|
||||
public getAllReviews(): Store<(Review & {kid: string, signature: string})[]>{
|
||||
if(this.allReviewsById !== undefined){
|
||||
return this.allReviewsById
|
||||
}
|
||||
this.allReviewsById = new UIEventSource( [])
|
||||
this.key_id.map(pem => {
|
||||
if(pem === undefined){
|
||||
return []
|
||||
}
|
||||
MangroveReviews.getReviews({
|
||||
kid: pem
|
||||
}).then(allReviews => {
|
||||
this.allReviewsById.setData(allReviews.reviews.map(r => ({
|
||||
...r, ...r.payload
|
||||
})))
|
||||
})
|
||||
})
|
||||
return this.allReviewsById
|
||||
}
|
||||
|
||||
addReview(review: Review & {kid, signature}) {
|
||||
this.allReviewsById?.setData(this.allReviewsById?.data?.concat([review]))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -176,26 +225,30 @@ export default class FeatureReviews {
|
|||
* The given review is uploaded to mangrove.reviews and added to the list of known reviews
|
||||
*/
|
||||
public async createReview(review: Omit<Review, "sub">): Promise<void> {
|
||||
if(review.opinion.length > FeatureReviews .REVIEW_OPINION_MAX_LENGTH){
|
||||
if(review.opinion !== undefined && review.opinion.length > FeatureReviews .REVIEW_OPINION_MAX_LENGTH){
|
||||
throw "Opinion too long, should be at most "+FeatureReviews.REVIEW_OPINION_MAX_LENGTH+" characters long"
|
||||
}
|
||||
const r: Review = {
|
||||
sub: this.subjectUri.data,
|
||||
...review,
|
||||
}
|
||||
const keypair: CryptoKeyPair = this._identity.keypair.data
|
||||
const keypair: CryptoKeyPair = await this._identity.getKeypair()
|
||||
const jwt = await MangroveReviews.signReview(keypair, r)
|
||||
const kid = await MangroveReviews.publicToPem(keypair.publicKey)
|
||||
await MangroveReviews.submitReview(jwt)
|
||||
this._reviews.data.push({
|
||||
const reviewWithKid = {
|
||||
...r,
|
||||
kid,
|
||||
signature: jwt,
|
||||
madeByLoggedInUser: new ImmutableStore(true),
|
||||
})
|
||||
}
|
||||
this._reviews.data.push( reviewWithKid)
|
||||
this._reviews.ping()
|
||||
this._identity.addReview(reviewWithKid)
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Adds given reviews to the 'reviews'-UI-eventsource
|
||||
* @param reviews
|
||||
|
@ -235,7 +288,7 @@ export default class FeatureReviews {
|
|||
...review,
|
||||
kid: reviewData.kid,
|
||||
signature: reviewData.signature,
|
||||
madeByLoggedInUser: this._identity.key_id.map((user_key_id) => {
|
||||
madeByLoggedInUser: this._identity.getKeyId().map((user_key_id) => {
|
||||
return reviewData.kid === user_key_id
|
||||
}),
|
||||
})
|
||||
|
|
|
@ -171,7 +171,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
|||
})
|
||||
this.userRelatedState = new UserRelatedState(
|
||||
this.osmConnection,
|
||||
layout?.language,
|
||||
layout,
|
||||
this.featureSwitches,
|
||||
this.mapProperties
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
center()
|
||||
}
|
||||
|
||||
const titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"]
|
||||
let titleIconBlacklist = ["osmlink", "sharelink", "favourite_title_icon"]
|
||||
</script>
|
||||
|
||||
{#if favLayer !== undefined}
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
|
||||
<div class="flex flex-col" on:keypress={(e) => console.log("Got keypress", e)}>
|
||||
<Tr t={Translations.t.favouritePoi.intro.Subs({ length: $favourites?.length ?? 0 })} />
|
||||
<Tr t={Translations.t.favouritePoi.priintroPrivacyvacy} />
|
||||
<Tr t={Translations.t.favouritePoi.introPrivacy} />
|
||||
|
||||
{#each $favourites as feature (feature.properties.id)}
|
||||
<FavouriteSummary {feature} {state} />
|
||||
|
|
|
@ -35,9 +35,9 @@
|
|||
|
||||
let _state: "ask" | "saving" | "done" = "ask"
|
||||
|
||||
const connection = state.osmConnection
|
||||
let connection = state.osmConnection
|
||||
|
||||
const hasError: Store<undefined | "too_long"> = opinion.mapD(op => {
|
||||
let hasError: Store<undefined | "too_long"> = opinion.mapD(op => {
|
||||
const tooLong = op.length > FeatureReviews.REVIEW_OPINION_MAX_LENGTH
|
||||
if (tooLong) {
|
||||
return "too_long"
|
||||
|
@ -45,6 +45,8 @@
|
|||
return undefined
|
||||
})
|
||||
|
||||
let uploadFailed: string = undefined
|
||||
|
||||
async function save() {
|
||||
if (hasError.data) {
|
||||
return
|
||||
|
@ -63,13 +65,24 @@
|
|||
console.log("Testing - not actually saving review", review)
|
||||
await Utils.waitFor(1000)
|
||||
} else {
|
||||
await reviews.createReview(review)
|
||||
try {
|
||||
|
||||
await reviews.createReview(review)
|
||||
} catch (e) {
|
||||
console.error("Could not create review due to", e)
|
||||
uploadFailed = "" + e
|
||||
}
|
||||
}
|
||||
_state = "done"
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if _state === "done"}
|
||||
{#if uploadFailed}
|
||||
<div class="alert flex">
|
||||
<ExclamationTriangle class="w-6 h-6" />
|
||||
<Tr t={Translations.t.general.error}/>
|
||||
{uploadFailed}
|
||||
</div>
|
||||
{:else if _state === "done"}
|
||||
<Tr cls="thanks w-full" t={t.saved} />
|
||||
{:else if _state === "saving"}
|
||||
<Loading>
|
||||
|
@ -109,8 +122,9 @@
|
|||
/>
|
||||
{#if $hasError === "too_long"}
|
||||
<div class="alert flex items-center px-2">
|
||||
<ExclamationTriangle class="w-12 h-12"/>
|
||||
<Tr t={t.too_long.Subs({max: FeatureReviews.REVIEW_OPINION_MAX_LENGTH, amount: $opinion?.length ?? 0})}> </Tr>
|
||||
<ExclamationTriangle class="w-12 h-12" />
|
||||
<Tr
|
||||
t={t.too_long.Subs({max: FeatureReviews.REVIEW_OPINION_MAX_LENGTH, amount: $opinion?.length ?? 0})}></Tr>
|
||||
</div>
|
||||
{/if}
|
||||
</label>
|
||||
|
|
40
src/UI/Reviews/ReviewsOverview.svelte
Normal file
40
src/UI/Reviews/ReviewsOverview.svelte
Normal file
|
@ -0,0 +1,40 @@
|
|||
<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"
|
||||
import LoginButton from "../Base/LoginButton.svelte"
|
||||
import SingleReview from "./SingleReview.svelte"
|
||||
import Mangrove_logo from "../../assets/svg/Mangrove_logo.svelte"
|
||||
|
||||
/**
|
||||
* A panel showing all the reviews by the logged-in user
|
||||
*/
|
||||
export let state: SpecialVisualizationState
|
||||
let reviews = state.userRelatedState.mangroveIdentity.getAllReviews()
|
||||
const t = Translations.t.reviews
|
||||
</script>
|
||||
|
||||
<LoginToggle {state}>
|
||||
<div slot="not-logged-in">
|
||||
<LoginButton osmConnection={state.osmConnection}>
|
||||
<Tr t={Translations.t.favouritePoi.loginToSeeList} />
|
||||
</LoginButton>
|
||||
</div>
|
||||
|
||||
|
||||
{#if $reviews?.length > 0}
|
||||
<div class="flex flex-col" on:keypress={(e) => console.log("Got keypress", e)}>
|
||||
{#each $reviews as review (review.sub)}
|
||||
<SingleReview {review} showSub={true} {state} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Tr t={t.your_reviews_empty} />
|
||||
{/if}
|
||||
<a class="link-underline" href="https://github.com/pietervdvn/MapComplete/issues/1782" target="_blank" rel="noopener noreferrer"><Tr t={t.reviews_bug}/></a>
|
||||
<div class="flex justify-end">
|
||||
<Mangrove_logo class="h-12 w-12 shrink-0 p-1" />
|
||||
<Tr cls="text-sm subtle" t={t.attribution} />
|
||||
</div>
|
||||
</LoginToggle>
|
|
@ -1,22 +1,43 @@
|
|||
<script lang="ts">
|
||||
import { Review } from "mangrove-reviews-typescript"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
|
||||
import StarsBar from "./StarsBar.svelte"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Tr from "../Base/Tr.svelte"
|
||||
import { ariaLabel } from "../../Utils/ariaLabel"
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
export let review: Review & { kid: string; signature: string; madeByLoggedInUser: Store<boolean> }
|
||||
export let state: SpecialVisualizationState = undefined
|
||||
export let review: Review & { kid: string; signature: string; madeByLoggedInUser?: Store<boolean> }
|
||||
let name = review.metadata.nickname
|
||||
name ??= ((review.metadata.given_name ?? "") + " " + (review.metadata.family_name ?? "")).trim()
|
||||
let d = new Date()
|
||||
d.setTime(review.iat * 1000)
|
||||
let date = d.toDateString()
|
||||
let byLoggedInUser = review.madeByLoggedInUser
|
||||
let byLoggedInUser = review.madeByLoggedInUser ?? ImmutableStore.FALSE
|
||||
|
||||
export let showSub = false
|
||||
let subUrl = new URL(review.sub)
|
||||
let [lat, lon] = subUrl.pathname.split(",").map(l => Number(l))
|
||||
let sub = subUrl.searchParams.get("q")
|
||||
|
||||
function selectFeature(){
|
||||
console.log("Selecting and zooming to", {lon, lat})
|
||||
state?.mapProperties?.location?.setData({lon, lat})
|
||||
state?.mapProperties?.zoom?.setData(Math.max(16, state?.mapProperties?.zoom?.data))
|
||||
|
||||
state?.guistate?.closeAll()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class={"low-interaction rounded-lg p-1 px-2 " + ($byLoggedInUser ? "border-interactive" : "")}>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class={"low-interaction rounded-lg p-1 px-2 flex flex-col" + ($byLoggedInUser ? "border-interactive" : "")}>
|
||||
{#if showSub}
|
||||
<button class="link" on:click={() => selectFeature()}>
|
||||
<h3>{sub}</h3>
|
||||
</button>
|
||||
{/if}
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div
|
||||
tabindex="0"
|
||||
use:ariaLabel={Translations.t.reviews.rated.Subs({
|
||||
|
|
|
@ -66,6 +66,7 @@
|
|||
import FilterPanel from "./BigComponents/FilterPanel.svelte"
|
||||
import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte"
|
||||
import { BBox } from "../Logic/BBox"
|
||||
import ReviewsOverview from "./Reviews/ReviewsOverview.svelte"
|
||||
|
||||
export let state: ThemeViewState
|
||||
let layout = state.layout
|
||||
|
@ -588,6 +589,10 @@
|
|||
<Tr t={Translations.t.favouritePoi.title} />
|
||||
</h3>
|
||||
<Favourites {state} />
|
||||
<h3>
|
||||
<Tr t={Translations.t.reviews.your_reviews} />
|
||||
</h3>
|
||||
<ReviewsOverview {state}/>
|
||||
</div>
|
||||
</TabbedGroup>
|
||||
</FloatOver>
|
||||
|
|
Loading…
Reference in a new issue