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:
parent
409e44762a
commit
926e0c1509
9 changed files with 220 additions and 103 deletions
154
frontend/app.js
154
frontend/app.js
|
|
@ -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 & 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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue