diff options
| author | Adam Soltys <[email protected]> | 2026-03-07 08:41:23 -0800 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-03-07 08:41:23 -0800 |
| commit | 13b2a17e6f6ed1697c51a6650874313a6da4aa8f (patch) | |
| tree | 44ba523709d52f17ca79cc501e684b0a67539dfc /src/components | |
| parent | a1400eea7b1bd9b7e66c0a698bfb02dc82eadb48 (diff) | |
| download | coinos-ui-13b2a17e6f6ed1697c51a6650874313a6da4aa8f.tar.gz coinos-ui-13b2a17e6f6ed1697c51a6650874313a6da4aa8f.zip | |
render decrypted images
Diffstat (limited to 'src/components')
| -rw-r--r-- | src/components/ChatMessage.svelte | 109 |
1 files changed, 98 insertions, 11 deletions
diff --git a/src/components/ChatMessage.svelte b/src/components/ChatMessage.svelte index aeaac210..d28aefc4 100644 --- a/src/components/ChatMessage.svelte +++ b/src/components/ChatMessage.svelte @@ -1,14 +1,50 @@ <script lang="ts"> import { parseContent, isImage, isVideo, isAudio } from "$lib/parseContent"; - let { content, tags = [] }: { content: string; tags?: any[] } = $props(); + let { + content, + tags = [], + decryptMedia, + }: { + content: string; + tags?: any[]; + decryptMedia?: (tag: string[]) => Promise<string>; + } = $props(); - // Extract image URLs from imeta tags (NIP-92) - let imageUrls: string[] = $derived( + // Detect MIP-04 encrypted media (has "v mip04-v2" field) + let encryptedMedia = $derived( + tags.filter( + (t: string[]) => + t[0] === "imeta" && + t.some((v) => v === "v mip04-v2") && + t.some((v) => v.startsWith("url ")), + ), + ); + + // Plain (unencrypted) media from imeta tags + let plainMedia = $derived( tags - .filter((t: string[]) => t[0] === "imeta" && t.some((v) => v.startsWith("m image/"))) - .map((t: string[]) => t.find((v) => v.startsWith("url "))?.slice(4)) - .filter((url): url is string => !!url), + .filter( + (t: string[]) => + t[0] === "imeta" && + !t.some((v) => v === "v mip04-v2") && + t.some((v) => v.startsWith("url ")), + ) + .map((t: string[]) => ({ + url: t.find((v) => v.startsWith("url "))?.slice(4) || "", + mime: t.find((v) => v.startsWith("m "))?.slice(2) || "", + })) + .filter((m) => m.url), + ); + + // Set of all imeta URLs to suppress duplicate link rendering + let imetaUrls = $derived( + new Set([ + ...plainMedia.map((m) => m.url), + ...encryptedMedia + .map((t: string[]) => t.find((v) => v.startsWith("url "))?.slice(4) || "") + .filter(Boolean), + ]), ); let parts = $derived(parseContent({ content, tags })); @@ -28,12 +64,47 @@ } catch {} return `@${pubkey.slice(0, 8)}...`; }; + + const getMime = (tag: string[]): string => + tag.find((v) => v.startsWith("m "))?.slice(2) || "image/jpeg"; </script> -{#each imageUrls as url} - <a href={url} target="_blank" rel="noopener noreferrer"> - <img src={url} alt="" class="chat-img" /> - </a> +{#each encryptedMedia as tag} + {#if decryptMedia} + {@const mime = getMime(tag)} + {#await decryptMedia(tag)} + <div class="chat-img-placeholder"></div> + {:then blobUrl} + {#if mime.startsWith("image/")} + <a href={blobUrl} target="_blank" rel="noopener noreferrer"> + <img src={blobUrl} alt="" class="chat-img" /> + </a> + {:else if mime.startsWith("video/")} + <!-- svelte-ignore a11y_media_has_caption --> + <video src={blobUrl} controls class="chat-video"></video> + {:else if mime.startsWith("audio/")} + <audio src={blobUrl} controls></audio> + {/if} + {:catch} + {@const url = tag.find((v) => v.startsWith("url "))?.slice(4) || ""} + <a href={url} target="_blank" rel="noopener noreferrer" class="chat-link">{truncUrl(url)}</a> + {/await} + {/if} +{/each} + +{#each plainMedia as media} + {#if media.mime.startsWith("image/")} + <a href={media.url} target="_blank" rel="noopener noreferrer"> + <img src={media.url} alt="" class="chat-img" /> + </a> + {:else if media.mime.startsWith("video/")} + <!-- svelte-ignore a11y_media_has_caption --> + <video src={media.url} controls class="chat-video"></video> + {:else if media.mime.startsWith("audio/")} + <audio src={media.url} controls></audio> + {:else} + <a href={media.url} target="_blank" rel="noopener noreferrer" class="chat-link">{truncUrl(media.url)}</a> + {/if} {/each} {#each parts as { type, value }} @@ -42,7 +113,9 @@ {:else if type === "newline"} <br /> {:else if type === "link"} - {#if isImage(value.url)} + {#if imetaUrls.has(value.url)} + <!-- Already rendered as imeta media above --> + {:else if isImage(value.url)} <a href={value.url} target="_blank" rel="noopener noreferrer"> <img src={value.url} alt="" class="chat-img" /> </a> @@ -78,6 +151,20 @@ margin: 4px 0; } + .chat-img-placeholder { + width: 200px; + height: 150px; + border-radius: 8px; + margin: 4px 0; + background: var(--bg-secondary, #e5e7eb); + animation: pulse 1.5s ease-in-out infinite; + } + + @keyframes pulse { + 0%, 100% { opacity: 0.5; } + 50% { opacity: 0.8; } + } + .chat-video { max-width: 100%; border-radius: 8px; |
