From f875fc18776484eb0dce057c5f9474ff555a5569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Sat, 18 Apr 2026 18:36:09 +0400 Subject: [PATCH] WIP Add Nostr auth --- app/components/place-photo-upload.gjs | 179 ++++++ app/components/user-menu.gjs | 40 +- app/services/nostr-auth.js | 60 ++ app/services/nostr-relay.js | 25 + package.json | 7 +- pnpm-lock.yaml | 802 ++++++++++++++++++++++++++ 6 files changed, 1110 insertions(+), 3 deletions(-) create mode 100644 app/components/place-photo-upload.gjs create mode 100644 app/services/nostr-auth.js create mode 100644 app/services/nostr-relay.js diff --git a/app/components/place-photo-upload.gjs b/app/components/place-photo-upload.gjs new file mode 100644 index 0000000..86d0343 --- /dev/null +++ b/app/components/place-photo-upload.gjs @@ -0,0 +1,179 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; +import { action } from '@ember/object'; +import { inject as service } from '@ember/service'; +import { on } from '@ember/modifier'; +import { EventFactory } from 'applesauce-core'; + +export default class PlacePhotoUpload extends Component { + @service nostrAuth; + @service nostrRelay; + + @tracked photoUrl = ''; + @tracked osmId = ''; + @tracked geohash = ''; + @tracked status = ''; + @tracked error = ''; + + @action + async login() { + try { + this.error = ''; + await this.nostrAuth.login(); + } catch (e) { + this.error = e.message; + } + } + + @action + async uploadPhoto(event) { + event.preventDefault(); + this.error = ''; + this.status = 'Uploading...'; + + try { + // Mock upload + await new Promise((resolve) => setTimeout(resolve, 1000)); + this.photoUrl = + 'https://dummyimage.com/600x400/000/fff.jpg&text=Mock+Place+Photo'; + this.status = 'Photo uploaded! Ready to publish.'; + } catch (e) { + this.error = 'Upload failed: ' + e.message; + this.status = ''; + } + } + + @action + async publish() { + if (!this.nostrAuth.isConnected) { + this.error = 'You must connect Nostr first.'; + return; + } + + if (!this.photoUrl || !this.osmId || !this.geohash) { + this.error = 'Please provide an OSM ID, Geohash, and upload a photo.'; + return; + } + + this.status = 'Publishing event...'; + this.error = ''; + + try { + const factory = new EventFactory({ signer: this.nostrAuth.signer }); + + // NIP-XX draft Place Photo event + const template = { + kind: 360, + content: '', + tags: [ + ['i', `osm:node:${this.osmId}`], + ['g', this.geohash], + [ + 'imeta', + `url ${this.photoUrl}`, + 'm image/jpeg', + 'dim 600x400', + 'alt A photo of a place', + ], + ], + }; + + // Ensure created_at is present before signing + if (!template.created_at) { + template.created_at = Math.floor(Date.now() / 1000); + } + + const event = await factory.sign(template); + await this.nostrRelay.publish(event); + + this.status = 'Published successfully!'; + // Reset form + this.photoUrl = ''; + this.osmId = ''; + this.geohash = ''; + } catch (e) { + this.error = 'Failed to publish: ' + e.message; + this.status = ''; + } + } + + @action updateOsmId(e) { + this.osmId = e.target.value; + } + @action updateGeohash(e) { + this.geohash = e.target.value; + } + + +} diff --git a/app/components/user-menu.gjs b/app/components/user-menu.gjs index cc76bbf..c1bac0f 100644 --- a/app/components/user-menu.gjs +++ b/app/components/user-menu.gjs @@ -8,6 +8,8 @@ export default class UserMenuComponent extends Component { @service storage; @service osmAuth; + @service nostrAuth; + @action connectRS() { this.args.onClose(); @@ -30,6 +32,21 @@ export default class UserMenuComponent extends Component { this.osmAuth.logout(); } + @action + async connectNostr() { + try { + await this.nostrAuth.login(); + } catch (e) { + console.error(e); + alert(e.message); + } + } + + @action + disconnectNostr() { + this.nostrAuth.logout(); + } +