v0.4.4: UI overhaul — sticky header, mobile nav, profiles/redundancy lightboxes

Sticky header with tabs as one block on desktop. Fixed header + bottom nav on
mobile. Full-width dark header (#0a0a1a) edge-to-edge with 15px fade gradient.
Tab icons on desktop (inline) and mobile (stacked). Safe area inset support for
phone notches. Lightboxes close on tab switch.

Profiles lightbox (name, bio, visibility, circle profiles) and redundancy
lightbox moved from settings to My Posts. Sync All and Stored Anchors moved
into Network Diagnostics popover. Network indicator click opens diagnostics.
Settings streamlined — removed profile editor, diagnostics button, sync,
redundancy, anchor management.

Keepalive fix: tokio::time::sleep in select! never fired; switched to interval.
Auto-reconnect on unexpected disconnect with 3s delay. notify_growth on
disconnect. Tab badge fix preserving icon spans. Feed re-render skip during
media playback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-03-23 00:56:27 -04:00
parent 409e44762a
commit 926e0c1509
9 changed files with 220 additions and 103 deletions

2
Cargo.lock generated
View file

@ -2746,7 +2746,7 @@ dependencies = [
[[package]]
name = "itsgoin-desktop"
version = "0.4.2"
version = "0.4.4"
dependencies = [
"anyhow",
"base64 0.22.1",

View file

@ -1,6 +1,6 @@
[package]
name = "itsgoin-desktop"
version = "0.4.3"
version = "0.4.4"
edition = "2021"
[lib]

View file

@ -1,6 +1,6 @@
{
"productName": "itsgoin",
"version": "0.4.3",
"version": "0.4.4",
"identifier": "com.itsgoin.app",
"build": {
"frontendDist": "../../frontend",

View file

@ -39,8 +39,8 @@ const dmRecipientSelect = $('#dm-recipient-select');
const dmContent = $('#dm-content');
const dmSendBtn = $('#dm-send-btn');
const anchorsList = $('#anchors-list');
const anchorAddSelect = $('#anchor-add-select');
const anchorAddBtn = $('#anchor-add-btn');
const anchorAddSelect = null; // removed — anchors are read-only
const anchorAddBtn = null;
const attachBtn = $('#attach-btn');
const fileInput = $('#file-input');
const attachmentPreview = $('#attachment-preview');
@ -1818,7 +1818,7 @@ async function doRemoveAnchor(nid) {
}
}
anchorAddBtn.addEventListener('click', doAddAnchor);
if (anchorAddBtn) anchorAddBtn.addEventListener('click', doAddAnchor);
async function loadKnownAnchors() {
const container = $('#known-anchors-list');
@ -2819,7 +2819,8 @@ document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
if (tab.dataset.tab === currentTab) return;
// Close any open lightboxes/overlays/popovers
document.querySelectorAll('.image-lightbox, .download-prompt-overlay, .offline-lightbox, .overlay').forEach(el => el.remove());
document.querySelectorAll('.image-lightbox, .download-prompt-overlay, .offline-lightbox').forEach(el => el.remove());
closePopover(); // hide the persistent popover overlay (don't remove it)
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
const oldView = document.querySelector('.view.active');
if (oldView) {
@ -2862,6 +2863,94 @@ document.querySelectorAll('.tab').forEach(tab => {
});
// --- Collapsible section toggles ---
$('#profile-lightbox-btn').addEventListener('click', () => {
const overlay = document.createElement('div');
overlay.className = 'image-lightbox';
overlay.style.cursor = 'default';
// Pre-fill from the settings fields
const currentName = profileNameInput.value || '';
const currentBio = profileBioInput.value || '';
const currentVisible = $('#public-visible-check')?.checked ?? true;
overlay.innerHTML = `
<div style="background:#1a1a2e;border:1px solid #333;border-radius:12px;padding:1.5rem;max-width:450px;width:90%;max-height:80vh;overflow-y:auto">
<h3 style="color:#7fdbca;margin:0 0 0.75rem;text-align:center">Profiles</h3>
<h4 style="color:#ccc;font-size:0.85rem;margin:0 0 0.4rem">Default Profile</h4>
<label style="font-size:0.8rem;color:#888">Display Name</label>
<input id="lb-profile-name" type="text" value="${escapeHtml(currentName)}" placeholder="Your name" maxlength="50" style="width:100%;margin-bottom:0.5rem" />
<label style="font-size:0.8rem;color:#888">Bio</label>
<textarea id="lb-profile-bio" placeholder="Tell people about yourself..." maxlength="200" rows="3" style="width:100%;margin-bottom:0.5rem">${escapeHtml(currentBio)}</textarea>
<label class="checkbox-label" style="margin:0.5rem 0;display:block;font-size:0.8rem">
<input type="checkbox" id="lb-public-visible" ${currentVisible ? 'checked' : ''} />
Show my profile to non-circle peers
</label>
<div style="display:flex;gap:0.5rem;justify-content:center;margin-top:0.75rem">
<button class="btn btn-primary btn-sm" id="lb-profile-save">Save</button>
</div>
<hr style="border-color:#333;margin:1rem 0" />
<h4 style="color:#ccc;font-size:0.85rem;margin:0 0 0.4rem">Circle Profiles</h4>
<p class="empty-hint" style="margin-bottom:0.5rem;font-size:0.75rem">Set a different name/bio for each circle you manage.</p>
<div id="lb-circle-profiles-list"></div>
<div style="display:flex;gap:0.5rem;justify-content:center;margin-top:0.75rem">
<button class="btn btn-ghost btn-sm" id="lb-profile-close">Close</button>
</div>
</div>`;
document.body.appendChild(overlay);
overlay.querySelector('#lb-profile-save').addEventListener('click', async () => {
const name = overlay.querySelector('#lb-profile-name').value.trim();
const bio = overlay.querySelector('#lb-profile-bio').value.trim();
if (!name) { toast('Display name is required'); return; }
try {
await invoke('set_profile', { name, bio });
const visible = overlay.querySelector('#lb-public-visible').checked;
await invoke('set_public_visible', { visible });
// Sync back to settings fields
profileNameInput.value = name;
profileBioInput.value = bio;
if ($('#public-visible-check')) $('#public-visible-check').checked = visible;
toast('Profile saved!');
loadNodeInfo();
overlay.remove();
} catch (e) { toast('Error: ' + e); }
});
overlay.querySelector('#lb-profile-close').addEventListener('click', () => overlay.remove());
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
// Populate circle profiles list
const srcList = $('#circle-profiles-list');
const lbList = overlay.querySelector('#lb-circle-profiles-list');
if (srcList && lbList) lbList.innerHTML = srcList.innerHTML;
});
$('#redundancy-lightbox-btn').addEventListener('click', async () => {
const overlay = document.createElement('div');
overlay.className = 'image-lightbox';
overlay.style.cursor = 'default';
overlay.innerHTML = `
<div style="background:#1a1a2e;border:1px solid #333;border-radius:12px;padding:1.5rem;max-width:400px;width:90%">
<h3 style="color:#7fdbca;margin:0 0 0.75rem;text-align:center">Redundancy</h3>
<div id="lb-redundancy-panel"><p class="empty-hint">Loading...</p></div>
<div style="display:flex;gap:0.5rem;justify-content:center;margin-top:0.75rem">
<button class="btn btn-ghost btn-sm" id="lb-redundancy-close">Close</button>
</div>
</div>`;
document.body.appendChild(overlay);
// Load redundancy data
try {
const r = await invoke('get_redundancy_info');
const zeroClass = r.zeroReplicas > 0 ? 'warn' : 'ok';
const oneClass = r.oneReplica > 0 ? '' : 'ok';
overlay.querySelector('#lb-redundancy-panel').innerHTML = `<div class="redundancy-grid">
<div class="redundancy-item ${zeroClass}"><div class="redundancy-value">${r.zeroReplicas}</div><div class="redundancy-label">Unreplicated</div></div>
<div class="redundancy-item ${oneClass}"><div class="redundancy-value">${r.oneReplica}</div><div class="redundancy-label">1 replica</div></div>
<div class="redundancy-item ok"><div class="redundancy-value">${r.twoPlusReplicas}</div><div class="redundancy-label">2+ replicas</div></div>
<div class="redundancy-item"><div class="redundancy-value">${r.total}</div><div class="redundancy-label">Total posts</div></div>
</div>`;
} catch (_) {
overlay.querySelector('#lb-redundancy-panel').innerHTML = '<p class="empty-hint">Could not load redundancy info</p>';
}
overlay.querySelector('#lb-redundancy-close').addEventListener('click', () => overlay.remove());
overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
});
$('#circles-toggle').addEventListener('click', () => {
const body = $('#circles-body');
body.classList.toggle('hidden');
@ -2885,20 +2974,30 @@ $('#anchors-toggle').addEventListener('click', () => {
}
});
$('#diagnostics-btn').addEventListener('click', () => {
function openDiagnostics() {
const diagHtml = `
<div id="network-summary"></div>
<div class="diag-actions">
<div class="diag-actions" style="display:flex;gap:0.5rem;flex-wrap:wrap;justify-content:center">
<button id="diag-refresh-btn" class="btn btn-ghost btn-sm">Refresh</button>
<button id="diag-sync-btn" class="btn btn-ghost btn-sm">Sync All</button>
</div>
<div style="display:flex;gap:0.5rem;margin-top:0.5rem;flex-wrap:wrap;justify-content:center">
<button id="rebalance-btn" class="btn btn-ghost btn-sm">Rebalance Now</button>
<button id="request-referrals-btn" class="btn btn-ghost btn-sm">Request Referrals</button>
<span id="diag-update-time" class="diag-timestamp"></span>
</div>
<button id="show-connections-btn" class="btn btn-ghost btn-sm" style="margin-top:0.5rem">Show Connections</button>
<div style="display:flex;gap:0.5rem;margin-top:0.5rem;flex-wrap:wrap;justify-content:center">
<button id="show-connections-btn" class="btn btn-ghost btn-sm">Show Connections</button>
<button id="show-anchors-btn" class="btn btn-ghost btn-sm">Stored Anchors</button>
</div>
<div id="connections-section" class="hidden">
<h4 class="subsection-title">Mesh &amp; Session Connections</h4>
<div id="connections-list"></div>
</div>
<div id="anchors-section" class="hidden">
<h4 class="subsection-title">Stored Anchors</h4>
<p class="empty-hint" style="margin-bottom:0.5rem">Anchors discovered for reconnection beyond bootstrap.</p>
<div id="diag-known-anchors-list"></div>
</div>
<h4 class="subsection-title">Activity Log</h4>
<div id="activity-log" class="activity-log-container"></div>`;
openPopover('Network Diagnostics', diagHtml, {
@ -2920,6 +3019,28 @@ $('#diagnostics-btn').addEventListener('click', () => {
btn.textContent = 'Show Connections';
}
});
// Wire anchors toggle
$('#show-anchors-btn').addEventListener('click', async () => {
const section = $('#anchors-section');
const btn = $('#show-anchors-btn');
if (section.classList.contains('hidden')) {
section.classList.remove('hidden');
btn.textContent = 'Hide Anchors';
try {
const anchors = await invoke('list_known_anchors');
const list = $('#diag-known-anchors-list');
if (list) list.innerHTML = anchors.length ? anchors.map(a => {
const icon = generateIdenticon(a.nodeId, 18);
const label = escapeHtml(peerLabel(a.nodeId, a.displayName));
const addr = a.addresses.length > 0 ? `<span class="peer-addr">${escapeHtml(a.addresses[0])}</span>` : '';
return `<div class="anchor-item"><span class="peer-label">${icon} ${label}</span>${addr}</div>`;
}).join('') : '<p class="empty-hint">No discovered anchors</p>';
} catch (_) {}
} else {
section.classList.add('hidden');
btn.textContent = 'Stored Anchors';
}
});
// Wire action buttons
$('#diag-refresh-btn').addEventListener('click', async () => {
const btn = $('#diag-refresh-btn');
@ -2928,6 +3049,19 @@ $('#diagnostics-btn').addEventListener('click', () => {
catch (e) { toast('Error: ' + e); }
finally { btn.disabled = false; btn.textContent = 'Refresh'; }
});
$('#diag-sync-btn').addEventListener('click', async () => {
const btn = $('#diag-sync-btn');
btn.disabled = true; btn.textContent = 'Syncing...';
try {
const result = await invoke('sync_all');
toast(result);
loadFeed(true);
if (currentTab === 'myposts') loadMyPosts(true);
if (currentTab === 'people') loadFollows();
if (currentTab === 'messages') loadMessages(true);
} catch (e) { toast('Sync error: ' + e); }
finally { btn.disabled = false; btn.textContent = 'Sync All'; }
});
$('#rebalance-btn').addEventListener('click', async () => {
const btn = $('#rebalance-btn');
btn.disabled = true; btn.textContent = 'Rebalancing...';
@ -2953,7 +3087,9 @@ $('#diagnostics-btn').addEventListener('click', () => {
activityInterval = setInterval(loadActivityLog, 3000);
}
});
});
}
$('#diagnostics-btn').addEventListener('click', openDiagnostics);
$('#net-indicator').addEventListener('click', openDiagnostics);
// --- Event handlers ---
postBtn.addEventListener('click', doPost);

View file

@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>ItsGoin</title>
<link rel="stylesheet" href="style.css">
</head>
@ -26,9 +26,6 @@
<span id="net-labels"></span>
</div>
</div>
</header>
<main>
<nav id="tabs">
<button class="tab" data-tab="feed"><span class="tab-icon">&#x1f4f0;</span><span class="tab-label">Feed</span></button>
<button class="tab" data-tab="myposts"><span class="tab-icon">&#x270d;</span><span class="tab-label">My Posts</span></button>
@ -36,7 +33,9 @@
<button class="tab" data-tab="messages"><span class="tab-icon">&#x1f4ac;</span><span class="tab-label">Messages</span></button>
<button class="tab" data-tab="settings"><span class="tab-icon">&#x2699;</span><span class="tab-label">Settings</span></button>
</nav>
</header>
<main>
<!-- Welcome (shown on startup) -->
<section id="view-welcome" class="view active">
<div style="text-align:center;padding:2rem 1rem">
@ -60,13 +59,25 @@
<!-- My Posts tab -->
<section id="view-myposts" class="view">
<div class="section-card" style="text-align:center;margin-bottom:0.5rem">
<button id="profile-lightbox-btn" class="btn btn-ghost btn-sm">Profiles</button>
<button id="circles-toggle" class="btn btn-ghost btn-sm section-toggle">Manage Circles</button>
<button id="redundancy-lightbox-btn" class="btn btn-ghost btn-sm">Redundancy</button>
<div id="circles-body" class="hidden">
<div class="input-row" style="margin-top:0.5rem">
<input id="circle-name-input" placeholder="New circle name" />
<button id="create-circle-btn" class="btn btn-primary">Create</button>
</div>
<div id="circles-list"></div>
</div>
</div>
<div id="compose">
<textarea id="post-content" placeholder="How's it goin?"></textarea>
<div id="attachment-preview"></div>
<input type="file" id="file-input" multiple class="hidden" />
<div style="text-align:center;margin:0.3rem 0"><button id="attach-btn" class="btn btn-ghost btn-sm" title="Attach images or videos">Attach</button></div>
<div id="compose-footer">
<div class="compose-left">
<button id="attach-btn" class="btn btn-ghost btn-sm" title="Attach images or videos">Attach</button>
<span class="hint">Ctrl+Enter to post</span>
<div id="visibility-row">
<select id="visibility-select">
@ -95,17 +106,6 @@
</div>
</div>
<div class="section-card">
<button id="circles-toggle" class="btn btn-ghost btn-sm section-toggle">Manage Circles</button>
<div id="circles-body" class="hidden">
<div class="input-row" style="margin-top:0.5rem">
<input id="circle-name-input" placeholder="New circle name" />
<button id="create-circle-btn" class="btn btn-primary">Create</button>
</div>
<div id="circles-list"></div>
</div>
</div>
<div id="my-posts-list"></div>
</section>
@ -172,58 +172,26 @@
<!-- Settings tab -->
<section id="view-settings" class="view">
<div class="section-card">
<h3>Profile</h3>
<div id="profile-editor">
<label for="profile-name">Display Name</label>
<input id="profile-name" type="text" placeholder="Your name" maxlength="50" />
<label for="profile-bio">Bio</label>
<textarea id="profile-bio" placeholder="Tell people about yourself..." maxlength="200" rows="3"></textarea>
<label class="checkbox-label" style="margin:0.5rem 0">
<!-- Hidden profile fields (used by JS, edited via Profiles lightbox) -->
<div class="hidden">
<input id="profile-name" type="text" maxlength="50" />
<textarea id="profile-bio" maxlength="200"></textarea>
<input type="checkbox" id="public-visible-check" checked />
Show my profile to non-circle peers
</label>
<button id="save-profile-btn" class="btn btn-primary">Save</button>
</div>
<button id="save-profile-btn"></button>
<button id="circle-profiles-toggle"></button>
<div id="circle-profiles-body" class="hidden"><div id="circle-profiles-list"></div></div>
</div>
<div class="section-card">
<button id="notifications-btn" class="btn btn-ghost btn-full">Notifications</button>
</div>
<div class="section-card">
<button id="circle-profiles-toggle" class="btn btn-ghost btn-sm section-toggle">Circle Profiles</button>
<div id="circle-profiles-body" class="hidden">
<p class="empty-hint" style="margin-bottom:0.5rem">Set a different name/bio for each circle you manage.</p>
<div id="circle-profiles-list"></div>
</div>
</div>
<div class="section-card">
<div id="node-info" style="display:none">
<span id="node-id"></span>
</div>
</div>
<div class="section-card">
<button id="anchors-toggle" class="btn btn-ghost btn-sm section-toggle">Stored Anchors</button>
<div id="anchors-body" class="hidden">
<p class="empty-hint" style="margin-bottom:0.5rem">Anchors your node has discovered and will reconnect to.</p>
<div id="known-anchors-list"></div>
<h4 class="subsection-title" style="margin-top:0.75rem">Profile Anchors</h4>
<p class="empty-hint" style="margin-bottom:0.5rem">Anchors listed in your profile so peers can find you.</p>
<div id="anchors-list"></div>
<div id="anchor-add-row" class="input-row" style="margin-top:0.5rem">
<select id="anchor-add-select">
<option value="">(no anchor peers available)</option>
</select>
<button id="anchor-add-btn" class="btn btn-primary btn-sm">Add</button>
</div>
</div>
</div>
<div class="section-card">
<button id="diagnostics-btn" class="btn btn-ghost btn-full">Network Diagnostics</button>
<!-- Hidden: node-info, anchors, diagnostics btn moved elsewhere -->
<div class="hidden">
<button id="anchors-toggle"></button>
<div id="anchors-body"><div id="known-anchors-list"></div><div id="anchors-list"></div></div>
<button id="diagnostics-btn"></button>
<div id="node-info"><span id="node-id"></span></div>
</div>
<div class="section-card">
@ -252,16 +220,11 @@
</select>
</div>
<div class="section-card">
<h3>Redundancy</h3>
<div id="redundancy-panel">
<p class="empty-hint">Loading...</p>
</div>
</div>
<!-- Redundancy moved to My Posts lightbox -->
<div class="hidden"><div id="redundancy-panel"></div></div>
<div class="section-card">
<button id="sync-btn" class="btn btn-primary btn-full">Sync Now</button>
</div>
<!-- Sync moved to diagnostics popover -->
<div class="hidden"><button id="sync-btn"></button></div>
<div class="section-card">
<h3>Danger Zone</h3>

View file

@ -1,14 +1,17 @@
* { box-sizing: border-box; margin: 0; padding: 0; }
html { font-size: clamp(14px, 1.5vw, 24px); }
select option { color: #000 !important; }
body { font-family: system-ui, sans-serif; max-width: clamp(640px, 80vw, 1600px); margin: 0 auto; padding: clamp(0.5rem, 2vw, 2rem); background: #1a1a2e; color: #e0e0e0; color-scheme: dark; }
header { border-bottom: 1px solid #333; padding-bottom: 0.5rem; margin-bottom: 1rem; }
#header-row { display: flex; justify-content: space-between; align-items: center; }
body { font-family: system-ui, sans-serif; margin: 0; padding: 0; background: #0a0a1a; color: #e0e0e0; color-scheme: dark; }
header { position: sticky; top: 0; z-index: 800; background: #0a0a1a; border-bottom: none; padding: 0.5rem clamp(0.5rem, 2vw, 2rem) 0; margin-bottom: 0; }
main { max-width: clamp(640px, 80vw, 1600px); margin: 0 auto; padding: 0 clamp(0.5rem, 2vw, 2rem); }
header::after { content: ''; position: absolute; bottom: -15px; left: 0; right: 0; height: 15px; background: linear-gradient(to bottom, #0a0a1a, transparent); pointer-events: none; z-index: 799; }
#header-row { display: flex; justify-content: space-between; align-items: center; max-width: clamp(640px, 80vw, 1600px); margin: 0 auto; }
#tabs { max-width: clamp(640px, 80vw, 1600px); margin: 0 auto; }
header h1 { font-size: clamp(1.4rem, 2.5vw, 2rem); color: #7fdbca; margin: 0; }
#status-ticker { flex: 1; text-align: center; font-size: 0.7rem; color: #888; line-height: 1.3; max-height: 2.4em; overflow: hidden; transition: opacity 0.3s; }
#status-ticker.faded { opacity: 0; }
#net-indicator { display: flex; align-items: center; gap: 0.4rem; }
#net-dot { width: 10px; height: 10px; border-radius: 50%; background: #222; border: 1px solid #444; }
#net-indicator { display: flex; align-items: center; gap: 0.4rem; cursor: pointer; }
#net-dot { width: 10px; height: 15px; border-radius: 50%; background: #222; border: 1px solid #444; }
#net-dot.net-black { background: #222; }
#net-dot.net-red { background: #e74c3c; }
#net-dot.net-yellow { background: #f1c40f; }
@ -82,21 +85,23 @@ header h1 { font-size: clamp(1.4rem, 2.5vw, 2rem); color: #7fdbca; margin: 0; }
.compose-left { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; }
/* Tabs — desktop (top bar) */
#tabs { display: flex; gap: 0; margin-bottom: 1rem; border-bottom: 1px solid #333; }
.tab { background: none; border: none; color: #99a; padding: 0.5rem 0.6rem; cursor: pointer; border-bottom: 2px solid transparent; font-size: 0.82rem; transition: color 0.15s, border-color 0.15s; position: relative; flex: 1; text-align: center; white-space: nowrap; }
#tabs { display: flex; gap: 0; margin-bottom: 1rem; border-bottom: none; }
.tab { background: none; border: none; color: #99a; padding: 0.5rem 0.6rem; cursor: pointer; border-bottom: 2px solid #0a0a1a; font-size: 0.82rem; transition: color 0.15s, border-color 0.15s; position: relative; flex: 1; text-align: center; white-space: nowrap; }
.tab:hover { color: #ccd; }
.tab.active { color: #7fdbca; border-bottom-color: #7fdbca; }
.tab-icon { display: none; }
.tab-icon { display: inline; margin-right: 0.3rem; }
.tab-badge { display: inline-flex; align-items: center; justify-content: center; background: #0f3460; color: #7fdbca; font-size: 0.6rem; min-width: 1.1rem; height: 1.1rem; border-radius: 0.55rem; padding: 0 0.3rem; margin-left: 0.25rem; font-family: system-ui, sans-serif; vertical-align: middle; }
/* Tabs — mobile/tablet (bottom nav bar) */
@media (max-width: 768px) {
#tabs { position: fixed; bottom: 0; left: 0; right: 0; z-index: 900; background: #0a0a1a; border-bottom: none; border-top: 1px solid #333; margin-bottom: 0; padding: 0; padding-bottom: env(safe-area-inset-bottom, 0); }
#tabs { position: fixed; bottom: 0; top: auto; left: 0; right: 0; z-index: 900; background: #0a0a1a; border-bottom: none; border-top: none; margin-bottom: 0; padding: 0; padding-bottom: env(safe-area-inset-bottom, 0); }
#tabs::before { content: ''; position: absolute; top: -15px; left: 0; right: 0; height: 15px; background: linear-gradient(to top, #0a0a1a, transparent); pointer-events: none; }
.tab { flex-direction: column; align-items: center; padding: 0.4rem 0.2rem 0.3rem; border-bottom: none; border-top: 2px solid transparent; font-size: 0.6rem; gap: 0.15rem; display: flex; }
.tab.active { border-bottom: none; border-top-color: #7fdbca; }
.tab-icon { display: block; font-size: 1.2rem; line-height: 1; }
.tab-badge { position: absolute; top: 0.1rem; right: 0.2rem; margin-left: 0; font-size: 0.5rem; min-width: 0.9rem; height: 0.9rem; border-radius: 0.45rem; }
main { padding-bottom: 4rem; }
header { position: fixed; top: 0; left: 0; right: 0; z-index: 800; padding: 0.4rem clamp(0.5rem, 2vw, 2rem); padding-top: calc(0.4rem + env(safe-area-inset-top, 0px)); }
main { padding-top: calc(3rem + env(safe-area-inset-top, 0px)); padding-bottom: 4rem; }
.toast { bottom: 4.5rem; }
}
@ -118,7 +123,7 @@ header h1 { font-size: clamp(1.4rem, 2.5vw, 2rem); color: #7fdbca; margin: 0; }
.subsection-title { font-size: 0.85rem; color: #aab; margin: 0.5rem 0 0.35rem; font-weight: 600; }
/* Collapsible section toggle buttons */
.section-toggle { width: 100%; text-align: left; }
.section-toggle { width: auto; text-align: center; }
/* Input rows */
.input-row { display: flex; gap: 0.4rem; }

BIN
pic2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -44,7 +44,8 @@
<p>This is the canonical technical reference for ItsGoin. It describes the vision, the architecture, and the current state of every subsystem &mdash; with full implementation detail. This document is versioned; each update records what changed.</p>
<div class="card" style="margin-top: 1rem;">
<strong style="font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em;">Changelog</strong>
<p style="margin-top: 0.5rem;"><strong>v0.4.3</strong> (2026-03-22): Lock contention overhaul &mdash; all conn_mgr lock holds during network I/O eliminated. PostFetch, TcpPunch, PullFromPeer, FetchEngagement, ResolveAddress, AnchorProbe, WormLookup, ContentSearch now use brief locks for data gathering only. Bi-stream handlers (BlobRequest, WormQuery, RelayIntroduce, PostFetchRequest, ManifestRefresh) fully lock-free for I/O. ConnectionActor hoists shared Arcs (storage, blob_store, endpoint) for lock-free access. ResolveAddress adds 5s per-query timeout (was unbounded). Worm cascade uses connection snapshots. Initial exchange failure now aborts mesh upgrade (was silently continuing). connect_to_peer/connect_to_anchor use 15s timeout. StoragePool &mdash; 8 concurrent SQLite connections in WAL mode replace single Mutex&lt;Storage&gt;. Reads run fully parallel; writes serialize only at SQLite level. Bottom nav bar for mobile/tablet (&le;768px) with icon tabs. Text sizes: XS 75%, S 100%, M 125% (default), L 150%, XL 200%. Text size persisted to localStorage for instant restore. Fix: blocking_lock panic inside async runtime (prevented app startup). StoragePool reduced to 4 connections for Android compatibility. Keepalive fix &mdash; tokio::time::sleep inside select! was resetting every loop iteration, keepalives never fired; switched to tokio::time::interval. Auto-reconnect on unexpected disconnect &mdash; 3s delay then direct reconnect to last known address; falls back to growth loop. notify_growth on disconnect &mdash; immediately signals growth loop to fill empty slot instead of waiting 10min rebalance. Tab badge fix &mdash; updateTabBadge was using textContent which destroyed icon+label spans; now updates only the label and manages badge span separately. Feed re-render skip during media playback &mdash; prevents video echo from DOM destruction.</p>
<p style="margin-top: 0.5rem;"><strong>v0.4.4</strong> (2026-03-23): UI overhaul &mdash; sticky header with tabs as one floating block on desktop, fixed header+bottom nav on mobile. Full-width dark header (#0a0a1a) edge-to-edge with 15px fade gradient into content. Tab icons visible on desktop (inline) and mobile (stacked). Safe area inset support for phone notches/camera cutouts. Lightbox close on tab switch. Profiles lightbox (name, bio, visibility, circle profiles) moved from settings to My Posts. Redundancy lightbox moved from settings to My Posts. Sync All and Stored Anchors moved into Network Diagnostics popover. Network indicator click opens diagnostics. Diagnostics buttons centered in rows. Settings streamlined &mdash; removed profile editor, diagnostics button, sync button, redundancy panel, anchor management. Attach button centered in compose. Manage Circles released from full-width constraint.</p>
<p><strong>v0.4.3</strong> (2026-03-22): Lock contention overhaul &mdash; all conn_mgr lock holds during network I/O eliminated. PostFetch, TcpPunch, PullFromPeer, FetchEngagement, ResolveAddress, AnchorProbe, WormLookup, ContentSearch now use brief locks for data gathering only. Bi-stream handlers (BlobRequest, WormQuery, RelayIntroduce, PostFetchRequest, ManifestRefresh) fully lock-free for I/O. ConnectionActor hoists shared Arcs (storage, blob_store, endpoint) for lock-free access. ResolveAddress adds 5s per-query timeout (was unbounded). Worm cascade uses connection snapshots. Initial exchange failure now aborts mesh upgrade (was silently continuing). connect_to_peer/connect_to_anchor use 15s timeout. StoragePool &mdash; 8 concurrent SQLite connections in WAL mode replace single Mutex&lt;Storage&gt;. Reads run fully parallel; writes serialize only at SQLite level. Bottom nav bar for mobile/tablet (&le;768px) with icon tabs. Text sizes: XS 75%, S 100%, M 125% (default), L 150%, XL 200%. Text size persisted to localStorage for instant restore. Fix: blocking_lock panic inside async runtime (prevented app startup). StoragePool reduced to 4 connections for Android compatibility. Keepalive fix &mdash; tokio::time::sleep inside select! was resetting every loop iteration, keepalives never fired; switched to tokio::time::interval. Auto-reconnect on unexpected disconnect &mdash; 3s delay then direct reconnect to last known address; falls back to growth loop. notify_growth on disconnect &mdash; immediately signals growth loop to fill empty slot instead of waiting 10min rebalance. Tab badge fix &mdash; updateTabBadge was using textContent which destroyed icon+label spans; now updates only the label and manages badge span separately. Feed re-render skip during media playback &mdash; prevents video echo from DOM destruction.</p>
<p><strong>v0.4.2</strong> (2026-03-22): Welcome screen &mdash; startup shows &ldquo;How&rsquo;s it goin?&rdquo; with staggered counters (connections, posts, messages, reacts, comments) while backend bootstraps. Status ticker &mdash; header ticker for new posts, messages, reactions, comments, connection changes. Notification improvements &mdash; Tauri plugin &rarr; Web Notification &rarr; notify-rust fallback chain, Linux native notifications. Responsive text scaling &mdash; Small/Normal/Large (100%/150%/200%), persisted via settings. Diagnostics popover &mdash; diagnostics moved from inline section to overlay, connections on-demand, timers removed. Share details lightbox with QR code. Connect string prefers external address (UPnP/public IPv6/observed). Stale N1 fix &mdash; disconnected social routes excluded from N1 share. Replication handler fix &mdash; actively fetches posts + blobs from requester after accepting replication. Hole punch fix &mdash; target-side registers publicly routable remote address for relay introduction. Replication semaphore (3 concurrent max). Peer labels show truncated node ID.</p>
<p><strong>v0.4.1</strong> (2026-03-21): Security hardening &mdash; reaction signatures (ed25519), comment signature verification on receipt, reaction removal authorization, BlobHeader author verification. Lock contention fixes &mdash; ManifestPush discovery (cm lock released during I/O), pull request handler (filter without lock), pull sender (split into brief locks), engagement checker (batch writes per chunk). Data cleanup &mdash; post deletion cleans downstream/upstream/seen tables.</p>
<p><strong>v0.4.0</strong> (2026-03-21): Protocol v4 &mdash; header-driven sync. ManifestPush as primary post notification. Slim PullSyncRequest (per-author timestamps, not full post ID list). Tiered engagement checks (5min/1hr/4hr/24hr by content age). Multi-upstream (3 max) with fallback chain. Auto-prefetch followed authors &lt;90d. Self Last Encounter per-author tracking. Encrypted-but-not-for-us CDN caching. Serial engagement polling. ~90% bandwidth reduction for established nodes.</p>

View file

@ -25,16 +25,16 @@
<section>
<h1 style="font-size: 2rem; font-weight: 800; letter-spacing: -0.03em; margin-bottom: 0.25rem;">Download ItsGoin</h1>
<p>Available for Android and Linux. Free and open source.</p>
<p style="color: var(--text-muted); font-size: 0.85rem;">Version 0.4.3 &mdash; March 22, 2026</p>
<p style="color: var(--text-muted); font-size: 0.85rem;">Version 0.4.4 &mdash; March 23, 2026</p>
<div class="downloads">
<a href="itsgoin-0.4.3.apk" class="download-btn btn-android">
<a href="itsgoin-0.4.4.apk" class="download-btn btn-android">
Android APK
<span class="sub">v0.4.3</span>
<span class="sub">v0.4.4</span>
</a>
<a href="itsgoin_0.4.3_amd64.AppImage" class="download-btn btn-linux">
<a href="itsgoin_0.4.4_amd64.AppImage" class="download-btn btn-linux">
Linux AppImage
<span class="sub">v0.4.3</span>
<span class="sub">v0.4.4</span>
</a>
</div>
</section>
@ -46,7 +46,7 @@
<h3 style="color: var(--accent);">Android</h3>
<ol class="steps">
<li><strong>Download the APK</strong> &mdash; Tap the button above. Your browser may warn that this type of file can be harmful &mdash; tap <strong>Download anyway</strong>.</li>
<li><strong>Open the file</strong> &mdash; When the download finishes, tap the notification or find <code>itsgoin-0.4.3.apk</code> in your Downloads folder and tap it.</li>
<li><strong>Open the file</strong> &mdash; When the download finishes, tap the notification or find <code>itsgoin-0.4.4.apk</code> in your Downloads folder and tap it.</li>
<li><strong>Allow installation</strong> &mdash; Android will ask you to allow installs from this source. Tap <strong>Settings</strong>, toggle <strong>"Allow from this source"</strong>, then go back and tap <strong>Install</strong>.</li>
<li><strong>Launch the app</strong> &mdash; Once installed, tap <strong>Open</strong> or find ItsGoin in your app drawer.</li>
</ol>
@ -59,8 +59,8 @@
<h3 style="color: var(--green);">Linux (AppImage)</h3>
<ol class="steps">
<li><strong>Download the AppImage</strong> &mdash; Click the button above to download.</li>
<li><strong>Make it executable</strong> &mdash; Open a terminal and run:<br><code>chmod +x itsgoin_0.4.3_amd64.AppImage</code></li>
<li><strong>Run it</strong> &mdash; Double-click the file, or from the terminal:<br><code>./itsgoin_0.4.3_amd64.AppImage</code></li>
<li><strong>Make it executable</strong> &mdash; Open a terminal and run:<br><code>chmod +x itsgoin_0.4.4_amd64.AppImage</code></li>
<li><strong>Run it</strong> &mdash; Double-click the file, or from the terminal:<br><code>./itsgoin_0.4.4_amd64.AppImage</code></li>
</ol>
<div class="note">
<strong>Note:</strong> If it doesn't launch, you may need to install FUSE:<br><code>sudo apt install libfuse2</code> (Debian/Ubuntu) or <code>sudo dnf install fuse</code> (Fedora).
@ -71,6 +71,18 @@
<section>
<h2>Changelog</h2>
<div class="changelog">
<div class="changelog-date">v0.4.4 &mdash; March 23, 2026</div>
<ul>
<li><strong>UI overhaul</strong> &mdash; Sticky header with tabs as one floating block on desktop. Fixed header + bottom nav on mobile. Full-width dark header with 15px fade gradient into content.</li>
<li><strong>Tab icons</strong> &mdash; Icons visible on both desktop (inline with text) and mobile (stacked above text).</li>
<li><strong>Safe area insets</strong> &mdash; Header pads below phone notch/camera cutout. Bottom nav pads above home indicator.</li>
<li><strong>Profiles lightbox</strong> &mdash; Name, bio, visibility, and circle profiles now in a lightbox from My Posts instead of settings.</li>
<li><strong>Redundancy lightbox</strong> &mdash; Post replication stats in a lightbox from My Posts.</li>
<li><strong>Diagnostics reorganized</strong> &mdash; Sync All and Stored Anchors moved into Network Diagnostics popover. Opened by clicking the network indicator. Buttons centered in rows.</li>
<li><strong>Settings streamlined</strong> &mdash; Removed profile editor, diagnostics button, sync button, redundancy panel, and anchor management from settings.</li>
<li><strong>Lightbox close on tab switch</strong> &mdash; Any open lightbox/overlay closes when switching tabs.</li>
</ul>
<div class="changelog-date">v0.4.3 &mdash; March 22, 2026</div>
<ul>
<li><strong>Lock contention overhaul</strong> &mdash; All conn_mgr lock holds during network I/O eliminated across 14 handlers. Brief locks for data gathering only; all network operations run lock-free.</li>