v0.3.5: Encrypted receipt & comment slots, message delivery indicators

Encrypted slots in BlobHeader:
- Private posts get noise-prefilled receipt slots (64B, 1 per participant)
  and comment slots (256B, ceil(participants/3), expandable)
- Slot key derived from post CEK via BLAKE3 — only participants can read
- CDN relays propagate opaque encrypted bytes without decryption
- 3 new BlobHeaderDiffOps: WriteReceiptSlot, WriteCommentSlot, AddCommentSlots

Receipt system:
- States: empty(0), delivered(1), seen(2), reacted(3)
- Slot index = position in sorted participant NodeId list
- Author can pre-feed emoji reaction at creation time
- 6 new crypto tests for slot encrypt/decrypt/derivation

Node methods:
- write_receipt_slot, write_comment_slot with upstream+downstream propagation
- read_receipt_slots, read_comment_slots with CEK-based decryption
- get_post_cek_and_participants helper for both Encrypted and GroupEncrypted

IPC: write_message_receipt, write_message_comment, get_message_receipts,
     get_message_comments

Frontend:
- DM chat bubbles show delivery indicators (check → double → blue → emoji)
- Opening conversation auto-marks incoming messages as seen
- React button on messages with emoji prompt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scott Reimers 2026-03-20 14:15:33 -04:00
parent a41b11c0b8
commit b7f2d369fa
9 changed files with 882 additions and 4 deletions

View file

@ -80,6 +80,26 @@ struct CommentPolicyDto {
blocklist: Vec<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ReceiptSlotDto {
slot_index: u32,
node_id: Option<String>,
state: String,
timestamp_ms: u64,
emoji: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct CommentSlotDto {
slot_index: u32,
author: String,
author_name: Option<String>,
timestamp_ms: u64,
content: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct CircleDto {
@ -1717,6 +1737,72 @@ async fn get_comment_thread(
Ok(dtos)
}
#[tauri::command]
async fn write_message_receipt(
state: State<'_, AppState>,
post_id: String,
receipt_state: String,
emoji: Option<String>,
) -> Result<(), String> {
let node = state.inner();
let pid = hex_to_postid(&post_id)?;
let state_val = itsgoin_core::types::ReceiptState::from_str_label(&receipt_state);
node.write_receipt_slot(pid, state_val, emoji).await.map_err(|e| e.to_string())
}
#[tauri::command]
async fn write_message_comment(
state: State<'_, AppState>,
post_id: String,
content: String,
) -> Result<(), String> {
let node = state.inner();
let pid = hex_to_postid(&post_id)?;
node.write_comment_slot(pid, content).await.map_err(|e| e.to_string())
}
#[tauri::command]
async fn get_message_receipts(
state: State<'_, AppState>,
post_id: String,
) -> Result<Vec<ReceiptSlotDto>, String> {
let node = state.inner();
let pid = hex_to_postid(&post_id)?;
let slots = node.read_receipt_slots(pid).await.map_err(|e| e.to_string())?;
Ok(slots.into_iter().map(|s| ReceiptSlotDto {
slot_index: s.slot_index,
node_id: s.node_id.map(hex::encode),
state: s.state.as_str().to_string(),
timestamp_ms: s.timestamp_ms,
emoji: s.emoji,
}).collect())
}
#[tauri::command]
async fn get_message_comments(
state: State<'_, AppState>,
post_id: String,
) -> Result<Vec<CommentSlotDto>, String> {
let node = state.inner();
let pid = hex_to_postid(&post_id)?;
let slots = node.read_comment_slots(pid).await.map_err(|e| e.to_string())?;
let mut dtos = Vec::new();
for s in slots {
let author_name = match node.resolve_display_name(&s.author).await {
Ok((name, _, _)) if !name.is_empty() => Some(name),
_ => None,
};
dtos.push(CommentSlotDto {
slot_index: s.slot_index,
author: hex::encode(s.author),
author_name,
timestamp_ms: s.timestamp_ms,
content: s.content,
});
}
Ok(dtos)
}
fn hex_to_postid(hex_str: &str) -> Result<itsgoin_core::types::PostId, String> {
let bytes = hex::decode(hex_str).map_err(|e| format!("invalid hex: {}", e))?;
if bytes.len() != 32 {
@ -1874,6 +1960,10 @@ pub fn run() {
set_comment_policy,
get_comment_policy,
get_comment_thread,
write_message_receipt,
write_message_comment,
get_message_receipts,
get_message_comments,
get_setting,
set_setting,
generate_share_link,