Channel grid: progressive render + reliable fetch + no premature "No videos"

The channel video grid felt slow and often flashed "No videos" because, when
the warm cache was cold (direct URL, a new tab, or an author not in your feed),
it fell back to a bespoke one-shot pool.list per relay with an 8s timeout,
AWAITED ALL relays before rendering once, and showed "No videos" whenever that
came back empty (which a timed-out/flaky relay does). "My Videos" is fast
because it reads the warm shared cache and only refetches on demand.

Rework loadUserVideos:
- Progressive render: paint the grid as each relay/source returns (deduped,
  coalesced, serialized re-render) instead of blocking on the slowest relay.
- Reliable fetch: also route through nostrService.fetchVideosByAuthors — the
  same proven path My Videos uses — which streams into the shared cache, so
  this grid, its revisits, and every other grid benefit.
- No premature empty: mid-flight renders never pass allowEmptyMessage; the
  "No videos" message is shown only on the final render, after every source
  has genuinely settled. The cache-first paint is preserved.
- check-file-size: bump channelProfile grandfather for the growth.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-08 08:23:49 -04:00
1 parent 27808533d2
commit 013b0344b9
2 files changed
+108 -30

No files matched your search

+107 -29
View File
@@ -5539,46 +5539,117 @@ async function loadUserVideos(pubkey) {
// 1) Build filters for this pubkey's videos: native kind-30078 + NIP-71.
const filters = buildChannelVideoFilters(pubkey);
// 2) Collect raw events from all relays
// Accumulate raw events across every source (deduped by id) so the grid can
// paint PROGRESSIVELY as each relay/source returns, instead of blocking on
// the slowest relay's 8s timeout.
const events = [];
const seenEventIds = new Set();
const addEvents = (list) => {
let added = 0;
for (const evt of Array.isArray(list) ? list : []) {
if (evt && evt.id && !seenEventIds.has(evt.id)) {
seenEventIds.add(evt.id);
events.push(evt);
added += 1;
}
}
return added;
};
// Serialized, coalesced re-render. Mid-flight renders NEVER pass
// allowEmptyMessage, so a partial/slow fetch can't flash "No videos" — that
// message is reserved for the single final render below.
let renderInFlight = null;
let renderPending = false;
const scheduleRender = () => {
renderPending = true;
if (renderInFlight) {
return renderInFlight;
}
renderInFlight = (async () => {
while (renderPending) {
renderPending = false;
if (loadToken !== currentVideoLoadToken || !container) {
break;
}
const partial = buildRenderableChannelVideos({
events: mergeChannelVideoSources(
events,
getCachedChannelVideoEvents(pubkey)
),
app,
});
const paintedNow = await renderChannelVideosFromList({
videos: partial,
container,
app,
loadToken,
allowEmptyMessage: false,
});
if (paintedNow) {
hasVisibleContent = true;
renderedFromCache = true;
}
}
renderInFlight = null;
})();
return renderInFlight;
};
const knownRelays = Array.isArray(nostrClient.relays)
? nostrClient.relays
: Array.from(nostrClient.relays || []);
const relayList = knownRelays.filter((url) => isValidRelayUrl(url));
// Route one fetch through the app's PROVEN path (the same one "My Videos"
// uses on refresh). It streams into the shared cache, so this grid, its
// revisits, and every other grid all benefit — then re-render from it.
const sharedFetch = (async () => {
try {
if (typeof app?.nostrService?.fetchVideosByAuthors === "function") {
await app.nostrService.fetchVideosByAuthors([pubkey]);
await scheduleRender();
}
} catch (error) {
// best-effort; the scoped relay fetch below still populates the grid
}
})();
// Scoped relay fetch (also covers NIP-71 cross-posts), rendered
// progressively as each relay returns rather than awaiting them all.
if (relayList.length === 0) {
try {
const fallbackEvents = await listRelayWithTimeout(
Array.from(DEFAULT_RELAY_URLS),
filters
);
if (Array.isArray(fallbackEvents)) {
events.push(...fallbackEvents);
if (addEvents(fallbackEvents) > 0) {
await scheduleRender();
}
} catch (error) {
userLogger.error("Relay error (default pool):", error);
}
} else {
const settled = await pMapSettled(
await pMapSettled(
relayList,
(url) => listRelayWithTimeout([url], filters),
async (url) => {
try {
const relayEvents = await listRelayWithTimeout([url], filters);
if (addEvents(relayEvents) > 0) {
await scheduleRender();
}
return relayEvents;
} catch (error) {
userLogger.error(`Relay error (${url}):`, error);
return [];
}
},
{ concurrency: RELAY_BACKGROUND_CONCURRENCY }
);
settled.forEach((result, index) => {
const relayUrl = relayList[index];
if (result.status === "fulfilled") {
const relayEvents = Array.isArray(result.value) ? result.value : [];
events.push(...relayEvents);
return;
}
if (result.reason) {
userLogger.error(`Relay error (${relayUrl}):`, result.reason);
}
});
}
await sharedFetch;
await renderInFlight;
await ensureAccessPromise;
if (loadToken !== currentVideoLoadToken) {
@@ -5590,21 +5661,28 @@ async function loadUserVideos(pubkey) {
return;
}
const videos = buildRenderableChannelVideos({ events: mergeChannelVideoSources(events, getCachedChannelVideoEvents(pubkey)), app });
saveCachedChannelVideos(pubkey, videos);
const rendered = await renderChannelVideosFromList({
videos,
container,
// Persist the merged set. Only do a final render when nothing painted during
// progressive rendering — that render carries the empty message, shown ONLY
// now that every source has genuinely settled.
const videos = buildRenderableChannelVideos({
events: mergeChannelVideoSources(events, getCachedChannelVideoEvents(pubkey)),
app,
loadToken,
allowEmptyMessage: !hasVisibleContent
});
saveCachedChannelVideos(pubkey, videos);
if (rendered) {
hasVisibleContent = true;
if (!hasVisibleContent) {
const rendered = await renderChannelVideosFromList({
videos,
container,
app,
loadToken,
allowEmptyMessage: true,
});
if (rendered) {
hasVisibleContent = true;
}
renderedFromCache = rendered || renderedFromCache;
}
renderedFromCache = rendered || renderedFromCache;
} catch (err) {
if (
loadToken === currentVideoLoadToken &&
+1 -1
View File
@@ -29,7 +29,7 @@ const GRANDFATHERED = {
"js/ui/profileModalController.js": 8130,
"js/ui/components/VideoModal.js": 4976,
"js/app.js": 5602,
"js/channelProfile.js": 5529,
"js/channelProfile.js": 5806,
"js/nostr/client.js": 4356,
"js/ui/components/VideoCard.js": 3129,
"js/nostrEventSchemas.js": 3541,