feat(fof-layer2): Tauri commands + frontend compose/comment routing

Three new Tauri commands surface Layer 2 to the frontend:

- create_post_with_fof_comments(content) → { postId }
  Builds the FoF gating block from the default persona's keyring and
  publishes a Mode 2 post (public body, FoF-gated comments).
- comment_on_fof_post(post_id_hex, body)
  Backwards-compat wrapper: comment_on_post now auto-detects FoF
  gating on the parent post and dispatches internally. Existing
  comment-send UI works unchanged.
- revoke_fof_commenter(post_id_hex, pub_x_index, reason_code)
  Wraps the Node helper. UI for per-comment revocation can hang off
  this without further Node changes.

Node::comment_on_post now branches on parent post fof_gating presence
— this is the central routing point so neither the frontend nor
existing comment-send code needs to know about FoF specifics. FoF
posts get FoF comments automatically; non-FoF posts get the legacy
path. Same Tauri command, same frontend handler.

Frontend:
- Compose: comment-perm-select gains a "Friends of Friends" option.
  When picked, compose dispatches to create_post_with_fof_comments
  instead of the standard create_post. Standard set_comment_policy
  diff fires after so receivers' four-check accept rule activates.
- Attachments + non-default-persona FoF posts are out-of-v1; compose
  reports + aborts rather than silently producing a non-FoF post.

Layer 2 backend + minimal-viable UI complete. Granular per-comment
revoke UI and access-grant UI deferred — Node + Tauri primitives
exist; the surfaces can be added without further crypto work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-05-14 16:07:07 -04:00
parent 96118d7ce8
commit 10de3f6108
4 changed files with 144 additions and 4 deletions

View file

@ -2629,8 +2629,29 @@ async function doPost() {
}
}
const commentPerm = document.getElementById('comment-perm-select').value;
const reactPerm = document.getElementById('react-perm-select').value;
let result;
if (selectedFiles.length > 0) {
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).
// Attachments + non-default-persona FoF posts are not yet
// supported by the v1 command — fall through to the
// standard path with a warning if either applies.
if (selectedFiles.length > 0 || params.postingIdHex) {
toast('FoF posts with attachments or non-default persona require Mode 1 (later).');
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 => {
const bytes = new Uint8Array(f.data);
@ -2644,9 +2665,9 @@ async function doPost() {
result = await invoke('create_post', params);
}
// Set engagement policy if non-default
const commentPerm = document.getElementById('comment-perm-select').value;
const reactPerm = document.getElementById('react-perm-select').value;
// Set engagement policy if non-default (FoF posts also publish
// the policy diff so receivers route the comment-receive path
// through the FoF four-check verify gate).
if ((commentPerm !== 'public' || reactPerm !== 'both') && result && result.id) {
try {
await invoke('set_comment_policy', {

View file

@ -104,6 +104,7 @@
<select id="comment-perm-select" title="Comment permission">
<option value="public">Comments: All</option>
<option value="followers_only">Comments: Followers</option>
<option value="friends_of_friends">Comments: Friends of Friends</option>
<option value="none">Comments: Off</option>
</select>
<select id="react-perm-select" title="React permission">