aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-10-05 08:30:51 -0700
committerAdam Soltys <[email protected]>2026-10-05 08:30:51 -0700
commit25b910ca99efa46c60896c1e182001cc9d017c41 (patch)
treea97cdbbac3c7bbaa9c497bb7a22f848607a0ea13
parent74c44a3799e44639c0d7941a2d036e15f3af6f57 (diff)
downloadcoinosv3-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.js25
-rw-r--r--src/style.css6
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; }