From 83fd30753f479a75474d8dd740b087e530b7a1e3 Mon Sep 17 00:00:00 2001 From: Scott Reimers Date: Thu, 14 May 2026 22:39:44 -0600 Subject: [PATCH] ux: default visibility = Extended Friends (FoF) + clean visibility picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #8 from the v0.7.0 device-test feedback round, confirmed by Scott. Promotes FoF from a comment-policy option to a top-level visibility choice, with FoF as the new default. - visibility-select: new option order with `fof_closed` selected by default: - Extended Friends (FoF) ← default - Friends - Public - Circle - comment-perm-select: removed the old `fof_closed` option (its job is now done by the visibility picker). Kept `friends_of_friends` for the Mode 2 combo (public body + FoF-gated comments). - updateVisibilityUI now hides comment-perm-select when visibility is `fof_closed` — the audience choice already implies the comment policy, no extra picker needed. Shown again on public/friends/circle. - Compose dispatch logic re-rooted on visibility instead of comment policy: - fof_closed → create_post_fof_closed (Mode 1) - public + comment-perm=friends_of_friends → create_post_with_fof_comments (Mode 2) - everything else → existing create_post path - After-post reset goes to `fof_closed` (not `public`) to preserve the privacy-by-default posture. Tracker memory updated: #8 marked complete; #10 clarified (per-group persona selection, scoped alongside the deferred Group UI work in #9). Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/app.js | 48 +++++++++++++++++++++++---------------------- frontend/index.html | 4 ++-- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/frontend/app.js b/frontend/app.js index 5bf2922..705cb18 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -2662,7 +2662,7 @@ async function doPost() { try { const vis = visibilitySelect.value; const params = { content: content || '' }; - if (vis !== 'public') { + if (vis !== 'public' && vis !== 'fof_closed') { params.visibility = vis; } if (vis === 'circle') { @@ -2686,28 +2686,11 @@ async function doPost() { const reactPerm = document.getElementById('react-perm-select').value; let result; - if (commentPerm === 'friends_of_friends') { - // FoF Layer 2: body is still public (Mode 2) but the post - // carries a fof_gating block built from the author's - // keyring. Routed through a dedicated command because the - // gating block is signed at publish time (can't be added - // via SetPolicy after the fact). + if (vis === 'fof_closed') { + // Visibility = Extended Friends (FoF). Body + comments are + // encrypted under the FoF gating CEK. Mode 1. if (selectedFiles.length > 0 || params.postingIdHex) { - toast('FoF posts with attachments or non-default persona not yet supported.'); - postBtn.disabled = false; - return; - } - const created = await invoke('create_post_with_fof_comments', { - content: params.content, - }); - result = { id: created.postId }; - } else if (commentPerm === 'fof_closed') { - // FoF Layer 3 / Mode 1: body itself encrypted under the - // gating CEK. Non-FoF observers see only ciphertext; - // FoF readers unlock + decrypt on render via - // read_fof_closed_body. - if (selectedFiles.length > 0 || params.postingIdHex) { - toast('FoFClosed posts with attachments or non-default persona not yet supported.'); + toast('FoF (Extended Friends) posts with attachments or non-default persona not yet supported.'); postBtn.disabled = false; return; } @@ -2715,6 +2698,17 @@ async function doPost() { content: params.content, }); result = { id: created.postId }; + } else if (vis === 'public' && commentPerm === 'friends_of_friends') { + // Public body, FoF-gated comments. Mode 2. + if (selectedFiles.length > 0 || params.postingIdHex) { + toast('FoF-comment posts with attachments or non-default persona not yet supported.'); + postBtn.disabled = false; + return; + } + const created = await invoke('create_post_with_fof_comments', { + content: params.content, + }); + result = { id: created.postId }; } else if (selectedFiles.length > 0) { // Convert ArrayBuffers to base64 strings const files = selectedFiles.map(f => { @@ -2747,7 +2741,7 @@ async function doPost() { selectedFiles = []; renderAttachmentPreview(); updateCharCount(); - visibilitySelect.value = 'public'; + visibilitySelect.value = 'fof_closed'; updateVisibilityUI(); toast('Posted!'); loadFeed(true); @@ -3001,6 +2995,11 @@ async function loadCircleProfiles() { function updateVisibilityUI() { const vis = visibilitySelect.value; circleSelect.classList.toggle('hidden', vis !== 'circle'); + // Hide the comment-permission picker for FoF (Extended Friends) — the + // visibility already implies comments-restricted-to-FoF. Show it + // again when audience is public / friends / circle. + const commentPerm = document.getElementById('comment-perm-select'); + if (commentPerm) commentPerm.classList.toggle('hidden', vis === 'fof_closed'); } async function loadCircleOptions() { @@ -3018,6 +3017,9 @@ visibilitySelect.addEventListener('change', () => { updateVisibilityUI(); if (visibilitySelect.value === 'circle') loadCircleOptions(); }); +// Run once on load so the comment-perm picker is hidden for the +// default FoF visibility (matches the dropdown's `selected` option). +updateVisibilityUI(); // --- Circles management --- async function loadCircles() { diff --git a/frontend/index.html b/frontend/index.html index ba80394..4c02f53 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -96,8 +96,9 @@
@@ -105,7 +106,6 @@ -