import Component from '@glimmer/component'; 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) { this.args.onClose(); } } @action handleBackgroundClick(e) { // Don't close if clicking on thumbnails, nav buttons, or the close button itself if ( e.target.closest('.thumbnail-strip-container') || e.target.closest('.carousel-nav-btn') || e.target.closest('.close-btn') || e.target.closest('.actions-btn-container') ) { return; } this.handleClose(); } @action selectPhoto(photo) { this.currentPhoto = photo; } @action handleVisiblePhotoChange(photo) { if (this.currentPhoto !== photo) { this.currentPhoto = photo; } } @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) { try { await navigator.clipboard.writeText(this.currentPhoto.eventId); this.toast.show('Event ID copied to clipboard'); } catch (err) { console.error('Failed to copy event ID:', err); this.toast.show('Failed to copy event ID'); } } 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(); } }); }