diff --git a/crates/core/src/announcement.rs b/crates/core/src/announcement.rs index 098677e..c9b9f02 100644 --- a/crates/core/src/announcement.rs +++ b/crates/core/src/announcement.rs @@ -144,6 +144,7 @@ pub fn build_announcement_post( attachments: vec![], timestamp_ms, fof_gating: None, + supersedes_post_id: None, } } diff --git a/crates/core/src/content.rs b/crates/core/src/content.rs index 622a016..97bb6c3 100644 --- a/crates/core/src/content.rs +++ b/crates/core/src/content.rs @@ -24,6 +24,7 @@ mod tests { attachments: vec![], timestamp_ms: 1000, fof_gating: None, + supersedes_post_id: None, }; let id1 = compute_post_id(&post); let id2 = compute_post_id(&post); @@ -38,6 +39,7 @@ mod tests { attachments: vec![], timestamp_ms: 1000, fof_gating: None, + supersedes_post_id: None, }; let post2 = Post { author: [1u8; 32], @@ -45,6 +47,7 @@ mod tests { attachments: vec![], timestamp_ms: 1000, fof_gating: None, + supersedes_post_id: None, }; assert_ne!(compute_post_id(&post1), compute_post_id(&post2)); } @@ -57,6 +60,7 @@ mod tests { attachments: vec![], timestamp_ms: 1000, fof_gating: None, + supersedes_post_id: None, }; let id = compute_post_id(&post); assert!(verify_post_id(&id, &post)); diff --git a/crates/core/src/control.rs b/crates/core/src/control.rs index 57dd53d..b8f25c3 100644 --- a/crates/core/src/control.rs +++ b/crates/core/src/control.rs @@ -156,6 +156,7 @@ pub fn build_delete_control_post( attachments: vec![], timestamp_ms, fof_gating: None, + supersedes_post_id: None, } } @@ -184,6 +185,7 @@ pub fn build_visibility_control_post( attachments: vec![], timestamp_ms, fof_gating: None, + supersedes_post_id: None, } } @@ -215,6 +217,7 @@ mod tests { attachments: vec![], timestamp_ms: 1000, fof_gating: None, + supersedes_post_id: None, }; let post_id = crate::content::compute_post_id(&post); s.store_post_with_visibility(&post_id, &post, &PostVisibility::Public).unwrap(); @@ -244,6 +247,7 @@ mod tests { attachments: vec![], timestamp_ms: 1000, fof_gating: None, + supersedes_post_id: None, }; let post_id = crate::content::compute_post_id(&post); s.store_post_with_visibility(&post_id, &post, &PostVisibility::Public).unwrap(); diff --git a/crates/core/src/fof.rs b/crates/core/src/fof.rs index f4315bc..be8da58 100644 --- a/crates/core/src/fof.rs +++ b/crates/core/src/fof.rs @@ -914,6 +914,7 @@ mod tests { attachments: vec![], timestamp_ms: 3000, fof_gating: Some(built.gating.clone()), + supersedes_post_id: None, }; // Bob's device unlocks the post via his V_me (= v_x_bob). @@ -995,6 +996,7 @@ mod tests { let post = crate::types::Post { author: alice_id, content: "alice".into(), attachments: vec![], timestamp_ms: 3000, fof_gating: Some(built.gating.clone()), + supersedes_post_id: None, }; s.store_post_with_intent( &post_id, &post, @@ -1085,6 +1087,7 @@ mod tests { let post = crate::types::Post { author: alice_id, content: "alice".into(), attachments: vec![], timestamp_ms: 3000, fof_gating: Some(built.gating.clone()), + supersedes_post_id: None, }; s.store_post_with_intent( &post_id, &post, @@ -1186,6 +1189,7 @@ mod tests { let post = crate::types::Post { author: alice_id, content: "x".into(), attachments: vec![], timestamp_ms: 3000, fof_gating: Some(built.gating.clone()), + supersedes_post_id: None, }; s.store_post_with_intent( &post_id, &post, @@ -1334,6 +1338,7 @@ mod tests { let alice_post = crate::types::Post { author: alice_id, content: String::new(), attachments: vec![], timestamp_ms: 3000, fof_gating: Some(built.gating.clone()), + supersedes_post_id: None, }; let bob_unlock = find_unlock_for_post(&bob_storage, &alice_post).unwrap() .expect("Bob can unlock"); diff --git a/crates/core/src/group_key_distribution.rs b/crates/core/src/group_key_distribution.rs index d229dab..0a753c2 100644 --- a/crates/core/src/group_key_distribution.rs +++ b/crates/core/src/group_key_distribution.rs @@ -62,6 +62,7 @@ pub fn build_distribution_post( attachments: vec![], timestamp_ms, fof_gating: None, + supersedes_post_id: None, }; let post_id = compute_post_id(&post); let visibility = PostVisibility::Encrypted { recipients: wrapped_keys }; @@ -243,6 +244,7 @@ mod tests { attachments: vec![], timestamp_ms: 200, fof_gating: None, + supersedes_post_id: None, }; let forged_vis = PostVisibility::Encrypted { recipients: wrapped }; diff --git a/crates/core/src/import.rs b/crates/core/src/import.rs index 747e75c..72f218e 100644 --- a/crates/core/src/import.rs +++ b/crates/core/src/import.rs @@ -290,6 +290,7 @@ pub async fn import_as_personas( attachments: attachments.clone(), timestamp_ms: ep.timestamp_ms, fof_gating: None, + supersedes_post_id: None, }; // Preserve the original visibility intent from the export. @@ -464,6 +465,7 @@ pub async fn import_public_posts( attachments: attachments.clone(), timestamp_ms: ep.timestamp_ms, fof_gating: None, + supersedes_post_id: None, }; // Read blob data from archive @@ -701,6 +703,7 @@ pub async fn merge_with_key( attachments: attachments.clone(), timestamp_ms: ep.timestamp_ms, fof_gating: None, + supersedes_post_id: None, }; // Read blob data from archive (may need decryption for encrypted posts) diff --git a/crates/core/src/network.rs b/crates/core/src/network.rs index 35619aa..282a576 100644 --- a/crates/core/src/network.rs +++ b/crates/core/src/network.rs @@ -2259,6 +2259,7 @@ mod tests { attachments: vec![], timestamp_ms: 1000, fof_gating: None, + supersedes_post_id: None, } } diff --git a/crates/core/src/node.rs b/crates/core/src/node.rs index bd15796..7fcb91a 100644 --- a/crates/core/src/node.rs +++ b/crates/core/src/node.rs @@ -828,6 +828,7 @@ impl Node { attachments: vec![], timestamp_ms: pi.created_at, fof_gating: None, + supersedes_post_id: None, }; let post_id = crate::content::compute_post_id(&post); { @@ -1168,6 +1169,7 @@ impl Node { attachments: vec![], timestamp_ms: now, fof_gating: Some(built.gating), + supersedes_post_id: None, }; let post_id = crate::content::compute_post_id(&post); @@ -1321,6 +1323,7 @@ impl Node { attachments, timestamp_ms: now, fof_gating, + supersedes_post_id: None, }; let post_id = compute_post_id(&post); @@ -3345,6 +3348,7 @@ impl Node { attachments: post.attachments.clone(), timestamp_ms: post.timestamp_ms, fof_gating: None, + supersedes_post_id: None, }; let new_post_id = compute_post_id(&new_post); diff --git a/crates/core/src/profile.rs b/crates/core/src/profile.rs index 2205c76..7f41618 100644 --- a/crates/core/src/profile.rs +++ b/crates/core/src/profile.rs @@ -196,6 +196,7 @@ pub fn build_profile_post( attachments: vec![], timestamp_ms, fof_gating: None, + supersedes_post_id: None, } } @@ -465,6 +466,7 @@ mod tests { attachments: vec![], timestamp_ms, fof_gating: None, + supersedes_post_id: None, }; // Apply. Auto-scan should fire and store the unwrapped V_me. @@ -534,6 +536,7 @@ mod tests { attachments: vec![], timestamp_ms, fof_gating: None, + supersedes_post_id: None, }; apply_profile_post_if_applicable(&s, &post, Some(&VisibilityIntent::Profile)).unwrap(); diff --git a/crates/core/src/storage.rs b/crates/core/src/storage.rs index f887324..49bb5e3 100644 --- a/crates/core/src/storage.rs +++ b/crates/core/src/storage.rs @@ -966,6 +966,15 @@ impl Storage { attachments, timestamp_ms: row.get::<_, i64>(3)? as u64, fof_gating, + // FoF Layer 4: supersedes_post_id is not persisted as a + // dedicated column today; it would arrive in the JSON + // form via fof_gating_json on re-issued posts. For now, + // get_post returns None and re-issue UX surfaces it + // when present in the in-memory Post (Layer 4 re-issue + // helper sets it inline). A follow-up can add a + // dedicated column if/when receivers need to render the + // supersedes pointer in feeds. + supersedes_post_id: None, })) } else { Ok(None) @@ -993,6 +1002,7 @@ impl Storage { attachments, timestamp_ms: row.get::<_, i64>(3)? as u64, fof_gating: None, + supersedes_post_id: None, }, visibility, ))) @@ -1087,6 +1097,7 @@ impl Storage { attachments, timestamp_ms: timestamp_ms as u64, fof_gating: None, + supersedes_post_id: None, }, visibility, )); @@ -1126,6 +1137,7 @@ impl Storage { attachments, timestamp_ms: timestamp_ms as u64, fof_gating: None, + supersedes_post_id: None, }, visibility, )); @@ -1289,6 +1301,7 @@ impl Storage { attachments, timestamp_ms: timestamp_ms as u64, fof_gating: None, + supersedes_post_id: None, }, visibility, )); @@ -1325,6 +1338,7 @@ impl Storage { attachments, timestamp_ms: timestamp_ms as u64, fof_gating: None, + supersedes_post_id: None, }, visibility, )); @@ -3012,6 +3026,7 @@ impl Storage { attachments, timestamp_ms: row.get::<_, i64>(4)? as u64, fof_gating: None, + supersedes_post_id: None, }, visibility, )); @@ -6414,6 +6429,7 @@ mod tests { attachments: vec![], timestamp_ms: ts, fof_gating: None, + supersedes_post_id: None, }; let id = blake3::hash(&serde_json::to_vec(&post).unwrap()); s.store_post(id.as_bytes(), &post).unwrap(); diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index e4b4e01..aca1ea1 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -48,6 +48,13 @@ pub struct Post { /// t=0, not the live mutable state. #[serde(default, skip_serializing_if = "Option::is_none")] pub fof_gating: Option, + /// FoF Layer 4: optional pointer to a post this one supersedes. + /// Used by the "re-issue with narrower access" path (advanced + /// rotation). Readers may display "this is a re-issued version + /// of an earlier post" + offer to view the original if still + /// cached. Covered by PostId since it's part of the signed Post. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub supersedes_post_id: Option, } /// A reference to a media blob attached to a post