ux: default visibility = Extended Friends (FoF) + clean visibility picker

#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) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-05-14 22:39:44 -06:00
parent 346d23d4d8
commit 83fd30753f
2 changed files with 27 additions and 25 deletions

View file

@ -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() {