import React, { useContext, useState, useRef } from "react" import { useGeolocationData } from "../hooks/useGeolocationData" import { typeDropdown, DraftPlace, requiredProperties, SignableDraftPlace, GooglePlaceType, GooglePlaceStatus, AddressKeys, } from "../types/Place" import { IdentityContext } from "../providers/IdentityProvider" import { IdentityContextType } from "../types/IdentityType" import { ModalContextType } from "../types/ModalType" import { ModalContext } from "../providers/ModalProvider" import { DraftPlaceContext } from "../providers/DraftPlaceProvider" import { DraftPlaceContextType } from "../types/Place" import { freshDefaultPlace } from "../libraries/defaultPlace" import { FancyButton } from "./FancyButton" import "../scss/PlaceForm.scss" import { getRelayList, getTag, pool } from "../libraries/Nostr" import Geohash from "latlon-geohash" import { signEvent } from "../libraries/NIP-07" import { createDraftPlace, createNaddr } from "../libraries/draftPlace" import { Event, UnsignedEvent, getEventHash, getSignature } from "nostr-tools" import { decryptPrivateKey } from "../libraries/EncryptAndStoreLocal" import { getUniqueDraftBeaconID } from "../libraries/NIP-33" /* create a tsx form to handle input for a new place based on the examplePlace: const examplePlace = ` { "kind": 37515, "tags": [ [ "d", "North Dakota Heritage Center & State Museum" ], [ "g", "cb266epj" ], [ "alt", "This event represents a place. View it on go.yondar.me/${exampleNaddr}" ] ], "content": { "type": "Feature", "properties": { "name": "North Dakota Heritage Center & State Museum", "abbrev": "Heritage Center", "description": "State history museum offering exhibits on the state's geologic prehistory, early peoples & culture.", "address": { "street-address": "612 E Boulevard Ave", "locality": "Bismarck", "region": "North Dakota", "country-name": "United States", "postal-code": "58505" }, "type": "museum", "status": "OPERATIONAL", "website": "https://statemuseum.nd.gov/", "phone": "+1-701-328-2666", "hours": "Mo-Fr 08:00-17:00; Sa-Su 10:00-17:00" }, "geometry": { "coordinates": [ -100.77873491903246, 46.81915362955226 ], "type": "Point" } } }` */ type PlaceFormProps = { edit: boolean // if true, we are editing; if false, we are creting a new place. You may recall that the modal can have a third value of 'edit' but this is abstracted away in the Dashboard.tsx component on line 63. https://github.com/innovatario/yondar-mono/blob/8d7d10fdd83b2deb0238768dbcfb8b60e2f1b305/src/components/Dashboard.tsx#L63 } export const PlaceForm: React.FC = ({ edit = false }) => { const { identity, relays } = useContext(IdentityContext) const { draftPlace, setDraftPlace } = useContext(DraftPlaceContext) const { cursorPosition, setCursorPosition } = useGeolocationData() const { modal } = useContext(ModalContext) // state for name field value so we can get an updated naddr const [naddr, setNaddr] = useState("") // state to refresh form on reset const [formKey, setFormKey] = useState(0) // refs for all fields const nameRef = useRef(null) const abbrevRef = useRef(null) const descriptionRef = useRef(null) const streetAddressRef = useRef(null) const localityRef = useRef(null) const regionRef = useRef(null) const countryNameRef = useRef(null) const postalCodeRef = useRef(null) const typeRef = useRef(null) const statusRef = useRef(null) const hoursRef = useRef(null) const websiteRef = useRef(null) const phoneRef = useRef(null) const relayList = getRelayList(relays, ['write']) // get geohash from coordinates from latlong-geohash library // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const geohash = Geohash.encode(cursorPosition!.lat, cursorPosition!.lng, 5) const updateNaddr = () => { const naddr = createNaddr(identity.pubkey, nameRef.current?.value || "", relayList.sort( (a, b) => a.length - b.length).slice(0, 3)) setNaddr(naddr) } // get data from form // compile it into a new DraftPlace // return the DraftPlace for saving to storage const prepareFormData = (): DraftPlace => { updateNaddr() const dtag = draftPlace.tags.find(getTag("d")) const unique = edit && dtag ? dtag[1] : null return createDraftPlace( nameRef.current?.value || "", geohash, naddr, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion descriptionRef.current?.value || "", typeRef.current?.value as GooglePlaceType || "point_of_interest" as GooglePlaceType, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion [cursorPosition!.lng, cursorPosition!.lat], unique ? unique : nameRef.current?.value || "", abbrevRef.current?.value || "", streetAddressRef.current?.value || "", localityRef.current?.value || "", regionRef.current?.value || "", countryNameRef.current?.value || "", postalCodeRef.current?.value || "", statusRef.current?.value as GooglePlaceStatus || "OPERATIONAL" as GooglePlaceStatus, hoursRef.current?.value || "", websiteRef.current?.value || "", phoneRef.current?.value || "" ) } // called when a fied changes // take all current form values and construct a new DraftPlace // then set the DraftPlace in storage/state const updateDraft = () => { const newPlace = prepareFormData() setDraftPlace(newPlace) } const resetForm = () => { setDraftPlace(freshDefaultPlace()) setFormKey(formKey + 1) } const cancelEdit = () => { resetForm() setCursorPosition(null) modal?.setPlaceForm(false) } const publishDeletion = async () => { const atag = getUniqueDraftBeaconID(draftPlace, identity.pubkey) const deletion = { content: prompt("Please enter a reason for deleting this place.") || "", created_at: Math.floor(Date.now() / 1000), kind: 5, pubkey: identity.pubkey, tags: [ ["a", atag], ] } as UnsignedEvent<5> // publish the event deletion let signedEvent if (localStorage.getItem("storens")){ // sign via nostr-tools let sk const tryToSign = async () => { try { sk = await decryptPrivateKey('signing') } catch (e) { alert("Wrong password! Could not decrypt local signing key. Please try publishing again.") } } await tryToSign() if (!sk) signedEvent = null else { const eventHash = await getEventHash(deletion) const eventSig = await getSignature(deletion, sk) signedEvent = deletion as Event<5> signedEvent.id = eventHash signedEvent.sig = eventSig } } else { signedEvent = await signEvent(deletion) } if (signedEvent === null) { // TODO: notify user console.error("Failed to sign event.") return } console.log('signedEvent',signedEvent) const relayList = getRelayList(relays, ['write']) const pub = pool.publish(relayList, signedEvent) pub.on("ok", () => { console.log("Event published successfully!") // TODO: clear form, show success message, close modal, happy animation, zoom in on new place? modal?.setPlaceForm(false) resetForm() setCursorPosition(null) }) pub.on("failed", (reason: string) => { console.error(`Failed to publish event. ${reason}`) }) } const publish = async () => { // create an event from the form data conforming to the type DraftPlace if (!nameRef.current?.value) return // name is required if (!cursorPosition) return // this should never happen const newPlace = prepareFormData() // eliminate empty optional fields Object.keys(newPlace.content.properties).forEach((key) => { if ( !requiredProperties.includes(key) && newPlace.content.properties[key] === "" ) { delete newPlace.content.properties[key] } }) // eliminate empty fields from address property, and eliminate the address property altogether if it is totally full of empty fields: if (newPlace.content.properties.address !== undefined) { Object.keys(newPlace.content.properties.address).forEach((key) => { if (newPlace.content.properties.address !== undefined) { if (newPlace.content.properties.address[key as AddressKeys] === "" || newPlace.content.properties.address[key as AddressKeys] === undefined) { delete newPlace.content.properties.address[key as AddressKeys] } } }) if (Object.keys(newPlace.content.properties.address).length === 0) { delete newPlace.content.properties.address } } // stringify content property const signableDraftPlace: SignableDraftPlace = { ...newPlace, content: JSON.stringify(newPlace.content), created_at: Math.floor(Date.now() / 1000), pubkey: identity.pubkey, } // publish the event let signedEvent if (localStorage.getItem("storens")){ // sign via nostr-tools let sk const tryToSign = async () => { try { sk = await decryptPrivateKey('signing') } catch (e) { alert("Wrong password! Could not decrypt local signing key. Please try publishing again.") } } await tryToSign() if (!sk) signedEvent = null else { const eventHash = await getEventHash(signableDraftPlace) const eventSig = await getSignature(signableDraftPlace, sk) signedEvent = signableDraftPlace as Event<37515> signedEvent.id = eventHash signedEvent.sig = eventSig } } else { signedEvent = await signEvent(signableDraftPlace) } if (signedEvent === null) { // TODO: notify user console.error("Failed to sign event.") return } const relayList = getRelayList(relays, ['write']) const pub = pool.publish(relayList, signedEvent) pub.on("ok", () => { console.log("Event published successfully!") // TODO: clear form, show success message, close modal, happy animation, zoom in on new place? modal?.setPlaceForm(false) resetForm() setCursorPosition(null) }) pub.on("failed", (reason: string) => { console.error(`Failed to publish event. ${reason}`) }) } const editClass = edit ? 'edit' : '' return (
{ edit ? : }

{edit ? "Edit your" : "Add a"} Place 📍

{edit ? null : (

Places can be edited later! They are replaceable events.

)} updateDraft()} /> updateDraft()} /> {typeDropdown(typeRef, draftPlace.content.properties.type || "", updateDraft)} updateDraft()} /> updateDraft()} /> updateDraft()} />
updateDraft()} /> updateDraft()} /> updateDraft()} /> updateDraft()} /> updateDraft()} />

{edit ? "Edit" : "Publish"} Place { edit ? Delete Place : null }

) }