Files
bitvid/components/upload-modal.html
T
thePR0M3TH3ANandClaude Opus 4.8 5466d97544 [ui] Remove NIP-71 authoring sections from upload/edit modals (#58)
bitvid authors kind-30078 and MIRRORS to NIP-71; it never authors NIP-71 directly.
Verified before removing: the My Videos mirror (buildNip71MirrorEvent) derives
everything from the bitvid note itself (duration/dims are auto-captured at upload,
contentWarning comes from the NSFW toggle, imeta is built from the video), and the
only consumer of the form's NIP-71 metadata (publishNip71Video) is gated behind
FEATURE_PUBLISH_NIP71: false — the fields fed a dead path and confused uploaders.

Removed from the upload modal: "NIP-71 Technical" (content warning, duration,
summary + Customize toggle) and the IMETA variant editor. Removed from the edit
modal: kind (21/22) selector, published_at, alt, duration, content warning,
summary, IMETA, text tracks, segments, participants, references. KEPT in both:
hashtags (they feed hashtag feeds, the suggestion chips, and the mirror's t tags)
and the web seeds / torrent (xs) inputs (bitvid-note fields, not NIP-71). All the
JS plumbing is missing-DOM tolerant, so no wiring changes were needed to collect.

Compatibility: existing videos' NIP-71 extras survive a save. EditModal keeps the
video's full stored metadata (storedNip71Metadata) and merges the edited hashtags
into it on submit instead of replacing it with the reduced form's empty output
(t mirror key kept in sync).

Also fixes a latent pre-existing bug the reduction exposed: nip71Edited compared
the submit-side copy AFTER publishedAt normalization (string→timestamp,
delete-empty) against an UN-normalized original, so any video with publishedAt
made every save look "edited". Both sides now share normalizeCollectedNip71.

Tests: +2 in edit-modal-submit-state (stored extras survive a hashtag edit;
untouched save keeps nip71Edited false). Modal/payload/mirror suites green
(39 + 7). Lint + build green.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-02 15:18:10 -04:00

366 lines
19 KiB
HTML

<!-- components/upload-modal.html -->
<div
id="uploadModal"
class="bv-modal hidden items-start justify-center md:items-center"
role="dialog"
aria-modal="true"
aria-labelledby="uploadModalTitle"
aria-describedby="uploadModalDescription"
data-ds="new"
data-testid="upload-modal"
>
<div class="bv-modal-backdrop" data-dismiss></div>
<!-- Modal Container -->
<div class="modal-sheet max-h-modal-shell overflow-hidden flex flex-col w-full max-w-2xl" tabindex="-1">
<!-- Top Bar -->
<div class="modal-header transition-transform duration-150">
<h2 id="uploadModalTitle" class="text-xl font-bold text-text">
Share a Video
</h2>
<button
id="closeUploadModal"
class="modal-close"
type="button"
aria-label="Close upload modal"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<!-- Main Scrollable Body -->
<div class="modal-body space-y-6 overflow-y-auto flex-1 min-h-0 custom-scrollbar p-6">
<!-- Source Selector -->
<div class="flex justify-center mb-2">
<div
class="inline-flex items-center rounded-full bg-surface-alt p-1 text-sm shadow-inner"
role="group"
aria-label="Upload source selector"
>
<button
type="button"
id="btn-mode-upload"
class="rounded-full px-6 py-1.5 font-medium transition-colors text-text bg-surface shadow-sm"
aria-pressed="true"
>
Upload File
</button>
<button
type="button"
id="btn-mode-external"
class="rounded-full px-6 py-1.5 font-medium transition-colors text-muted hover:text-text"
aria-pressed="false"
>
External Link
</button>
</div>
</div>
<form id="unifiedUploadForm" class="space-y-6">
<!-- Source: Upload File -->
<div id="section-source-upload" class="space-y-4">
<div class="relative">
<label
for="input-file"
class="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer bg-surface/30 border-border hover:bg-surface/50 transition-colors"
>
<div class="flex flex-col items-center justify-center pt-5 pb-6">
<svg class="w-8 h-8 mb-3 text-muted" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 16">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 13h3a3 3 0 0 0 0-6h-.025A5.56 5.56 0 0 0 16 6.5 5.5 5.5 0 0 0 5.207 5.021C5.137 5.017 5.071 5 5 5a4 4 0 0 0 0 8h2.167M10 15V6m0 0L8 8m2-2 2 2"/>
</svg>
<p class="mb-2 text-sm text-text"><span class="font-semibold">Click to upload</span> or drag and drop</p>
<p class="text-xs text-muted">MP4, WebM, MKV, MOV (Max 2GB recommended)</p>
</div>
<input id="input-file" type="file" class="hidden" accept="video/*,.m3u8,.ts,.mp4,.webm,.mov,.mkv,.mpg,.mpeg" />
</label>
</div>
<!-- Upload Progress -->
<div id="upload-progress-container" class="space-y-2 hidden">
<div class="flex justify-between text-xs">
<span id="upload-status-text" class="text-text font-medium">Uploading...</span>
<span id="upload-percent-text" class="text-muted">0%</span>
</div>
<progress
id="input-progress"
class="progress progress--surface w-full"
max="100"
value="0"
></progress>
</div>
<!-- Generated Links (Results) -->
<div id="upload-results-container" class="space-y-3 bg-surface/30 p-3 rounded-lg border border-border/50 hidden">
<h4 class="text-xs font-semibold text-text uppercase tracking-wider">Generated Links</h4>
<div class="space-y-1">
<label class="text-xs text-muted">Video URL</label>
<input id="result-video-url" type="text" readonly class="input input-sm text-xs bg-surface-alt font-mono text-muted" placeholder="Pending..." />
</div>
<div class="grid grid-cols-2 gap-3">
<div class="space-y-1">
<label class="text-xs text-muted">Magnet URI</label>
<input id="result-magnet" type="text" readonly class="input input-sm text-xs bg-surface-alt font-mono text-muted" placeholder="Pending..." />
</div>
<div class="space-y-1">
<label class="text-xs text-muted">Torrent URL (xs)</label>
<input id="result-torrent-url" type="text" readonly class="input input-sm text-xs bg-surface-alt font-mono text-muted" placeholder="Pending..." />
</div>
</div>
</div>
<!-- Storage Configuration Toggle -->
<div class="border-t border-border/50 pt-2">
<button
type="button"
id="btn-storage-settings"
class="flex items-center gap-2 text-xs text-muted hover:text-text transition-colors"
aria-expanded="false"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>Storage Status</span>
</button>
<!-- Storage Settings Container -->
<div id="section-storage-settings" class="hidden mt-3 space-y-4 bg-surface-alt/50 p-4 rounded-lg border border-border/50">
<!-- Read-Only Summary (for configured storage) -->
<div id="storage-summary-view" class="hidden space-y-3">
<div class="flex items-center justify-between">
<h4 class="text-sm font-semibold text-text">Active Storage</h4>
<div class="flex items-center gap-2">
<span id="storage-lock-status" class="text-xs text-muted">Locked 🔒</span>
<button type="button" id="btn-storage-unlock" class="text-xs text-accent hover:underline hidden">Unlock</button>
<button type="button" id="btn-manage-storage" class="text-xs text-muted hover:text-text hover:underline">Manage</button>
</div>
</div>
<div class="grid grid-cols-3 gap-2 text-xs">
<div class="min-w-0">
<span class="block text-muted">Provider</span>
<span id="summary-provider" class="block font-medium text-text truncate" tabindex="0">--</span>
</div>
<div class="min-w-0">
<span class="block text-muted">Bucket</span>
<span id="summary-bucket" class="block font-medium text-text truncate" tabindex="0">--</span>
</div>
<div class="min-w-0">
<span class="block text-muted">URL Style</span>
<span id="summary-url-style" class="block font-medium text-text truncate" tabindex="0">--</span>
</div>
</div>
<p id="summary-copy" class="text-xs text-muted">
Your videos will be uploaded using this storage provider.
</p>
</div>
<!-- Empty State / Not Configured -->
<div id="storage-empty-view" class="space-y-4 text-center py-4">
<p class="text-sm text-muted">
Storage is not configured.
</p>
<button
type="button"
id="btn-configure-storage"
class="btn btn-sm btn-secondary"
>
Configure Storage
</button>
</div>
</div>
</div>
</div>
<!-- Source: External Link -->
<div id="section-source-external" class="space-y-4 hidden">
<div class="space-y-2">
<label for="input-url" class="block text-sm font-medium text-text">Video URL</label>
<input id="input-url" type="url" class="input" placeholder="https://example.com/video.mp4" data-testid="upload-url" />
<p class="text-xs text-muted">Primary hosted video source (URL-first playback).</p>
</div>
<div class="space-y-2">
<label for="input-magnet" class="block text-sm font-medium text-text">Magnet Link</label>
<input id="input-magnet" type="text" class="input" placeholder="magnet:?xt=urn:btih:..." data-testid="upload-magnet" />
<p class="text-xs text-muted">Optional WebTorrent fallback when the URL is unavailable.</p>
</div>
</div>
<hr class="border-border/50" />
<!-- Metadata -->
<div class="space-y-4">
<div class="space-y-2">
<label for="input-title" class="block text-sm font-medium text-text">Title</label>
<input id="input-title" type="text" required class="input" placeholder="Give your video a title" data-testid="upload-title" />
</div>
<div class="space-y-2">
<label for="input-description" class="block text-sm font-medium text-text">Description</label>
<textarea id="input-description" rows="4" class="textarea" placeholder="Tell viewers about your video"></textarea>
</div>
<div class="space-y-2">
<label for="input-thumbnail" class="block text-sm font-medium text-text">Thumbnail URL</label>
<div class="flex gap-2">
<input id="input-thumbnail" type="url" class="input flex-1" placeholder="https://example.com/thumbnail.jpg" />
<input id="input-thumbnail-file" type="file" class="hidden" accept="image/*" />
<button
type="button"
id="btn-thumbnail-file"
class="btn-secondary px-3 text-sm whitespace-nowrap"
aria-label="Upload thumbnail image"
>
Browse...
</button>
</div>
<!-- Thumbnail Progress -->
<div id="thumbnail-progress-container" class="space-y-1 hidden mt-2">
<div class="flex justify-between text-xs">
<span id="thumbnail-status-text" class="text-text font-medium">Uploading Thumbnail...</span>
<span id="thumbnail-percent-text" class="text-muted">0%</span>
</div>
<progress
id="thumbnail-progress"
class="progress progress--surface w-full h-1.5"
max="100"
value="0"
></progress>
</div>
</div>
<!-- Hashtags -->
<div class="space-y-2">
<label class="block text-sm font-medium text-text">Hashtags</label>
<div id="hashtag-suggestions-wrap" class="space-y-1 hidden">
<p class="text-xs text-muted">Reuse a tag from your videos:</p>
<div id="hashtag-suggestions" class="flex flex-wrap gap-2"></div>
</div>
<div
class="rounded-lg border border-border/70 bg-surface/30 p-3 space-y-3"
data-nip71-repeater="t"
>
<div id="hashtag-list" class="flex flex-wrap gap-2" data-nip71-list="t"></div>
<div class="flex gap-2">
<button
type="button"
class="btn-ghost px-3 py-1.5 text-xs bg-surface border border-border/50"
data-nip71-add="t"
>
+ Add Tag
</button>
</div>
<template data-nip71-template="t">
<div class="inline-flex items-center bg-surface-alt rounded-full px-1 border border-border/50" data-nip71-entry="t">
<span class="text-muted pl-2 text-sm">#</span>
<input
type="text"
data-nip71-field="value"
placeholder="nostr"
class="bg-transparent border-none text-sm text-text focus:ring-0 p-1 w-24"
/>
<button
type="button"
class="text-muted hover:text-critical p-1 rounded-full"
data-nip71-remove="t"
aria-label="Remove hashtag"
>
&times;
</button>
</div>
</template>
</div>
</div>
</div>
<!-- Flags / Toggles -->
<div class="flex flex-wrap gap-4 py-2">
<label class="flex items-center gap-2 cursor-pointer select-none">
<input type="checkbox" id="check-comments" class="checkbox" checked />
<span class="text-sm text-text">Enable Comments</span>
</label>
<label class="flex items-center gap-2 cursor-pointer select-none">
<input type="checkbox" id="check-nsfw" class="checkbox" />
<span class="text-sm text-text">NSFW</span>
</label>
<label class="flex items-center gap-2 cursor-pointer select-none">
<input type="checkbox" id="check-kids" class="checkbox" />
<span class="text-sm text-text">For Kids</span>
</label>
</div>
<!-- Advanced Options -->
<div class="border-t border-border/70 pt-4">
<button
type="button"
id="btn-advanced-toggle"
class="flex items-center gap-2 text-sm font-medium text-text hover:text-accent transition-colors w-full"
aria-expanded="false"
>
<svg
id="icon-advanced-toggle"
xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 transform text-muted transition-transform duration-150"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
</svg>
<span>Advanced Options</span>
</button>
<div id="section-advanced" class="hidden mt-4 space-y-6 pl-2 border-l-2 border-border/30">
<!-- The old "NIP-71 Technical" inputs (content warning, duration,
summary, IMETA variants) were removed for a cleaner UI: they fed
the disabled FEATURE_PUBLISH_NIP71 path, and everything the NIP-71
mirror needs (duration/dimensions, content warning from the NSFW
toggle, the primary imeta) is derived automatically. -->
<!-- Additional Technical Sections (Web Seeds, etc) -->
<div class="grid gap-3 md:grid-cols-2">
<div class="space-y-2">
<label class="block text-xs font-medium text-muted-strong">Web seeds (one per line)</label>
<textarea id="input-ws" rows="2" class="textarea text-sm" placeholder="https://cdn.example.com/video.mp4&#10;https://backup.example.com/video.mp4"></textarea>
<p class="text-xs text-muted">Add a backup origin so the swarm can keep streaming if the primary is overloaded. Each must serve the exact same file.</p>
</div>
<div class="space-y-2">
<label class="block text-xs font-medium text-muted-strong">Torrent File (xs)</label>
<input id="input-xs" type="url" class="input text-sm" placeholder="https://..." />
</div>
</div>
</div>
</div>
<!-- Submit -->
<div class="pt-4">
<button type="submit" id="btn-submit" class="btn w-full py-3 text-base font-semibold shadow-md" data-testid="upload-submit">
Publish Video
</button>
<p id="submit-status" class="text-center text-xs text-muted mt-2"></p>
</div>
</form>
</div>
</div>
</div>