From 14827fce3e20c077741b6e0b8712dccd61030076 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A2u=20Cao?= Date: Tue, 5 May 2026 11:05:24 +0200 Subject: [PATCH] Delete own photos --- app/components/photo-gallery.gjs | 126 ++++++- app/services/nostr-data.js | 7 +- app/utils/nostr.js | 2 + .../components/photo-gallery-test.gjs | 329 ++++++++++++++++++ 4 files changed, 456 insertions(+), 8 deletions(-) create mode 100644 tests/integration/components/photo-gallery-test.gjs diff --git a/app/components/photo-gallery.gjs b/app/components/photo-gallery.gjs index 7bf9aca..95de195 100644 --- a/app/components/photo-gallery.gjs +++ b/app/components/photo-gallery.gjs @@ -3,15 +3,36 @@ import { tracked } from '@glimmer/tracking'; import { inject as service } from '@ember/service'; import { action } from '@ember/object'; import { on } from '@ember/modifier'; +import { modifier } from 'ember-modifier'; import { fn } from '@ember/helper'; +import { task } from 'ember-concurrency'; +import { EventFactory } from 'applesauce-core'; import Icon from '#components/icon'; import PhotoCarousel from './photo-carousel'; import DropdownMenu from '#components/dropdown-menu'; export default class PhotoGallery extends Component { @service toast; + @service nostrAuth; + @service nostrData; + @service nostrRelay; + @service blossom; + @tracked currentPhoto = this.args.selectedPhoto || this.args.photos?.[0]; + get isCreator() { + return ( + this.currentPhoto?.pubkey && + this.nostrAuth.pubkey && + this.currentPhoto.pubkey === this.nostrAuth.pubkey + ); + } + + bindKeyboard = modifier((element, [handler]) => { + document.addEventListener('keydown', handler); + return () => document.removeEventListener('keydown', handler); + }); + @action handleClose() { if (this.args.onClose) { @@ -46,6 +67,28 @@ export default class PhotoGallery extends Component { } } + @action + handleKeydown(e) { + if (!this.args.photos || this.args.photos.length === 0) return; + + if (e.key === 'Escape') { + this.handleClose(); + return; + } + + const currentIndex = this.args.photos.indexOf(this.currentPhoto); + if (currentIndex === -1) return; + + if (e.key === 'ArrowLeft' && currentIndex > 0) { + this.currentPhoto = this.args.photos[currentIndex - 1]; + } else if ( + e.key === 'ArrowRight' && + currentIndex < this.args.photos.length - 1 + ) { + this.currentPhoto = this.args.photos[currentIndex + 1]; + } + } + @action async copyEventId(closeMenu) { if (this.currentPhoto?.eventId) { @@ -60,12 +103,83 @@ export default class PhotoGallery extends Component { closeMenu(); } + deletePhotoTask = task(async (closeMenu) => { + if ( + !confirm( + 'Are you sure you want to delete this photo? This cannot be undone.' + ) + ) { + if (closeMenu) closeMenu(); + return; + } + + try { + const eventId = this.currentPhoto.eventId; + + // Publish Nostr kind: 5 deletion event first so we don't end up with dead blossom links on a failure + const factory = new EventFactory({ signer: this.nostrAuth.signer }); + const tags = [['e', eventId]]; + + if (this.currentPhoto.placeIdentifier) { + tags.push(['i', this.currentPhoto.placeIdentifier]); + } + + const template = { + kind: 5, + created_at: Math.floor(Date.now() / 1000), + content: 'Deleted photo', + tags, + }; + + const event = await factory.sign(template); + await this.nostrRelay.publish(this.nostrData.activeWriteRelays, event); + + // Remove from local store by adding the kind 5 to it + this.nostrData.store.add(event); + + // Now that the event is published, try to delete from Blossom + const hashRegex = /[0-9a-f]{64}/i; + + if (this.currentPhoto.url) { + const match = this.currentPhoto.url.match(hashRegex); + if (match) { + try { + await this.blossom.delete(match[0]); + } catch (e) { + console.warn('Failed to delete main image from blossom:', e); + } + } + } + + if (this.currentPhoto.thumbUrl) { + const match = this.currentPhoto.thumbUrl.match(hashRegex); + if (match) { + try { + await this.blossom.delete(match[0]); + } catch (e) { + console.warn('Failed to delete thumb image from blossom:', e); + } + } + } + + this.toast.show('Photo deleted successfully'); + + if (closeMenu) closeMenu(); + this.handleClose(); + } catch (e) { + console.error('Failed to delete photo:', e); + this.toast.show('Failed to delete photo: ' + e.message); + if (closeMenu) closeMenu(); + } + }); +