aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/messages.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/messages.js')
-rw-r--r--src/features/messages.js46
1 files changed, 33 insertions, 13 deletions
diff --git a/src/features/messages.js b/src/features/messages.js
index 54c9cfe..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) },
@@ -9120,6 +9121,7 @@ export function messagesFeature(ctx) {
mayRing: async (pk) => mayRing(pk),
changed: () => { callRing(); render(); },
ended: (rec) => { logCall(rec).catch(() => {}); },
+ warm: (pk) => callWarm(pk),
// a key of our own signs each candidate at once; a remote signer gets
// them all inside the offer or answer instead
trickle: () => { try { if (localStorage.getItem('coinos-call-trickle') === '0') return false; } catch {} const id = hook('nostrLoginIdentity'); return !id || id.signer instanceof Uint8Array; },
@@ -9238,6 +9240,17 @@ export function messagesFeature(ctx) {
}
} catch {}
}
+ // A call with them may be coming (it rings, or their conversation is open):
+ // their relays looked up, TURN fetched — the seconds the first send or
+ // answer used to spend on it. Not through Amber: a prompt to open a chat.
+ const warmed = new Set();
+ function callWarm(pk) {
+ if (!pk || warmed.has(pk)) return;
+ warmed.add(pk);
+ inboxFor(pk).catch(() => {});
+ const login = hook('nostrLoginIdentity');
+ if (!(login && login.signer && login.signer.interactive)) callIce().catch(() => {});
+ }
function startCall(peer, type) {
if (typeof RTCPeerConnection === 'undefined' || !navigator.mediaDevices?.getUserMedia) { toast(t('callUnsupported')); return; }
const cur = callsNow().call;
@@ -12745,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';
@@ -13443,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);
@@ -14868,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', {
@@ -14898,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
@@ -14909,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)
@@ -16467,6 +16486,7 @@ export function messagesFeature(ctx) {
const peer = ui.msgPeer;
if (!peer) { ui.msgView = 'home'; return homeView(); }
const thread = threads.get(peer) || new Map();
+ if (!isMe(peer)) setTimeout(() => callWarm(peer), 1500); // a call may be next
const msgs = foldCallLogs([...thread.values()].sort((a, b) => a.rumor.created_at - b.rumor.created_at));
// a first message may go by White Noise: have their KeyPackage looked up before Send
wnKnow(peer);
@@ -17312,7 +17332,7 @@ export function messagesFeature(ctx) {
clearTimeout(drainTimer);
pendingWraps.clear();
dmStarted = false;
- if (calls) { try { calls.stop(); } catch {} calls = null; callInbox.clear(); iceCache = null; callRing(); }
+ if (calls) { try { calls.stop(); } catch {} calls = null; callInbox.clear(); iceCache = null; warmed.clear(); callRing(); }
listsSynced = false;
if (zapLiveUnsub) { try { zapLiveUnsub(); } catch {} zapLiveUnsub = null; }
clearTimeout(zapTimer); zapTimer = null; zapQueue = new Set(); zapRecent = [];