diff --git a/Logic/ElementStorage.ts b/Logic/ElementStorage.ts index 1f942e3f4..49213b99f 100644 --- a/Logic/ElementStorage.ts +++ b/Logic/ElementStorage.ts @@ -22,7 +22,7 @@ export class ElementStorage { * * Note: it will cleverly merge the tags, if needed */ - addOrGetElement(feature: any): UIEventSource { + addOrGetElement(feature: Feature): UIEventSource { const elementId = feature.properties.id const newProperties = feature.properties diff --git a/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts b/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts index a30364025..849ea4c7d 100644 --- a/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/OsmFeatureSource.ts @@ -143,7 +143,6 @@ export default class OsmFeatureSource { try { const osmJson = await Utils.downloadJson(url) try { - console.log("Got tile", z, x, y, "from the osm api") this.rawDataHandlers.forEach((handler) => handler(osmJson, Tiles.tile_index(z, x, y)) ) diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index 7db59652f..f9d49b9a0 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -371,12 +371,12 @@ class ListenerTracker { * It'll fuse */ class MappedStore extends Store { - private _upstream: Store - private _upstreamCallbackHandler: ListenerTracker | undefined + private readonly _upstream: Store + private readonly _upstreamCallbackHandler: ListenerTracker | undefined private _upstreamPingCount: number = -1 private _unregisterFromUpstream: () => void - private _f: (t: TIn) => T + private readonly _f: (t: TIn) => T private readonly _extraStores: Store[] | undefined private _unregisterFromExtraStores: (() => void)[] | undefined diff --git a/Logic/Web/MangroveReviews.ts b/Logic/Web/MangroveReviews.ts index 448fa6700..763b7fa66 100644 --- a/Logic/Web/MangroveReviews.ts +++ b/Logic/Web/MangroveReviews.ts @@ -1,36 +1,37 @@ -import { UIEventSource } from "../UIEventSource" -import { Review } from "./Review" +import { ImmutableStore, Store, UIEventSource } from "../UIEventSource" +import { MangroveReviews, Review } from "mangrove-reviews-typescript" +import { Utils } from "../../Utils" +import { Feature, Geometry, Position } from "geojson" +import { GeoOperations } from "../GeoOperations" +import { OsmTags } from "../../Models/OsmFeature" +import { ElementStorage } from "../ElementStorage" export class MangroveIdentity { - public keypair: any = undefined - public readonly kid: UIEventSource = new UIEventSource(undefined) - private readonly _mangroveIdentity: UIEventSource + public readonly keypair: Store + public readonly key_id: Store constructor(mangroveIdentity: UIEventSource) { - const self = this - /* - this._mangroveIdentity = mangroveIdentity - mangroveIdentity.addCallbackAndRunD((str) => { - if (str === "") { + const key_id = new UIEventSource(undefined) + this.key_id = key_id + const keypairEventSource = new UIEventSource(undefined) + this.keypair = keypairEventSource + mangroveIdentity.addCallbackAndRunD(async (data) => { + if (data === "") { return } - mangrove.jwkToKeypair(JSON.parse(str)).then((keypair) => { - self.keypair = keypair - mangrove.publicToPem(keypair.publicKey).then((pem) => { - console.log("Identity loaded") - self.kid.setData(pem) - }) - }) + const keypair = await MangroveReviews.jwkToKeypair(JSON.parse(data)) + keypairEventSource.setData(keypair) + const pem = await MangroveReviews.publicToPem(keypair.publicKey) + key_id.setData(pem) }) + try { if (!Utils.runningFromConsole && (mangroveIdentity.data ?? "") === "") { - this.CreateIdentity() + MangroveIdentity.CreateIdentity(mangroveIdentity).then((_) => {}) } } catch (e) { console.error("Could not create identity: ", e) } - - // */ } /** @@ -38,170 +39,204 @@ export class MangroveIdentity { * Is written into the UIEventsource, which was passed into the constructor * @constructor */ - private CreateIdentity() { - if ("" !== (this._mangroveIdentity.data ?? "")) { - throw "Identity already defined - not creating a new one" + private static async CreateIdentity(identity: UIEventSource): Promise { + const keypair = await MangroveReviews.generateKeypair() + const jwk = await MangroveReviews.keypairToJwk(keypair) + if ((identity.data ?? "") !== "") { + // Identity has been loaded via osmPreferences by now - we don't overwrite + return } - const self = this - /*mangrove.generateKeypair().then((keypair) => { - self.keypair = keypair - mangrove.keypairToJwk(keypair).then((jwk) => { - self._mangroveIdentity.setData(JSON.stringify(jwk)) - }) - })//*/ + identity.setData(JSON.stringify(jwk)) } } -export default class MangroveReviews { - private static _reviewsCache = {} - private static didWarn = false - private readonly _lon: number +/** + * Tracks all reviews of a given feature, allows to create a new review + */ +export default class FeatureReviews { + private static readonly _featureReviewsCache: Record = {} + public readonly subjectUri: Store + private readonly _reviews: UIEventSource<(Review & { madeByLoggedInUser: Store })[]> = + new UIEventSource([]) + public readonly reviews: Store<(Review & { madeByLoggedInUser: Store })[]> = + this._reviews private readonly _lat: number - private readonly _name: string - private readonly _reviews: UIEventSource = new UIEventSource([]) - private _dryRun: boolean - private _mangroveIdentity: MangroveIdentity - private _lastUpdate: Date = undefined + private readonly _lon: number + private readonly _uncertainty: number + private readonly _name: Store + private readonly _identity: MangroveIdentity private constructor( - lon: number, - lat: number, - name: string, - identity: MangroveIdentity, - dryRun?: boolean - ) { - this._lon = lon - this._lat = lat - this._name = name - this._mangroveIdentity = identity - this._dryRun = dryRun - if (dryRun && !MangroveReviews.didWarn) { - MangroveReviews.didWarn = true - console.warn("Mangrove reviews will _not_ be saved as dryrun is specified") + feature: Feature, + state: { + allElements: ElementStorage + mangroveIdentity?: MangroveIdentity + }, + options?: { + nameKey?: "name" | string + fallbackName?: string + uncertaintyRadius?: number } + ) { + const centerLonLat = GeoOperations.centerpointCoordinates(feature) + ;[this._lon, this._lat] = centerLonLat + this._identity = + state?.mangroveIdentity ?? new MangroveIdentity(new UIEventSource(undefined)) + const nameKey = options?.nameKey ?? "name" + + if (feature.geometry.type === "Point") { + this._uncertainty = options?.uncertaintyRadius ?? 10 + } else { + let coords: Position[][] + if (feature.geometry.type === "LineString") { + coords = [feature.geometry.coordinates] + } else if ( + feature.geometry.type === "MultiLineString" || + feature.geometry.type === "Polygon" + ) { + coords = feature.geometry.coordinates + } + let maxDistance = 0 + for (const coord of coords) { + maxDistance = Math.max( + maxDistance, + GeoOperations.distanceBetween(centerLonLat, coord) + ) + } + + this._uncertainty = options?.uncertaintyRadius ?? maxDistance + } + this._name = state.allElements + .getEventSourceById(feature.properties.id) + .map((tags) => tags[nameKey] ?? options?.fallbackName) + + this.subjectUri = this.ConstructSubjectUri() + + const self = this + this.subjectUri.addCallbackAndRunD(async (sub) => { + const reviews = await MangroveReviews.getReviews({ sub }) + self.addReviews(reviews.reviews) + }) + /* We also construct all subject queries _without_ encoding the name to work around a previous bug + * See https://github.com/giggls/opencampsitemap/issues/30 + */ + this.ConstructSubjectUri(true).addCallbackAndRunD(async (sub) => { + try { + const reviews = await MangroveReviews.getReviews({ sub }) + self.addReviews(reviews.reviews) + } catch (e) { + console.log("Could not fetch reviews for partially incorrect query ", sub) + } + }) } - public static Get( - lon: number, - lat: number, - name: string, - identity: MangroveIdentity, - dryRun?: boolean + /** + * Construct a featureReviewsFor or fetches it from the cache + */ + public static construct( + feature: Feature, + state: { + allElements: ElementStorage + mangroveIdentity?: MangroveIdentity + }, + options?: { + nameKey?: "name" | string + fallbackName?: string + uncertaintyRadius?: number + } ) { - const newReviews = new MangroveReviews(lon, lat, name, identity, dryRun) - - const uri = newReviews.GetSubjectUri() - const cached = MangroveReviews._reviewsCache[uri] + const key = feature.properties.id + const cached = FeatureReviews._featureReviewsCache[key] if (cached !== undefined) { return cached } - MangroveReviews._reviewsCache[uri] = newReviews + const featureReviews = new FeatureReviews(feature, state, options) + FeatureReviews._featureReviewsCache[key] = featureReviews + return featureReviews + } - return newReviews + /** + * The given review is uploaded to mangrove.reviews and added to the list of known reviews + */ + public async createReview(review: Omit): Promise { + const r: Review = { + sub: this.subjectUri.data, + ...review, + } + const keypair: CryptoKeyPair = this._identity.keypair.data + console.log(r) + const jwt = await MangroveReviews.signReview(keypair, r) + console.log("Signed:", jwt) + await MangroveReviews.submitReview(jwt) + this._reviews.data.push({ ...r, madeByLoggedInUser: new ImmutableStore(true) }) + this._reviews.ping() + } + + /** + * Adds given reviews to the 'reviews'-UI-eventsource + * @param reviews + * @private + */ + private addReviews(reviews: { payload: Review; kid: string }[]) { + const self = this + const alreadyKnown = new Set(self._reviews.data.map((r) => r.rating + " " + r.opinion)) + + let hasNew = false + for (const reviewData of reviews) { + const review = reviewData.payload + + try { + const url = new URL(review.sub) + console.log("URL is", url) + if (url.protocol === "geo:") { + const coordinate = <[number, number]>( + url.pathname.split(",").map((n) => Number(n)) + ) + const distance = GeoOperations.distanceBetween( + [this._lat, this._lon], + coordinate + ) + if (distance > this._uncertainty) { + continue + } + } + } catch (e) { + console.warn(e) + } + + const key = review.rating + " " + review.opinion + if (alreadyKnown.has(key)) { + continue + } + self._reviews.data.push({ + ...review, + madeByLoggedInUser: this._identity.key_id.map((user_key_id) => { + return reviewData.kid === user_key_id + }), + }) + hasNew = true + } + if (hasNew) { + self._reviews.ping() + } } /** * Gets an URI which represents the item in a mangrove-compatible way + * + * See https://mangrove.reviews/standard#mangrove-core-uri-schemes * @constructor */ - public GetSubjectUri() { - let uri = `geo:${this._lat},${this._lon}?u=50` - if (this._name !== undefined && this._name !== null) { - uri += "&q=" + this._name - } - return uri - } - - /** - * Gives a UIEVentsource with all reviews. - * Note: rating is between 1 and 100 - */ - public GetReviews(): UIEventSource { - /* - if ( - this._lastUpdate !== undefined && - this._reviews.data !== undefined && - new Date().getTime() - this._lastUpdate.getTime() < 15000 - ) { - // Last update was pretty recent - return this._reviews - } - this._lastUpdate = new Date() - + private ConstructSubjectUri(dontEncodeName: boolean = false): Store { + // https://www.rfc-editor.org/rfc/rfc5870#section-3.4.2 + // `u` stands for `uncertainty`, https://www.rfc-editor.org/rfc/rfc5870#section-3.4.3 const self = this - mangrove - .getReviews({ sub: this.GetSubjectUri() }) - .then((data) => { - const reviews = [] - const reviewsByUser = [] - for (const review of data.reviews) { - const r = review.payload - - console.log( - "PublicKey is ", - self._mangroveIdentity.kid.data, - "reviews.kid is", - review.kid - ) - const byUser = self._mangroveIdentity.kid.map( - (data) => data === review.signature - ) - const rev: Review = { - made_by_user: byUser, - date: new Date(r.iat * 1000), - comment: r.opinion, - author: r.metadata.nickname, - affiliated: r.metadata.is_affiliated, - rating: r.rating, // percentage points - } - - ;(rev.made_by_user ? reviewsByUser : reviews).push(rev) - } - self._reviews.setData(reviewsByUser.concat(reviews)) - }) - .catch((e) => { - console.error("Could not download review for ", e) - }) - //*/ - return this._reviews - } - - AddReview(r: Review, callback?: () => void) { - callback = - callback ?? - (() => { - return undefined - }) - - const payload = { - sub: this.GetSubjectUri(), - rating: r.rating, - opinion: r.comment, - metadata: { - nickname: r.author, - }, - } - if (r.affiliated) { - // @ts-ignore - payload.metadata.is_affiliated = true - } - if (this._dryRun) { - console.warn("DRYRUNNING mangrove reviews: ", payload) - if (callback) { - if (callback) { - callback() - } - this._reviews.data.push(r) - this._reviews.ping() + return this._name.map(function (name) { + let uri = `geo:${self._lat},${self._lon}?u=${self._uncertainty}` + if (name) { + uri += "&q=" + (dontEncodeName ? name : encodeURIComponent(name)) } - } else { - /*mangrove.signAndSubmitReview(this._mangroveIdentity.keypair, payload).then(() => { - if (callback) { - callback() - } - this._reviews.data.push(r) - this._reviews.ping() - })//*/ - } + return uri + }) } } diff --git a/Logic/Web/Review.ts b/Logic/Web/Review.ts deleted file mode 100644 index 9834192cd..000000000 --- a/Logic/Web/Review.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Store } from "../UIEventSource" - -export interface Review { - comment?: string - author: string - date: Date - rating: number - affiliated: boolean - /** - * True if the current logged in user is the creator of this comment - */ - made_by_user: Store -} diff --git a/UI/Base/SubtleButton.ts b/UI/Base/SubtleButton.ts index 90242fbc9..0975ca7ce 100644 --- a/UI/Base/SubtleButton.ts +++ b/UI/Base/SubtleButton.ts @@ -12,7 +12,12 @@ import Loading from "./Loading" export class SubtleButton extends UIElement { private readonly imageUrl: string | BaseUIElement private readonly message: string | BaseUIElement - private readonly options: { url?: string | Store; newTab?: boolean; imgSize?: string } + private readonly options: { + url?: string | Store + newTab?: boolean + imgSize?: string + extraClasses?: string + } constructor( imageUrl: string | BaseUIElement, @@ -21,6 +26,7 @@ export class SubtleButton extends UIElement { url?: string | Store newTab?: boolean imgSize?: "h-11 w-11" | string + extraClasses?: string } = undefined ) { super() @@ -31,7 +37,8 @@ export class SubtleButton extends UIElement { protected InnerRender(): string | BaseUIElement { const classes = - "block flex p-3 my-2 bg-subtle rounded-lg hover:shadow-xl hover:bg-unsubtle transition-colors transition-shadow link-no-underline" + "block flex p-3 my-2 bg-subtle rounded-lg hover:shadow-xl hover:bg-unsubtle transition-colors transition-shadow link-no-underline " + + (this?.options?.extraClasses ?? "") const message = Translations.W(this.message)?.SetClass( "block text-ellipsis no-images flex-shrink" ) diff --git a/UI/Reviews/ReviewElement.ts b/UI/Reviews/ReviewElement.ts index 85f4093f2..efdf42fe8 100644 --- a/UI/Reviews/ReviewElement.ts +++ b/UI/Reviews/ReviewElement.ts @@ -1,5 +1,3 @@ -import { UIEventSource } from "../../Logic/UIEventSource" -import { Review } from "../../Logic/Web/Review" import Combine from "../Base/Combine" import Translations from "../i18n/Translations" import SingleReview from "./SingleReview" @@ -7,44 +5,52 @@ import BaseUIElement from "../BaseUIElement" import Img from "../Base/Img" import { VariableUiElement } from "../Base/VariableUIElement" import Link from "../Base/Link" +import FeatureReviews from "../../Logic/Web/MangroveReviews" /** * Shows the reviews and scoring base on mangrove.reviews * The middle element is some other component shown in the middle, e.g. the review input element */ export default class ReviewElement extends VariableUiElement { - constructor(subject: string, reviews: UIEventSource, middleElement: BaseUIElement) { + constructor(reviews: FeatureReviews, middleElement: BaseUIElement) { super( - reviews.map((revs) => { - const elements = [] - revs.sort((a, b) => b.date.getTime() - a.date.getTime()) // Sort with most recent first - const avg = - revs.map((review) => review.rating).reduce((a, b) => a + b, 0) / revs.length - elements.push( - new Combine([ - SingleReview.GenStars(avg), - new Link( - revs.length === 1 - ? Translations.t.reviews.title_singular.Clone() - : Translations.t.reviews.title.Subs({ count: "" + revs.length }), - `https://mangrove.reviews/search?sub=${encodeURIComponent(subject)}`, - true - ), - ]).SetClass("font-2xl flex justify-between items-center pl-2 pr-2") - ) + reviews.reviews.map( + (revs) => { + const elements = [] + revs.sort((a, b) => b.iat - a.iat) // Sort with most recent first + const avg = + revs.map((review) => review.rating).reduce((a, b) => a + b, 0) / revs.length + elements.push( + new Combine([ + SingleReview.GenStars(avg), + new Link( + revs.length === 1 + ? Translations.t.reviews.title_singular.Clone() + : Translations.t.reviews.title.Subs({ + count: "" + revs.length, + }), + `https://mangrove.reviews/search?sub=${encodeURIComponent( + reviews.subjectUri.data + )}`, + true + ), + ]).SetClass("font-2xl flex justify-between items-center pl-2 pr-2") + ) - elements.push(middleElement) + elements.push(middleElement) - elements.push(...revs.map((review) => new SingleReview(review))) - elements.push( - new Combine([ - Translations.t.reviews.attribution.Clone(), - new Img("./assets/mangrove_logo.png"), - ]).SetClass("review-attribution") - ) + elements.push(...revs.map((review) => new SingleReview(review))) + elements.push( + new Combine([ + Translations.t.reviews.attribution.Clone(), + new Img("./assets/mangrove_logo.png"), + ]).SetClass("review-attribution") + ) - return new Combine(elements).SetClass("block") - }) + return new Combine(elements).SetClass("block") + }, + [reviews.subjectUri] + ) ) } } diff --git a/UI/Reviews/ReviewForm.ts b/UI/Reviews/ReviewForm.ts index 101de512f..dd9928f91 100644 --- a/UI/Reviews/ReviewForm.ts +++ b/UI/Reviews/ReviewForm.ts @@ -1,118 +1,89 @@ -import { InputElement } from "../Input/InputElement" -import { Review } from "../../Logic/Web/Review" +import { Review } from "mangrove-reviews-typescript" import { Store, UIEventSource } from "../../Logic/UIEventSource" import { TextField } from "../Input/TextField" import Translations from "../i18n/Translations" import Combine from "../Base/Combine" import Svg from "../../Svg" import { VariableUiElement } from "../Base/VariableUIElement" -import { SaveButton } from "../Popup/SaveButton" -import CheckBoxes from "../Input/Checkboxes" +import { CheckBox } from "../Input/Checkboxes" import UserDetails, { OsmConnection } from "../../Logic/Osm/OsmConnection" -import BaseUIElement from "../BaseUIElement" import Toggle from "../Input/Toggle" import { LoginToggle } from "../Popup/LoginButton" +import { SubtleButton } from "../Base/SubtleButton" -export default class ReviewForm extends InputElement { - IsSelected: UIEventSource = new UIEventSource(false) - private readonly _value: UIEventSource - private readonly _comment: BaseUIElement - private readonly _stars: BaseUIElement - private _saveButton: BaseUIElement - private readonly _isAffiliated: BaseUIElement - private readonly _postingAs: BaseUIElement - private readonly _state: { - readonly osmConnection: OsmConnection - readonly featureSwitchUserbadge: Store - } - +export default class ReviewForm extends LoginToggle { constructor( - onSave: (r: Review, doneSaving: () => void) => void, + onSave: (r: Omit) => Promise, state: { readonly osmConnection: OsmConnection readonly featureSwitchUserbadge: Store } ) { - super() - this._state = state - const osmConnection = state.osmConnection - this._value = new UIEventSource({ - made_by_user: new UIEventSource(true), + /* made_by_user: new UIEventSource(true), rating: undefined, comment: undefined, author: osmConnection.userDetails.data.name, affiliated: false, - date: new Date(), - }) - const comment = new TextField({ + date: new Date(),*/ + const commentForm = new TextField({ placeholder: Translations.t.reviews.write_a_comment.Clone(), htmlType: "area", textAreaRows: 5, }) - comment.GetValue().addCallback((comment) => { - self._value.data.comment = comment - self._value.ping() - }) - const self = this - this._postingAs = new Combine([ + const rating = new UIEventSource(undefined) + const isAffiliated = new CheckBox(Translations.t.reviews.i_am_affiliated) + const reviewMade = new UIEventSource(false) + + const postingAs = new Combine([ Translations.t.reviews.posting_as.Clone(), new VariableUiElement( - osmConnection.userDetails.map((ud: UserDetails) => ud.name) + state.osmConnection.userDetails.map((ud: UserDetails) => ud.name) ).SetClass("review-author"), ]).SetStyle("display:flex;flex-direction: column;align-items: flex-end;margin-left: auto;") - const reviewIsSaved = new UIEventSource(false) - const reviewIsSaving = new UIEventSource(false) - this._saveButton = new Toggle( - Translations.t.reviews.saved.Clone().SetClass("thanks"), - new Toggle( - Translations.t.reviews.saving_review.Clone(), - new SaveButton( - this._value.map((r) => self.IsValid(r)), - osmConnection - ).onClick(() => { - reviewIsSaving.setData(true) - onSave(this._value.data, () => { - reviewIsSaved.setData(true) - }) - }), - reviewIsSaving - ), - reviewIsSaved - ).SetClass("break-normal") + const saveButton = new Toggle( + Translations.t.reviews.no_rating.SetClass("block alert"), + new SubtleButton(Svg.confirm_svg(), Translations.t.reviews.save, { + extraClasses: "border-attention-catch", + }) + .OnClickWithLoading( + Translations.t.reviews.saving_review.SetClass("alert"), + async () => { + const review: Omit = { + rating: rating.data, + opinion: commentForm.GetValue().data, + metadata: { nickname: state.osmConnection.userDetails.data.name }, + } + await onSave(review) + } + ) + .SetClass("break-normal"), + rating.map((r) => r === undefined, [commentForm.GetValue()]) + ) - this._isAffiliated = new CheckBoxes([Translations.t.reviews.i_am_affiliated.Clone()]) - - this._comment = comment const stars = [] for (let i = 1; i <= 5; i++) { stars.push( new VariableUiElement( - this._value.map((review) => { - if (review.rating === undefined) { + rating.map((score) => { + if (score === undefined) { return Svg.star_outline.replace(/#000000/g, "#ccc") } - return review.rating < i * 20 ? Svg.star_outline : Svg.star + return score < i * 20 ? Svg.star_outline : Svg.star }) ).onClick(() => { - self._value.data.rating = i * 20 - self._value.ping() + rating.setData(i * 20) }) ) } - this._stars = new Combine(stars).SetClass("review-form-rating") - } - GetValue(): UIEventSource { - return this._value - } - - InnerConstructElement(): HTMLElement { const form = new Combine([ - new Combine([this._stars, this._postingAs]).SetClass("flex"), - this._comment, - new Combine([this._isAffiliated, this._saveButton]).SetClass("review-form-bottom"), + new Combine([new Combine(stars).SetClass("review-form-rating"), postingAs]).SetClass( + "flex" + ), + commentForm, + new Combine([isAffiliated, saveButton]), Translations.t.reviews.tos.Clone().SetClass("subtle"), ]) .SetClass("flex flex-col p-4") @@ -123,22 +94,10 @@ export default class ReviewForm extends InputElement { " border: 2px solid var(--subtle-detail-color-contrast)" ) - return new LoginToggle( - form, - Translations.t.reviews.plz_login.Clone(), - this._state - ).ConstructElement() - } - - IsValid(r: Review): boolean { - if (r === undefined) { - return false - } - return ( - (r.comment?.length ?? 0) <= 1000 && - (r.author?.length ?? 0) <= 20 && - r.rating >= 0 && - r.rating <= 100 + super( + new Toggle(Translations.t.reviews.saved.Clone().SetClass("thanks"), form, reviewMade), + Translations.t.reviews.plz_login, + state ) } } diff --git a/UI/Reviews/SingleReview.ts b/UI/Reviews/SingleReview.ts index a3c996ca8..5e1e050d4 100644 --- a/UI/Reviews/SingleReview.ts +++ b/UI/Reviews/SingleReview.ts @@ -1,33 +1,47 @@ -import { Review } from "../../Logic/Web/Review" import Combine from "../Base/Combine" import { FixedUiElement } from "../Base/FixedUiElement" import Translations from "../i18n/Translations" import { Utils } from "../../Utils" import BaseUIElement from "../BaseUIElement" import Img from "../Base/Img" +import { Review } from "mangrove-reviews-typescript" +import { Store } from "../../Logic/UIEventSource" +import WikidataPreviewBox from "../Wikipedia/WikidataPreviewBox" export default class SingleReview extends Combine { - constructor(review: Review) { - const d = review.date + constructor(review: Review & { madeByLoggedInUser: Store }) { + const d = review + const date = new Date(review.iat * 1000) + const reviewAuthor = + review.metadata.nickname ?? + (review.metadata.given_name ?? "") + (review.metadata.family_name ?? "") super([ new Combine([SingleReview.GenStars(review.rating)]), - new FixedUiElement(review.comment), + new FixedUiElement(review.opinion), new Combine([ new Combine([ - new FixedUiElement(review.author).SetClass("font-bold"), - review.affiliated ? Translations.t.reviews.affiliated_reviewer_warning : "", + new FixedUiElement(reviewAuthor).SetClass("font-bold"), + review.metadata.is_affiliated + ? Translations.t.reviews.affiliated_reviewer_warning + : "", ]).SetStyle("margin-right: 0.5em"), new FixedUiElement( - `${d.getFullYear()}-${Utils.TwoDigits(d.getMonth() + 1)}-${Utils.TwoDigits( - d.getDate() - )} ${Utils.TwoDigits(d.getHours())}:${Utils.TwoDigits(d.getMinutes())}` + `${date.getFullYear()}-${Utils.TwoDigits( + date.getMonth() + 1 + )}-${Utils.TwoDigits(date.getDate())} ${Utils.TwoDigits( + date.getHours() + )}:${Utils.TwoDigits(date.getMinutes())}` ).SetClass("subtle-lighter"), ]).SetClass("flex mb-4 justify-end"), ]) this.SetClass("block p-2 m-4 rounded-xl subtle-background review-element") - if (review.made_by_user.data) { - this.SetClass("border-attention-catch") - } + review.madeByLoggedInUser.addCallbackAndRun((madeByUser) => { + if (madeByUser) { + this.SetClass("border-attention-catch") + } else { + this.RemoveClass("border-attention-catch") + } + }) } public static GenStars(rating: number): BaseUIElement { diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 5a187d268..67349cdf1 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -52,6 +52,7 @@ import { GeoOperations } from "../Logic/GeoOperations" import StatisticsPanel from "./BigComponents/StatisticsPanel" import AutoApplyButton from "./Popup/AutoApplyButton" import { LanguageElement } from "./Popup/LanguageElement" +import FeatureReviews from "../Logic/Web/MangroveReviews" export default class SpecialVisualizations { public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList() @@ -204,24 +205,16 @@ export default class SpecialVisualizations { }, ], constr: (state, tags, args) => { - const tgs = tags.data - const key = args[0] ?? "name" - let subject = tgs[key] ?? args[1] - if (subject === undefined || subject === "") { - return Translations.t.reviews.name_required - } - const mangrove = MangroveReviews.Get( - Number(tgs._lon), - Number(tgs._lat), - encodeURIComponent(subject), - state.mangroveIdentity, - state.featureSwitchIsTesting.data - ) - const form = new ReviewForm( - (r, whenDone) => mangrove.AddReview(r, whenDone), - state - ) - return new ReviewElement(mangrove.GetSubjectUri(), mangrove.GetReviews(), form) + const nameKey = args[0] ?? "name" + let fallbackName = args[1] + const feature = state.allElements.ContainingFeatures.get(tags.data.id) + const mangrove = FeatureReviews.construct(feature, state, { + nameKey: nameKey, + fallbackName, + }) + + const form = new ReviewForm((r) => mangrove.createReview(r), state) + return new ReviewElement(mangrove, form) }, }, { diff --git a/assets/themes/mapcomplete-changes/mapcomplete-changes.json b/assets/themes/mapcomplete-changes/mapcomplete-changes.json index 137bb9260..9cbbdc87c 100644 --- a/assets/themes/mapcomplete-changes/mapcomplete-changes.json +++ b/assets/themes/mapcomplete-changes/mapcomplete-changes.json @@ -1,13 +1,21 @@ { "id": "mapcomplete-changes", "title": { - "en": "Changes made with MapComplete" + "en": "Changes made with MapComplete", + "de": "Mit MapComplete vorgenommene Änderungen", + "nl": "Wijzigingen gemaakt met MapComplete" }, "shortDescription": { - "en": "Shows changes made by MapComplete" + "en": "Shows changes made by MapComplete", + "de": "Zeigt Änderungen, die von MapComplete vorgenommen wurden", + "nl": "Toont wijzigingen gemaakt met MapComplete" }, "description": { - "en": "This maps shows all the changes made with MapComplete" + "en": "This map shows all the changes made with MapComplete", + "ca": "Aquest mapa mostra tots els canvis fets amb MapComplete", + "de": "Diese Karte zeigt alle mit MapComplete vorgenommenen Änderungen", + "fr": "Cette carte montre tous les changements faits avec MapComplete", + "nl": "Deze kaart toont alle wijzigingen gemaakt met MapComplete" }, "icon": "./assets/svg/logo.svg", "hideFromOverview": true, @@ -20,7 +28,10 @@ { "id": "mapcomplete-changes", "name": { - "en": "Changeset centers" + "en": "Changeset centers", + "de": "Zentrum der Änderungssätze", + "fr": "Centres de modifications de paramètres", + "nl": "Middelpunt van de wijzigingenset" }, "minzoom": 0, "source": { @@ -31,41 +42,58 @@ }, "title": { "render": { - "en": "Changeset for {theme}" + "en": "Changeset for {theme}", + "de": "Änderungssatz für {theme}", + "nl": "Changeset voor {theme}" } }, "description": { - "en": "Shows all MapComplete changes" + "en": "Shows all MapComplete changes", + "de": "Zeigt alle MapComplete-Änderungen", + "fr": "Montre tous les changements de MapComplete", + "nl": "Toon alle MapComplete wijzigingen" }, "tagRenderings": [ { "id": "show_changeset_id", "render": { - "en": "Changeset {id}" + "en": "Changeset {id}", + "de": "Änderungssatz {id}", + "nl": "Wijzigingenset {id}" } }, { "id": "contributor", "question": { - "en": "What contributor did make this change?" + "en": "What contributor did make this change?", + "de": "Welcher Mitwirkende hat diese Änderung vorgenommen?", + "fr": "Quel contributeur a fait ce changement ?", + "nl": "Welke bijdrager maakte deze wijziging?" }, "freeform": { "key": "user" }, "render": { - "en": "Change made by {user}" + "en": "Change made by {user}", + "de": "Änderung vorgenommen von {user}", + "fr": "Modification faite par {user}", + "nl": "Wijziging gemaakt door {user}" } }, { "id": "theme-id", "question": { - "en": "What theme was used to make this change?" + "en": "What theme was used to make this change?", + "de": "Welches Thema wurde für diese Änderung verwendet?", + "nl": "Welk thema is gebruikt voor deze wijziging?" }, "freeform": { "key": "theme" }, "render": { - "en": "Change with theme {theme}" + "en": "Change with theme {theme}", + "de": "Geändert mit Thema {theme}", + "nl": "Wijziging met thema {theme}" } }, { @@ -74,19 +102,32 @@ "key": "locale" }, "question": { - "en": "What locale (language) was this change made in?" + "en": "What locale (language) was this change made in?", + "de": "In welchem Gebietsschema (Sprache) wurde diese Änderung vorgenommen?", + "fr": "En quelle langue est-ce que ce changement a été fait ?", + "nl": "In welke taal (en cultuur) werd deze wijziging gemaakt?" }, "render": { - "en": "User locale is {locale}" + "en": "User locale is {locale}", + "de": "Benutzergebietsschema ist {locale}", + "fr": "La langue de l'utilisateur est {locale}", + "nl": "De locale van de bijdrager is {locale}" } }, { "id": "host", "render": { - "en": "Change with with {host}" + "en": "Change with {host}", + "ca": "Canvi amb {host}", + "de": "Geändert über {host}", + "fr": "Changement avec {host}", + "nl": "Wijziging met {host}" }, "question": { - "en": "What host (website) was this change made with?" + "en": "What host (website) was this change made with?", + "de": "Über welchen Host (Webseite) wurde diese Änderung vorgenommen?", + "fr": "Depuis quel serveur (site web) ce changement a-t-il été fait ?", + "nl": "Op welk webadres werd deze wijziging gemaakt?" }, "freeform": { "key": "host" @@ -427,7 +468,10 @@ } ], "question": { - "en": "Themename contains {search}" + "en": "Themename contains {search}", + "de": "Themename enthält {search}", + "fr": "Nom de thème contenant {search}", + "nl": "Themanaam bevat {search}" } } ] @@ -443,7 +487,10 @@ } ], "question": { - "en": "Made by contributor {search}" + "en": "Made by contributor {search}", + "de": "Erstellt vom Mitwirkenden {search}", + "fr": "Fait par le contributeur {search}", + "nl": "Gemaakt door {search}" } } ] @@ -459,7 +506,10 @@ } ], "question": { - "en": "Not made by contributor {search}" + "en": "Not made by contributor {search}", + "de": "Nicht von Mitwirkendem {search}", + "fr": "Non réalisé par le contributeur{search}", + "nl": "Niet gemaakt door {search}" } } ] @@ -476,7 +526,10 @@ } ], "question": { - "en": "Made before {search}" + "en": "Made before {search}", + "de": "Erstellt vor {search}", + "fr": "Fait avant {search}", + "nl": "Gemaakt voor {search}" } } ] @@ -493,7 +546,10 @@ } ], "question": { - "en": "Made after {search}" + "en": "Made after {search}", + "de": "Erstellt nach {search}", + "fr": "Fait après {search}", + "nl": "Gemaakt na {search}" } } ] @@ -509,7 +565,10 @@ } ], "question": { - "en": "User language (iso-code) {search}" + "en": "User language (iso-code) {search}", + "de": "Benutzersprache (ISO-Code) {search}", + "fr": "Langage utilisateur (code-iso) {search}", + "nl": "Gebruikerstaal (iso-code) {search}" } } ] @@ -525,7 +584,10 @@ } ], "question": { - "en": "Made with host {search}" + "en": "Made with host {search}", + "de": "Erstellt mit host {search}", + "fr": "Fait par le serveur {search}", + "nl": "Gemaakt met host {search}" } } ] @@ -551,7 +613,9 @@ { "id": "link_to_more", "render": { - "en": "More statistics can be found here" + "en": "More statistics can be found here", + "de": "Weitere Statistiken hier", + "nl": "Meer statistieken zijn hier te vinden" } }, { diff --git a/langs/en.json b/langs/en.json index 8dbf89fed..c55c88e65 100644 --- a/langs/en.json +++ b/langs/en.json @@ -905,10 +905,11 @@ "attribution": "Reviews are powered by Mangrove Reviews and are available under CC-BY 4.0.", "i_am_affiliated": "I am affiliated with this object
Check if you are an owner, creator, employee, …", "name_required": "A name is required in order to display and create reviews", - "no_rating": "No rating given", + "no_rating": "Give a rating before submitting…", "no_reviews_yet": "There are no reviews yet. Be the first to write one and help open data and the business!", "plz_login": "Log in to leave a review", "posting_as": "Posting as", + "save": "Save", "saved": "Review saved. Thanks for sharing!", "saving_review": "Saving…", "title": "{count} reviews", diff --git a/package-lock.json b/package-lock.json index f57af406b..5b97b19d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "leaflet-simple-map-screenshoter": "^0.4.5", "libphonenumber-js": "^1.10.8", "lz-string": "^1.4.4", + "mangrove-reviews-typescript": "^0.0.6", "opening_hours": "^3.6.0", "osm-auth": "^1.0.2", "osmtogeojson": "^3.0.0-beta.5", @@ -4055,6 +4056,17 @@ "safer-buffer": "~2.1.0" } }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/assert": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/assert/-/assert-2.0.0.tgz", @@ -4124,6 +4136,29 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" }, + "node_modules/axios": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.3.tgz", + "integrity": "sha512-pdDkMYJeuXLZ6Xj/Q5J3Phpe+jbGdsSzlQaFVkMQzRUL05+6+tetX8TV3p4HrU4kzuO9bt+io/yGQxuyxA/xcw==", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/babel-plugin-polyfill-corejs2": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.1.10.tgz", @@ -4229,6 +4264,11 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, "node_modules/bops": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/bops/-/bops-0.0.6.tgz", @@ -4266,6 +4306,11 @@ "node": ">=8" } }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, "node_modules/browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -5188,6 +5233,20 @@ "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", "dev": true }, + "node_modules/elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/email-validator": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/email-validator/-/email-validator-2.0.4.tgz", @@ -5472,6 +5531,25 @@ "flat": "cli.js" } }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -5882,6 +5960,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -5890,6 +5977,16 @@ "he": "bin/he" } }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/html2canvas": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", @@ -6357,6 +6454,14 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, + "node_modules/jose": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.11.2.tgz", + "integrity": "sha512-njj0VL2TsIxCtgzhO+9RRobBvws4oYyCM8TpvoUQwl/MbIM3NFJRR9+e6x0sS5xXaP1t6OCBkaBME98OV9zU5A==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6561,6 +6666,16 @@ "node": ">= 4" } }, + "node_modules/jwk-to-pem": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz", + "integrity": "sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==", + "dependencies": { + "asn1.js": "^5.3.0", + "elliptic": "^6.5.4", + "safe-buffer": "^5.0.1" + } + }, "node_modules/jxon": { "version": "2.0.0-beta.5", "resolved": "https://registry.npmjs.org/jxon/-/jxon-2.0.0-beta.5.tgz", @@ -6764,6 +6879,17 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "devOptional": true }, + "node_modules/mangrove-reviews-typescript": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/mangrove-reviews-typescript/-/mangrove-reviews-typescript-0.0.6.tgz", + "integrity": "sha512-31wF20PdaKUhxP5lek7YouF50QbNk4U571I86e0lG5U/khP96wbVToZB2P4Anb0OPoQ2alHfpqJPuc491ptw2Q==", + "dependencies": { + "axios": "^1.2.3", + "jose": "^4.11.2", + "jwk-to-pem": "^2.0.5", + "typescript": "^4.9.4" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -6820,6 +6946,16 @@ "dom-walk": "^0.1.0" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -7575,6 +7711,11 @@ "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -13439,6 +13580,17 @@ "safer-buffer": "~2.1.0" } }, + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "assert": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/assert/-/assert-2.0.0.tgz", @@ -13487,6 +13639,28 @@ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" }, + "axios": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.2.3.tgz", + "integrity": "sha512-pdDkMYJeuXLZ6Xj/Q5J3Phpe+jbGdsSzlQaFVkMQzRUL05+6+tetX8TV3p4HrU4kzuO9bt+io/yGQxuyxA/xcw==", + "requires": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "babel-plugin-polyfill-corejs2": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.1.10.tgz", @@ -13563,6 +13737,11 @@ "readable-stream": "^3.4.0" } }, + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, "bops": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/bops/-/bops-0.0.6.tgz", @@ -13596,6 +13775,11 @@ "fill-range": "^7.0.1" } }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, "browser-stdout": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", @@ -14289,6 +14473,20 @@ "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", "dev": true }, + "elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "email-validator": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/email-validator/-/email-validator-2.0.4.tgz", @@ -14502,6 +14700,11 @@ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==" }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, "for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -14816,11 +15019,30 @@ "has-symbols": "^1.0.2" } }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "html2canvas": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", @@ -15154,6 +15376,11 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, + "jose": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.11.2.tgz", + "integrity": "sha512-njj0VL2TsIxCtgzhO+9RRobBvws4oYyCM8TpvoUQwl/MbIM3NFJRR9+e6x0sS5xXaP1t6OCBkaBME98OV9zU5A==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -15312,6 +15539,16 @@ "resolved": "https://registry.npmjs.org/jsts/-/jsts-1.1.2.tgz", "integrity": "sha512-4qWAI9gR72HcGWCl7bej9/2dCM6Nv6dh5Zn1G+wzJYW9wsFL/2bPA3kdR8IAPObmF4gb56l5EGlXxErmB+9GOw==" }, + "jwk-to-pem": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/jwk-to-pem/-/jwk-to-pem-2.0.5.tgz", + "integrity": "sha512-L90jwellhO8jRKYwbssU9ifaMVqajzj3fpRjDKcsDzrslU9syRbFqfkXtT4B89HYAap+xsxNcxgBSB09ig+a7A==", + "requires": { + "asn1.js": "^5.3.0", + "elliptic": "^6.5.4", + "safe-buffer": "^5.0.1" + } + }, "jxon": { "version": "2.0.0-beta.5", "resolved": "https://registry.npmjs.org/jxon/-/jxon-2.0.0-beta.5.tgz", @@ -15475,6 +15712,17 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "devOptional": true }, + "mangrove-reviews-typescript": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/mangrove-reviews-typescript/-/mangrove-reviews-typescript-0.0.6.tgz", + "integrity": "sha512-31wF20PdaKUhxP5lek7YouF50QbNk4U571I86e0lG5U/khP96wbVToZB2P4Anb0OPoQ2alHfpqJPuc491ptw2Q==", + "requires": { + "axios": "^1.2.3", + "jose": "^4.11.2", + "jwk-to-pem": "^2.0.5", + "typescript": "^4.9.4" + } + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -15513,6 +15761,16 @@ "dom-walk": "^0.1.0" } }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -16072,6 +16330,11 @@ "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", diff --git a/package.json b/package.json index 34d47fc1a..3f5dc1708 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "leaflet-simple-map-screenshoter": "^0.4.5", "libphonenumber-js": "^1.10.8", "lz-string": "^1.4.4", + "mangrove-reviews-typescript": "^0.0.6", "opening_hours": "^3.6.0", "osm-auth": "^1.0.2", "osmtogeojson": "^3.0.0-beta.5", diff --git a/test.ts b/test.ts index e65fce4fb..909cd17fa 100644 --- a/test.ts +++ b/test.ts @@ -1,26 +1,90 @@ -import { LanguageElement } from "./UI/Popup/LanguageElement" -import { ImmutableStore, UIEventSource } from "./Logic/UIEventSource" +import MangroveReviewsOfFeature, { MangroveIdentity } from "./Logic/Web/MangroveReviews" +import { Feature, Point } from "geojson" +import { OsmTags } from "./Models/OsmFeature" import { VariableUiElement } from "./UI/Base/VariableUIElement" -import Locale from "./UI/i18n/Locale" -import { OsmConnection } from "./Logic/Osm/OsmConnection" +import List from "./UI/Base/List" +import { UIEventSource } from "./Logic/UIEventSource" +import UserRelatedState from "./Logic/State/UserRelatedState" -const tgs = new UIEventSource({ - name: "xyz", - id: "node/1234", - _country: "BE", +const feature: Feature = { + type: "Feature", + id: "node/6739848322", + properties: { + "addr:city": "San Diego", + "addr:housenumber": "2816", + "addr:postcode": "92106", + "addr:street": "Historic Decatur Road", + "addr:unit": "116", + amenity: "restaurant", + cuisine: "burger", + delivery: "yes", + "diet:halal": "no", + "diet:vegetarian": "yes", + dog: "yes", + image: "https://i.imgur.com/AQlGNHQ.jpg", + internet_access: "wlan", + "internet_access:fee": "no", + "internet_access:ssid": "Public-stinebrewingCo", + microbrewery: "yes", + name: "Stone Brewing World Bistro & Gardens", + opening_hours: "Mo-Fr, Su 11:30-21:00; Sa 11:30-22:00", + organic: "no", + "payment:cards": "yes", + "payment:cash": "yes", + "service:electricity": "ask", + takeaway: "yes", + website: "https://www.stonebrewing.com/visit/bistros/liberty-station", + wheelchair: "designated", + "_last_edit:contributor": "Drew Dowling", + "_last_edit:timestamp": "2023-01-11T23:22:28Z", + id: "node/6739848322", + timestamp: "2023-01-11T23:22:28Z", + user: "Drew Dowling", + _backend: "https://www.openstreetmap.org", + _lat: "32.7404614", + _lon: "-117.211684", + _layer: "food", + _length: "0", + "_length:km": "0.0", + "_now:date": "2023-01-20", + "_now:datetime": "2023-01-20 17:46:54", + "_loaded:date": "2023-01-20", + "_loaded:datetime": "2023-01-20 17:46:54", + "_geometry:type": "Point", + _surface: "0", + "_surface:ha": "0", + _country: "us", + }, + geometry: { + type: "Point", + coordinates: [0, 0], + }, +} +const state = new UserRelatedState(undefined) + +state.allElements.addOrGetElement(feature) + +const reviews = MangroveReviewsOfFeature.construct(feature, state) + +reviews.reviews.addCallbackAndRun((r) => { + console.log("Reviews are:", r) }) -Locale.language.setData("nl") -console.log(tgs) -console.log("Locale", Locale.language) -const conn = new OsmConnection({}) -new LanguageElement() - .constr({ osmConnection: conn, featureSwitchIsTesting: new ImmutableStore(true) }, tgs, [ - "language", - "What languages are spoken here?", - "{language()} is spoken here", - "{language()} is the only language spoken here", - "The following languages are spoken here: {list()}", - ]) - .AttachTo("maindiv") +window.setTimeout(async () => { + await reviews.createReview({ + opinion: "Cool bar", + rating: 90, + metadata: { + nickname: "Pietervdvn", + }, + }) + console.log("Submitted review") +}, 1000) -new VariableUiElement(tgs.map(JSON.stringify)).AttachTo("extradiv") +new VariableUiElement( + reviews.reviews.map( + (reviews) => + new List( + reviews.map((r) => r.rating + "% " + r.opinion + " (" + r.metadata.nickname + ")") + ) + ) +).AttachTo("maindiv")