diff options
| author | Adam Soltys <[email protected]> | 2026-10-18 13:26:56 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-10-18 13:26:56 -0700 |
| commit | 73030636812a20fd025b6c558695b50224bdfb0e (patch) | |
| tree | 72e8cb352f9b95bfda482cf4f01a6c74349f04d2 /src | |
| parent | d86f7cb039bbd4bf2873be332af278bc6372aeae (diff) | |
| download | coinosv3-73030636812a20fd025b6c558695b50224bdfb0e.tar.gz coinosv3-73030636812a20fd025b6c558695b50224bdfb0e.zip | |
Live streams have their own chip, after Videos: a grid of the feed's streams on now, a red dot on the chip while someone in the feed is streaming — no longer a strip above every feed's posts.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013ZDf1cyCQLkfjftYhymZAg
Diffstat (limited to 'src')
| -rw-r--r-- | src/features/messages.js | 31 | ||||
| -rw-r--r-- | src/i18n.js | 2 | ||||
| -rw-r--r-- | src/style.css | 6 |
3 files changed, 24 insertions, 15 deletions
diff --git a/src/features/messages.js b/src/features/messages.js index e239aee..8f0ed42 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -7666,6 +7666,7 @@ export function messagesFeature(ctx) { feedAheadTimer = setTimeout(() => { feedAheadTimer = null; if (c !== feed || !ui.chatOpen || ui.msgView !== 'feed' || ui.profilePk || ui.noteThread) return; + if (mediaMode === 'live') return; if (!listView()) { // a grid holds only the posts with media: keep fetching while its // foot is near (within reason — a firehose has no end) @@ -8569,13 +8570,13 @@ export function messagesFeature(ctx) { const s = liveStatus(ev); return h('span', { class: 'live-badge ' + s }, t(s === 'live' ? 'liveBadge' : s === 'planned' ? 'livePlanned' : 'liveEnded')); } - // the row of streams above a feed's posts - function liveStrip(def) { - if (mediaMode === 'articles') return null; - refreshLive(); - const evs = liveForFeed(def).slice(0, 12); - if (!evs.length) return null; - return h('div', { class: 'live-strip', 'data-key': 'live-strip' }, ...evs.map((ev) => { + // the feed's Live chip: its streams on now, as a grid of tiles + function liveGrid(def) { + const evs = liveForFeed(def).slice(0, 60); + if (!evs.length) return liveAsking + ? h('div', { class: 'row gap6', 'data-key': 'live-empty', style: 'justify-content:center;padding:12px 0' }, h('span', { class: 'spinner sm' })) + : h('div', { class: 'small faint', 'data-key': 'live-empty', style: 'text-align:center;padding:12px 0' }, t('mediaNoLive')); + return h('div', { class: 'live-grid', 'data-key': 'live-grid' }, ...evs.map((ev) => { const host = liveHost(ev), n = liveViewers(ev); return h('button', { type: 'button', class: 'live-tile', 'data-key': 'live:' + liveAddr(ev), onClick: (e) => { e.stopPropagation(); openLive(ev); } }, h('div', { class: 'live-cover', style: liveCoverStyle(ev) }, @@ -12757,7 +12758,7 @@ export function messagesFeature(ctx) { // it), built outside the morph so a repaint never restarts a playing clip. const MEDIA_MODE_KEY = 'coinos-feed-media'; let mediaMode = 'all'; - try { const m = localStorage.getItem(MEDIA_MODE_KEY); if (m === 'replies' || m === 'images' || m === 'videos' || m === 'articles') mediaMode = m; } catch {} + try { const m = localStorage.getItem(MEDIA_MODE_KEY); if (m === 'replies' || m === 'images' || m === 'videos' || m === 'live' || m === 'articles') mediaMode = m; } catch {} // Notes and Conversations are lists of posts (the feed's own state, or its // Conversations view); the rest are grids read from the media index. const listView = () => mediaMode === 'all' || mediaMode === 'replies'; @@ -13455,8 +13456,13 @@ export function messagesFeature(ctx) { class: 'feed-chip media-mode' + (mediaMode === m ? ' on' : ''), type: 'button', 'data-mode': m, onClick: () => setMediaMode(m), }, label); + // Live wears a red dot while someone in the feed is streaming + const def = feedDef(curFeedId); + const onAir = def && liveForFeed(def).length > 0; return h('div', { class: 'row media-modes', 'data-key': 'media-modes' }, - b('all', t('mediaAll')), b('replies', t('mediaConversations')), b('images', t('mediaImages')), b('videos', t('mediaVideos')), b('articles', t('mediaArticles'))); + b('all', t('mediaAll')), b('replies', t('mediaConversations')), b('images', t('mediaImages')), b('videos', t('mediaVideos')), + b('live', onAir ? [h('span', { class: 'live-dot', 'aria-hidden': 'true' }), t('mediaLive')] : t('mediaLive')), + b('articles', t('mediaArticles'))); } function mediaGrid(c) { const items = mediaItems(c, mediaMode); @@ -14880,8 +14886,7 @@ export function messagesFeature(ctx) { ), ui.feedRelayEdit && ui.feedRelayEdit.id === def.id ? relayPanel(def) : null, visitor ? null : feedChips(), - mediaModes(), - liveStrip(def), + (refreshLive(), mediaModes()), // floating bottom right, wherever the reader is in the feed: the // composer opens at the top and takes the focus visitor || ui.profCompose != null ? null : h('button', { @@ -14910,6 +14915,7 @@ export function messagesFeature(ctx) { : h('div', { class: 'col', style: 'gap:8px' }, h('div', { class: 'small muted' }, t('feedNoQuery')), h('button', { class: 'btn-sm', onClick: () => openFeedEditor(def) }, t('feedEdit'))) + : mediaMode === 'live' ? liveGrid(def) : (c.booting || c.status === 'loading') && !visible.length ? h('div', { class: 'row gap6', style: 'justify-content:center;padding:12px 0' }, h('span', { class: 'spinner sm' })) : !visible.length @@ -14921,7 +14927,8 @@ export function messagesFeature(ctx) { // posts on their way, or nothing older left to fetch. (Older pages // prepare offscreen, so the line is mostly seen by a reader who // outruns them.) - c.loadingMore || (mediaMode === 'articles' ? articlesCrawling && mediaIndex(c.id).articles.size : !listView() && mediaCrawling) + mediaMode === 'live' ? null + : c.loadingMore || (mediaMode === 'articles' ? articlesCrawling && mediaIndex(c.id).articles.size : !listView() && mediaCrawling) ? h('div', { class: 'row gap6 feed-foot', style: 'justify-content:center;align-items:center;padding:10px 0' }, h('span', { class: 'spinner sm' }), h('span', { class: 'small muted' }, t('feedLoadingMore'))) : !listView() ? (mediaMode === 'articles' && mediaIndex(c.id).art.end ? h('div', { class: 'small faint feed-foot', style: 'text-align:center;padding:10px 0' }, t('feedEnd')) : null) diff --git a/src/i18n.js b/src/i18n.js index b7432b0..db290bb 100644 --- a/src/i18n.js +++ b/src/i18n.js @@ -348,10 +348,12 @@ const STR = { mediaConversations: "Conversations", mediaImages: "Images", mediaVideos: "Videos", + mediaLive: "Live", mediaArticles: "Articles", mediaNoArticles: "No articles in this feed yet.", mediaNoImages: "No pictures in this feed yet.", mediaNoVideos: "No vertical videos in this feed yet.", + mediaNoLive: "Nobody in this feed is live right now.", feedRelays: 'Relays', feedRelaysFormHelp: 'Read this feed from these relays only. Leave empty for the usual ones.', feedAllToggle: 'Every post on these relays (a firehose)', diff --git a/src/style.css b/src/style.css index 235cb4b..fcef5a0 100644 --- a/src/style.css +++ b/src/style.css @@ -894,9 +894,9 @@ html.no-scroll { overflow: hidden; scrollbar-gutter: auto; } .chat-avatar.live-ring:not(.mini)::after { content: 'LIVE'; position: absolute; left: 50%; bottom: -7px; transform: translateX(-50%); padding: 0 4px; border-radius: 3px; font-size: 9px; font-weight: 700; line-height: 13px; letter-spacing: .04em; color: #fff; background: #e0245e; box-shadow: 0 0 0 2px var(--paper); text-transform: none; } -.live-strip { display: flex; gap: 10px; overflow-x: auto; padding: 2px 0 4px; scrollbar-width: none; } -.live-strip::-webkit-scrollbar { display: none; } -.live-tile { flex: 0 0 168px; display: flex; flex-direction: column; gap: 5px; padding: 0; text-align: left; +.live-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 14px 10px; padding: 2px 0 4px; } +.live-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: #e0245e; margin-right: 5px; vertical-align: 1px; } +.live-tile { display: flex; flex-direction: column; gap: 5px; padding: 0; text-align: left; background: none; border: 0; color: inherit; cursor: pointer; min-width: 0; } .live-cover { position: relative; width: 100%; aspect-ratio: 16 / 9; border-radius: 10px; overflow: hidden; background: var(--line) center / cover no-repeat; } |
