Keepalive fix, auto-reconnect on disconnect, tab icon fix, video playback guard

Keepalive: tokio::time::sleep inside select! was resetting every iteration —
keepalives never fired. Switched to tokio::time::interval which ticks reliably.
This caused connections to be zombie-reaped (10min timeout with no pings).

Auto-reconnect: unexpected disconnects (stream error, not SocialDisconnectNotice)
now attempt direct reconnect after 3s delay using last known address from peers
table or social route. Falls back to notify_growth() if direct reconnect fails.

Tab icons: updateTabBadge was using textContent which destroyed the icon and
label spans inside tab buttons. Now updates only the .tab-label span and manages
a separate .tab-badge element.

Video playback: feed re-render skipped while any video or audio is actively
playing, preventing echo from DOM destruction and media element recreation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-03-22 23:27:41 -04:00
parent 68afc40b16
commit 6320a82852
4 changed files with 118 additions and 12 deletions

View file

@ -591,8 +591,24 @@ const TAB_BASE_LABELS = { feed: 'Feed', myposts: 'My Posts', people: 'People', m
function updateTabBadge(tabName, count) {
const tab = document.querySelector(`.tab[data-tab="${tabName}"]`);
if (!tab) return;
// Update the label span (preserve icon span)
const label = tab.querySelector('.tab-label');
const base = TAB_BASE_LABELS[tabName] || tabName;
tab.textContent = count > 0 ? `${base} (${count})` : base;
if (label) {
label.textContent = base;
}
// Update or create badge span
let badge = tab.querySelector('.tab-badge');
if (count > 0) {
if (!badge) {
badge = document.createElement('span');
badge.className = 'tab-badge';
tab.appendChild(badge);
}
badge.textContent = count;
} else if (badge) {
badge.remove();
}
}
let _lastFeedViewMs = 0;
@ -743,6 +759,18 @@ async function loadFeed(force) {
} catch (_) {}
}
// Skip full re-render if any video/audio is actively playing (prevents echo/restart)
const mediaPlaying = [...feedList.querySelectorAll('video, audio')].some(el => !el.paused);
if (mediaPlaying) {
// Don't destroy the DOM while media is playing — re-render on next cycle when stopped
return;
}
// Revoke old object URLs to prevent memory leaks
feedList.querySelectorAll('video[src^="blob:"], audio[src^="blob:"], img[src^="blob:"]').forEach(el => {
if (el.src.startsWith('blob:')) URL.revokeObjectURL(el.src);
});
// Preserve expanded comment threads
const expandedComments = new Set();
feedList.querySelectorAll('.comment-thread:not(.hidden)').forEach(el => {
@ -780,6 +808,13 @@ async function loadMyPosts(force) {
const fp = mine.map(p => `${p.id}:${(p.reactionCounts||[]).map(r=>r.emoji+r.count).join(',')}:${p.commentCount||0}`).join('|');
if (!force && fp === _myPostsFingerprint) return;
_myPostsFingerprint = fp;
// Skip re-render if media is playing
const mediaPlaying = [...myPostsList.querySelectorAll('video, audio')].some(el => !el.paused);
if (mediaPlaying) return;
// Revoke old blob URLs
myPostsList.querySelectorAll('video[src^="blob:"], audio[src^="blob:"], img[src^="blob:"]').forEach(el => {
if (el.src.startsWith('blob:')) URL.revokeObjectURL(el.src);
});
const expandedComments = new Set();
myPostsList.querySelectorAll('.comment-thread:not(.hidden)').forEach(el => {
const postEl = el.closest('.post');