diff options
| author | Adam Soltys <[email protected]> | 2026-10-05 08:30:51 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-05 08:30:51 -0700 |
| commit | 25b910ca99efa46c60896c1e182001cc9d017c41 (patch) | |
| tree | a97cdbbac3c7bbaa9c497bb7a22f848607a0ea13 | |
| parent | 74c44a3799e44639c0d7941a2d036e15f3af6f57 (diff) | |
| download | coinosv3-25b910ca99efa46c60896c1e182001cc9d017c41.tar.gz coinosv3-25b910ca99efa46c60896c1e182001cc9d017c41.zip | |
Audio links (mp3/m4a/ogg/opus/wav/flac/aac) render as a player, like Amethyst
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YRC6aKhwAud231ot9FPmhi
| -rw-r--r-- | src/features/messages.js | 25 | ||||
| -rw-r--r-- | src/style.css | 6 |
2 files changed, 31 insertions, 0 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index 3bd9ac2..801ad28 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -5545,6 +5545,7 @@ export function messagesFeature(ctx) { // moov-first file — with a short cap, since it gates the row. Remembered // on disk so a clip seen once is never probed again. const VIDEO_RE = /\.(mp4|webm|mov|m4v)(\?[^\s]*)?$/i; + const AUDIO_RE = /\.(mp3|m4a|aac|ogg|oga|opus|wav|flac)(\?[^\s]*)?$/i; const VIDEO_PROBE_MS = 3000, VIDEO_DIMS_KEEP = 400, VIDEO_DIMS_KEY = 'coinos-video-dims'; const videoDims = new Map(); // url -> { width, height } const videoUnknown = new Set(); // probed this session and no answer: paint 16:9, don't ask again @@ -6887,6 +6888,7 @@ export function messagesFeature(ctx) { if (VIDEO_RE.test(url)) { return videoNode(url, { stable: !!feedPaint }); } + if (AUDIO_RE.test(url)) return audioNode(url, label); if (isImage || /\.(png|jpe?g|gif|webp|avif)(\?[^\s]*)?$/i.test(url)) { const size = feedPaint ? feedMedia(url) : null; if (feedPaint && !size) return h('a', { href: url, target: '_blank', rel: 'noopener noreferrer' }, label || url); @@ -6918,6 +6920,29 @@ export function messagesFeature(ctx) { return h('a', { href: url, target: '_blank', rel: 'noopener noreferrer' }, shown); } + // An audio file (a podcast episode on Blossom, a voice note) as a player, + // the way Amethyst shows one. Nothing is fetched until play; once playing + // the box is the listener's and no repaint touches it. + function audioNode(url, label) { + let name = label; + if (!name) { + try { + const u = new URL(url); + const file = decodeURIComponent(u.pathname.split('/').pop() || ''); + // a Blossom name is the file's hash: the host says more than that does + name = /^[0-9a-f]{64}\./i.test(file) ? u.hostname : file || u.hostname; + } catch { name = url; } + } + return h('div', { class: 'note-audio', onClick: (e) => e.stopPropagation() }, + h('div', { class: 'note-audio-head' }, + h('span', { class: 'note-audio-ico', 'aria-hidden': 'true' }, '\u{1F3A7}'), + h('a', { href: url, target: '_blank', rel: 'noopener noreferrer', class: 'small muted note-audio-name' }, name)), + h('audio', { + controls: true, preload: 'none', src: url, + onPlay: (e) => { const box = e.target.closest('.note-audio'); if (box) box._skipMorph = true; }, + })); + } + // ---- encrypted attachments (NIP-92 imeta + NIP-17 file fields) ---- // A picture in a Concord channel never reaches the media server in the // clear: the sender encrypts it under a one-off key, uploads the blob to diff --git a/src/style.css b/src/style.css index 7544c07..178fd4b 100644 --- a/src/style.css +++ b/src/style.css @@ -811,6 +811,12 @@ html.no-scroll { overflow: hidden; scrollbar-gutter: auto; } .quote-card.clickable { cursor: pointer; } .quote-card.clickable:hover { border-color: var(--line-strong); } .quote-loading { flex-direction: row; align-items: center; gap: 8px; } +/* An audio link as a player (a podcast on Blossom, a voice note). */ +.note-audio { margin: 8px 0; padding: 10px; border: 1px solid var(--line); border-radius: 12px; + background: var(--surface2); display: flex; flex-direction: column; gap: 6px; white-space: normal; } +.note-audio-head { display: flex; align-items: center; gap: 8px; min-width: 0; } +.note-audio-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.note-audio audio { width: 100%; height: 40px; } /* A referenced article or list (nostr:naddr). The cover's box is fixed, so the row doesn't grow under the reader when the picture lands. */ .addr-article { padding: 0 0 10px; overflow: hidden; gap: 6px; } |
