ux: Friend-button default + profile-rename plumbing + export/import clarity

Three of the v0.7.0 device-testing feedback items, deferred for later
rebuild/redeploy (Scott opted to batch UI fixes).

(#1) Friend button on bio modal:
- Primary action when neither following nor vouched: [Friend] (=
  follow + vouch in one click) plus secondary [Follow only].
- When following without a vouch: [Add Vouch] primary, [Unfollow]
  secondary.
- When both follow + vouch (= friends): [Unfriend] (= revoke vouch
  + unfollow, with the rotation-cost confirm wording).
- The standalone [Vouch] / [Revoke Vouch] flows stay reachable from
  the existing Vouches list in Settings.

(#2) Profile shows "unnamed" — bug fix:
- set_profile updated the profiles table + emitted a profile post,
  but never updated posting_identities.display_name. list_posting_
  identities returns from the latter, so the Personas list kept
  showing "(unnamed)" forever after the first-run auto-persona was
  named.
- Now set_profile also upserts the posting_identities row with the
  new display_name (secret_seed + created_at preserved).

(#5) Export/import + persona-vs-device clarity:
- Settings reorder: new "Your data on this device" explainer card up
  top (Personas = who you are to peers; Identities = device's
  network address, rarely useful to touch).
- "Move to another device" section renamed + given a plain-English
  description; primary [Export personas] / [Import from another
  device] buttons.
- "Identities (advanced)" demoted below; warning text added.
- Export wizard heading: "Export your personas"; radio labels use
  persona/keys language consistently.
- Import wizard heading: "Import from another device"; explainer
  notes that the default action restores personas.

Tracking memory created at memory/project_v071_followups.md for
deferred items (#4 PQ vouch delivery, #6 rename, #7 redundancy,
#8/#9/#10 awaiting clarification).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-05-14 22:32:30 -06:00
parent f714a17385
commit 346d23d4d8
3 changed files with 79 additions and 30 deletions

View file

@ -1793,6 +1793,20 @@ impl Node {
}
}
}
// Keep posting_identities.display_name in sync with the
// profile post so the Personas list and any UI reading
// PostingIdentity sees the current name (not the original
// empty/auto-gen one). The upsert preserves the persona's
// secret_seed / created_at; only display_name changes.
if let Ok(Some(existing)) = storage.get_posting_identity(&posting_id) {
let updated = crate::types::PostingIdentity {
node_id: existing.node_id,
secret_seed: existing.secret_seed,
display_name: display_name.clone(),
created_at: existing.created_at,
};
let _ = storage.upsert_posting_identity(&updated);
}
}
// Propagate via neighbor-manifest header diffs like any other post.

View file

@ -1687,12 +1687,13 @@ async function openBioModal(nodeId, preloadedName) {
${bio ? `<p style="font-size:0.9rem;line-height:1.45;margin:0 0 0.75rem">${escapeHtml(bio)}</p>` : '<p class="empty-hint" style="margin:0 0 0.75rem">No bio.</p>'}
<div style="display:flex;gap:0.4rem;flex-wrap:wrap">
<button id="bio-view-posts" class="btn btn-primary btn-sm">View Posts</button>
${following
? `<button id="bio-unfollow" class="btn btn-ghost btn-sm">Unfollow</button>`
: `<button id="bio-follow" class="btn btn-primary btn-sm">Follow</button>`}
${isVouched
? `<button id="bio-revoke-vouch" class="btn btn-ghost btn-sm">Revoke Vouch</button>`
: `<button id="bio-vouch" class="btn btn-ghost btn-sm">Vouch</button>`}
${(following && isVouched)
? `<button id="bio-unfriend" class="btn btn-ghost btn-sm">Unfriend</button>`
: (following
? `<button id="bio-vouch" class="btn btn-primary btn-sm">Add Vouch</button>
<button id="bio-unfollow" class="btn btn-ghost btn-sm">Unfollow</button>`
: `<button id="bio-friend" class="btn btn-primary btn-sm">Friend</button>
<button id="bio-follow" class="btn btn-ghost btn-sm">Follow only</button>`)}
<button id="bio-message" class="btn btn-ghost btn-sm">Message</button>
${isIgnored
? `<button id="bio-unignore" class="btn btn-ghost btn-sm">Unignore</button>`
@ -1749,16 +1750,34 @@ async function openBioModal(nodeId, preloadedName) {
} catch (e) { toast('Error: ' + e); }
finally { vouch.disabled = false; }
};
const revokeVouch = document.getElementById('bio-revoke-vouch');
if (revokeVouch) revokeVouch.onclick = async () => {
if (!confirm(`Revoke vouch for ${name}? This rotates your vouch key — they keep access to existing posts but not future ones.`)) return;
revokeVouch.disabled = true;
// Friend = follow + vouch in one click. Default action per v0.7.x UX.
const friend = document.getElementById('bio-friend');
if (friend) friend.onclick = async () => {
friend.disabled = true;
try {
await invoke('follow_node', { nodeIdHex: nodeId });
await invoke('vouch_for_peer', { nodeIdHex: nodeId });
toast(`Friended ${name}`);
close();
loadFollows();
loadFeed(true);
} catch (e) { toast('Error: ' + e); }
finally { friend.disabled = false; }
};
// Unfriend = revoke vouch + unfollow. Rotation cost is real; confirm.
const unfriend = document.getElementById('bio-unfriend');
if (unfriend) unfriend.onclick = async () => {
if (!confirm(`Unfriend ${name}? This revokes your vouch (rotates your vouch key — they keep access to existing posts but not future ones) AND unfollows them.`)) return;
unfriend.disabled = true;
try {
await invoke('revoke_vouch_for_peer', { nodeIdHex: nodeId });
toast('Revoked and rotated');
await invoke('unfollow_node', { nodeIdHex: nodeId });
toast(`Unfriended ${name}`);
close();
loadFollows();
loadFeed(true);
} catch (e) { toast('Error: ' + e); }
finally { revokeVouch.disabled = false; }
finally { unfriend.disabled = false; }
};
} catch (e) {
bodyEl.innerHTML = `<p class="status-err">Error: ${e}</p>`;
@ -4041,14 +4060,14 @@ $('#export-btn').addEventListener('click', () => {
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%;text-align:center">
<h3 style="color:#7fdbca;margin:0 0 0.75rem">Export Data</h3>
<p style="font-size:0.75rem;color:#888;margin-bottom:0.75rem">Choose what to include in the export ZIP.</p>
<div style="background:#1a1a2e;border:1px solid #333;border-radius:12px;padding:1.5rem;max-width:420px;width:90%;text-align:center">
<h3 style="color:#7fdbca;margin:0 0 0.5rem">Export your personas</h3>
<p style="font-size:0.75rem;color:#888;margin-bottom:0.75rem">Save your personas + (optionally) your posts to a ZIP file so you can import them on another device.</p>
<div style="display:flex;flex-direction:column;gap:0.5rem;text-align:left;margin-bottom:1rem">
<label class="checkbox-label"><input type="radio" name="export-scope" value="identity_only" /> Identity key only (tiny backup)</label>
<label class="checkbox-label"><input type="radio" name="export-scope" value="posts_only" /> Posts + media (no key safe to share)</label>
<label class="checkbox-label"><input type="radio" name="export-scope" value="posts_with_identity" checked /> Posts + media + identity key</label>
<label class="checkbox-label"><input type="radio" name="export-scope" value="everything" /> Everything (posts, key, follows, settings)</label>
<label class="checkbox-label"><input type="radio" name="export-scope" value="identity_only" /> Persona keys only (tiny backup &mdash; restores your identity but not your posts)</label>
<label class="checkbox-label"><input type="radio" name="export-scope" value="posts_only" /> Posts + media only (no keys &mdash; safe to share publicly)</label>
<label class="checkbox-label"><input type="radio" name="export-scope" value="posts_with_identity" checked /> Posts + media + persona keys (typical &ldquo;move to new device&rdquo;)</label>
<label class="checkbox-label"><input type="radio" name="export-scope" value="everything" /> Everything (posts, keys, follows, settings)</label>
</div>
<div style="margin-bottom:0.75rem">
<label style="font-size:0.75rem;color:#888">Save to folder:</label>
@ -4118,8 +4137,8 @@ $('#import-btn').addEventListener('click', () => {
overlay.style.cursor = 'default';
overlay.innerHTML = `
<div style="background:#1a1a2e;border:1px solid #333;border-radius:12px;padding:1.5rem;max-width:420px;width:90%;text-align:center">
<h3 style="color:#7fdbca;margin:0 0 0.75rem">Import Data</h3>
<p style="font-size:0.75rem;color:#888;margin-bottom:0.75rem">Select an ItsGoin export ZIP file.</p>
<h3 style="color:#7fdbca;margin:0 0 0.5rem">Import from another device</h3>
<p style="font-size:0.75rem;color:#888;margin-bottom:0.75rem">Select an ItsGoin export ZIP. Default action restores the exported personas onto this device so you can post as them.</p>
<div style="display:flex;gap:0.25rem;margin-bottom:0.75rem">
<input id="import-zip-path" type="text" placeholder="/path/to/itsgoin-export.zip" style="flex:1;font-size:0.8rem" />
<button class="btn btn-ghost btn-sm" id="import-browse">Browse</button>

View file

@ -231,13 +231,13 @@
<button id="check-updates-btn" class="btn btn-ghost btn-sm" style="margin-top:0.5rem">Check now</button>
</div>
<div class="section-card" style="text-align:center">
<h3 style="margin-bottom:0.5rem">Identities</h3>
<div id="identities-list" style="margin-bottom:0.5rem"></div>
<div style="display:flex;gap:0.5rem;justify-content:center;flex-wrap:wrap">
<button id="create-identity-btn" class="btn btn-ghost btn-sm">New Identity</button>
<button id="import-identity-btn" class="btn btn-ghost btn-sm">Import Key</button>
</div>
<div class="section-card" style="text-align:left">
<h3 style="margin-bottom:0.4rem;text-align:center">Your data on this device</h3>
<p class="empty-hint" style="margin-bottom:0.5rem;font-size:0.78rem;line-height:1.5">
<strong style="color:#7fdbca">Personas</strong> are who you are to peers &mdash; the keys you post and message with. Most people only need one. To move your account to a new device, you <em>export your personas</em> from this device and <em>import them</em> on the new one.
<br><br>
<strong style="color:#888">Identities</strong> below are this device's own network address &mdash; usually not what you want to move. Leave them alone unless you know why you're touching them.
</p>
</div>
<div class="section-card" style="text-align:center">
@ -248,9 +248,25 @@
</div>
<div class="section-card" style="text-align:center">
<h3 style="margin-bottom:0.4rem">Move to another device</h3>
<p class="empty-hint" style="margin-bottom:0.5rem;font-size:0.78rem">
Export creates a ZIP containing your personas (and optionally posts/follows). Import on the other device's Settings &gt; Move to another device.
</p>
<div style="display:flex;gap:0.5rem;justify-content:center;flex-wrap:wrap">
<button id="export-btn" class="btn btn-ghost btn-sm">Export</button>
<button id="import-btn" class="btn btn-ghost btn-sm">Import</button>
<button id="export-btn" class="btn btn-primary btn-sm">Export personas</button>
<button id="import-btn" class="btn btn-ghost btn-sm">Import from another device</button>
</div>
</div>
<div class="section-card" style="text-align:center">
<h3 style="margin-bottom:0.4rem;font-size:0.85rem;color:#888">Identities (advanced)</h3>
<p class="empty-hint" style="margin-bottom:0.5rem;font-size:0.72rem">
This device's network address. Changing this is rarely useful &mdash; it lets you move the device's QUIC endpoint, NOT your posting identity.
</p>
<div id="identities-list" style="margin-bottom:0.5rem"></div>
<div style="display:flex;gap:0.5rem;justify-content:center;flex-wrap:wrap">
<button id="create-identity-btn" class="btn btn-ghost btn-sm">New Identity</button>
<button id="import-identity-btn" class="btn btn-ghost btn-sm">Import Key</button>
</div>
</div>