diff --git a/src/js/app.js b/src/js/app.js index f1898d2..340a66f 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -279,7 +279,7 @@ class NosTubeApp { magnet: document.getElementById('magnet') ? document.getElementById('magnet').value.trim() : '', thumbnail: document.getElementById('thumbnail') ? document.getElementById('thumbnail').value.trim() : '', description: descriptionElement ? descriptionElement.value.trim() : '', - mode: isDevMode ? 'dev' : 'live', + mode: isDevMode ? 'dev' : 'live' }; // Debugging Log: Check formData @@ -430,6 +430,16 @@ class NosTubeApp { }; const timeAgo = this.formatTimeAgo(video.created_at); + // Only show "Edit" button if this user owns the video (video.pubkey === this.pubkey) + const canEdit = (video.pubkey === this.pubkey); + const editButton = canEdit + ? `` + : ''; + return `
-

${this.escapeHTML(video.title)}

-
+
${profile.name} @@ -467,6 +477,7 @@ class NosTubeApp {
+ ${editButton}
`; @@ -708,6 +719,75 @@ class NosTubeApp { setTimeout(() => this.updateTorrentStatus(torrent), 1000); } } + + /** + * Allows the user to edit a video note (only if they are the owner). + * We reuse the note's existing d tag via nostrClient.editVideo. + * @param {number} index - The index of the video in the rendered list + */ + async handleEditVideo(index) { + try { + const videos = await nostrClient.fetchVideos(); + const video = videos[index]; + + if (!this.pubkey) { + this.showError('Please login to edit videos.'); + return; + } + if (video.pubkey !== this.pubkey) { + this.showError('You do not own this video.'); + return; + } + + // Prompt for new fields, but leave old value if user cancels or leaves blank. + const newTitle = prompt('New Title? (Leave blank to keep existing)', video.title); + const newMagnet = prompt('New Magnet Link? (Leave blank to keep existing)', video.magnet); + const newThumbnail = prompt('New Thumbnail URL? (Leave blank to keep existing)', video.thumbnail); + const newDescription = prompt('New Description? (Leave blank to keep existing)', video.description); + + // If user cancels ANY prompt, it returns `null`. + // If user typed nothing and clicked OK, it’s an empty string ''. + // So we do checks to keep the old value if needed: + const title = (newTitle === null || newTitle.trim() === '') + ? video.title + : newTitle.trim(); + + const magnet = (newMagnet === null || newMagnet.trim() === '') + ? video.magnet + : newMagnet.trim(); + + const thumbnail = (newThumbnail === null || newThumbnail.trim() === '') + ? video.thumbnail + : newThumbnail.trim(); + + const description = (newDescription === null || newDescription.trim() === '') + ? video.description + : newDescription.trim(); + + // Build updated data + const updatedData = { + title, + magnet, + thumbnail, + description, + mode: isDevMode ? 'dev' : 'live' + }; + + const originalEvent = { + id: video.id, + pubkey: video.pubkey, + tags: video.tags // Must include ["d","someValue"] to reuse the same note + }; + + await nostrClient.editVideo(originalEvent, updatedData, this.pubkey); + this.showSuccess('Video updated successfully!'); + await this.loadVideos(); + } catch (err) { + this.log('Failed to edit video:', err.message); + this.showError('Failed to edit video. Please try again later.'); + } + } + } export const app = new NosTubeApp(); diff --git a/src/js/nostr.js b/src/js/nostr.js index 5d02761..2501a8b 100644 --- a/src/js/nostr.js +++ b/src/js/nostr.js @@ -143,7 +143,7 @@ class NostrClient { } /** - * Publishes a new video event to all relays. + * Publishes a new video event to all relays (creates a new note). */ async publishVideo(videoData, pubkey) { if (!pubkey) { @@ -212,6 +212,81 @@ class NostrClient { throw new Error('Failed to sign event.'); } } + + /** + * Edits an existing video event by reusing its "d" tag. + * @param {Object} originalEvent - The entire event object you're editing. + * @param {Object} updatedVideoData - The updated fields (title, magnet, etc.). + * @param {string} pubkey - The user's pubkey (must match originalEvent.pubkey). + */ + async editVideo(originalEvent, updatedVideoData, pubkey) { + if (!pubkey) { + throw new Error('User is not logged in.'); + } + if (originalEvent.pubkey !== pubkey) { + throw new Error('You do not own this event (different pubkey).'); + } + + // Debugging log + if (isDevMode) { + console.log('Editing video event:', originalEvent); + console.log('New video data:', updatedVideoData); + } + + // Grab the d tag from the original event + const dTag = originalEvent.tags.find(tag => tag[0] === 'd'); + if (!dTag) { + throw new Error('This event has no "d" tag, cannot edit as addressable kind=30078.'); + } + const existingD = dTag[1]; + + // Build the updated event with the same (kind, pubkey, d) so relays see it as an update + const event = { + kind: 30078, + pubkey, + created_at: Math.floor(Date.now() / 1000), + tags: [ + ['t', 'video'], + ['d', existingD] + ], + content: JSON.stringify(updatedVideoData) + }; + + if (isDevMode) { + console.log('Reusing d tag:', existingD); + console.log('Updated event content:', event.content); + } + + try { + // Sign the new updated event + const signedEvent = await window.nostr.signEvent(event); + + if (isDevMode) { + console.log('Signed edited event:', signedEvent); + } + + // Publish the edited event to all relays + await Promise.all(this.relays.map(async url => { + try { + await this.pool.publish([url], signedEvent); + if (isDevMode) { + console.log(`Edited event published to ${url} (d="${existingD}")`); + } + } catch (err) { + if (isDevMode) { + console.error(`Failed to publish edited event to ${url}:`, err.message); + } + } + })); + + return signedEvent; + } catch (error) { + if (isDevMode) { + console.error('Failed to sign edited event:', error.message); + } + throw new Error('Failed to sign edited event.'); + } + } /** * Fetches videos from all configured relays. @@ -227,7 +302,6 @@ class NostrClient { // Use a Map so duplicates (same event ID) across multiple relays don't overwrite each other const videoEvents = new Map(); - // Optional: Only log if in dev mode (to avoid flooding console in production). if (isDevMode) { console.log('[fetchVideos] Starting fetch from all relays...'); console.log('[fetchVideos] Filter:', filter); @@ -237,24 +311,20 @@ class NostrClient { // Fetch from each relay in parallel await Promise.all( this.relays.map(async (url) => { - // Log relay being queried if (isDevMode) console.log(`[fetchVideos] Querying relay: ${url}`); try { const events = await this.pool.list([url], [filter]); - // How many events came back from this relay? if (isDevMode) { console.log(`Events from ${url}:`, events.length); - } - - // For deeper insight, you can log each event - if (isDevMode && events.length > 0) { - events.forEach((evt, idx) => { - console.log( - `[fetchVideos] [${url}] Event[${idx}] ID: ${evt.id} | pubkey: ${evt.pubkey} | created_at: ${evt.created_at}` - ); - }); + if (events.length > 0) { + events.forEach((evt, idx) => { + console.log( + `[fetchVideos] [${url}] Event[${idx}] ID: ${evt.id} | pubkey: ${evt.pubkey} | created_at: ${evt.created_at}` + ); + }); + } } // Process each event @@ -272,7 +342,9 @@ class NostrClient { description: content.description || '', mode: content.mode || 'live', pubkey: event.pubkey, - created_at: event.created_at + created_at: event.created_at, + // Keep the original tags array in case we need them later (e.g. for editing) + tags: event.tags }); } } catch (parseError) { @@ -330,7 +402,7 @@ class NostrClient { typeof content.mode === 'string' && ['dev', 'live'].includes(content.mode) && (typeof content.thumbnail === 'string' || typeof content.thumbnail === 'undefined') && - (typeof content.description === 'string' || typeof content.description === 'undefined') // Ensure description is optional + (typeof content.description === 'string' || typeof content.description === 'undefined') ); if (isDevMode && !isValid) {