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:
parent
a41b11c0b8
commit
b7f2d369fa
9 changed files with 882 additions and 4 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue