v0.0.6: Delivery receipts (sent/delivered/read)

Protocol:
- WireMessage::Receipt { sender_fingerprint, message_id, receipt_type }
- ReceiptType enum: Delivered, Read
- id field added to KeyExchange and Message variants
- Receipts are plaintext (not encrypted) — contain only ID + type

Web client:
- Auto-sends Delivered receipt on successful decrypt
- Tracks sent message IDs with receipt status
- Displays: ✓ (sent, gray), ✓✓ (delivered, white), ✓✓ (read, blue)
- Receipt indicators update live via DOM reference

CLI TUI:
- Auto-sends Delivered receipt back to sender on decrypt
- Tracks receipt status per message ID
- Displays receipt indicators after sent messages

WASM:
- create_receipt() function for web client
- encrypt_with_id/encrypt_key_exchange_with_id for tracking
- decrypt_wire_message handles Receipt variant

17/17 protocol tests pass. Zero warnings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-27 10:12:43 +04:00
parent 8fad8d8374
commit 104ba78b85
8 changed files with 395 additions and 79 deletions

View File

@@ -29,6 +29,7 @@ pub async fn run(server_url: &str, identity: &IdentityKeyPair) -> Result<()> {
for raw in &messages {
match bincode::deserialize::<WireMessage>(raw) {
Ok(WireMessage::KeyExchange {
id: _,
sender_fingerprint,
sender_identity_encryption_key,
ephemeral_public,
@@ -80,6 +81,7 @@ pub async fn run(server_url: &str, identity: &IdentityKeyPair) -> Result<()> {
}
}
Ok(WireMessage::Message {
id: _,
sender_fingerprint,
ratchet_message,
}) => {
@@ -105,6 +107,16 @@ pub async fn run(server_url: &str, identity: &IdentityKeyPair) -> Result<()> {
}
}
}
Ok(WireMessage::Receipt {
sender_fingerprint,
message_id,
receipt_type,
}) => {
println!(
" [receipt] {} acknowledged message {} ({:?})",
sender_fingerprint, message_id, receipt_type
);
}
Err(e) => {
eprintln!(" failed to deserialize message: {}", e);
}

View File

@@ -27,6 +27,7 @@ pub async fn run(recipient_fp: &str, message: &str, server_url: &str, identity:
db.save_session(&recipient, state)?;
WireMessage::Message {
id: uuid::Uuid::new_v4().to_string(),
sender_fingerprint: our_pub.fingerprint.to_string(),
ratchet_message: encrypted,
}
@@ -51,6 +52,7 @@ pub async fn run(recipient_fp: &str, message: &str, server_url: &str, identity:
db.save_session(&recipient, &state)?;
WireMessage::KeyExchange {
id: uuid::Uuid::new_v4().to_string(),
sender_fingerprint: our_pub.fingerprint.to_string(),
sender_identity_encryption_key: *our_pub.encryption.as_bytes(),
ephemeral_public: *x3dh_result.ephemeral_public.as_bytes(),

View File

@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;
@@ -9,15 +10,23 @@ use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap};
use ratatui::Frame;
use warzone_protocol::identity::IdentityKeyPair;
use warzone_protocol::message::{ReceiptType, WireMessage};
use warzone_protocol::ratchet::RatchetState;
use warzone_protocol::types::Fingerprint;
use warzone_protocol::x3dh;
use x25519_dalek::PublicKey;
use warzone_protocol::message::WireMessage;
use crate::net::ServerClient;
use crate::storage::LocalDb;
/// Receipt status for a sent message.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ReceiptStatus {
Sent,
Delivered,
Read,
}
pub struct App {
pub input: String,
pub messages: Arc<Mutex<Vec<ChatLine>>>,
@@ -25,6 +34,8 @@ pub struct App {
pub peer_fp: Option<String>,
pub server_url: String,
pub should_quit: bool,
/// Track receipt status for messages we sent, keyed by message ID.
pub receipts: Arc<Mutex<HashMap<String, ReceiptStatus>>>,
}
#[derive(Clone)]
@@ -33,6 +44,8 @@ pub struct ChatLine {
pub text: String,
pub is_system: bool,
pub is_self: bool,
/// Message ID (for sent messages, used to track receipts).
pub message_id: Option<String>,
}
impl App {
@@ -42,6 +55,7 @@ impl App {
text: format!("You are {}", our_fp),
is_system: true,
is_self: false,
message_id: None,
}]));
if let Some(ref peer) = peer_fp {
@@ -50,6 +64,7 @@ impl App {
text: format!("Chatting with {}", peer),
is_system: true,
is_self: false,
message_id: None,
});
} else {
messages.lock().unwrap().push(ChatLine {
@@ -57,6 +72,7 @@ impl App {
text: "No peer set. Use /peer <fp>, /peer @alias, or /g <group>".into(),
is_system: true,
is_self: false,
message_id: None,
});
}
@@ -65,6 +81,7 @@ impl App {
text: "Commands: /alias <name>, /peer <fp|@alias>, /g <group>, /info, /quit".into(),
is_system: true,
is_self: false,
message_id: None,
});
App {
@@ -74,6 +91,7 @@ impl App {
peer_fp,
server_url,
should_quit: false,
receipts: Arc::new(Mutex::new(HashMap::new())),
}
}
@@ -81,6 +99,34 @@ impl App {
self.messages.lock().unwrap().push(line);
}
fn receipt_indicator(&self, message_id: &Option<String>) -> &'static str {
match message_id {
Some(id) => {
let receipts = self.receipts.lock().unwrap();
match receipts.get(id) {
Some(ReceiptStatus::Read) => " \u{2713}\u{2713}", // ✓✓ (read)
Some(ReceiptStatus::Delivered) => " \u{2713}\u{2713}", // ✓✓ (delivered)
Some(ReceiptStatus::Sent) | None => " \u{2713}", // ✓ (sent)
}
}
None => "",
}
}
fn receipt_color(&self, message_id: &Option<String>) -> Color {
match message_id {
Some(id) => {
let receipts = self.receipts.lock().unwrap();
match receipts.get(id) {
Some(ReceiptStatus::Read) => Color::Blue,
Some(ReceiptStatus::Delivered) => Color::White,
Some(ReceiptStatus::Sent) | None => Color::DarkGray,
}
}
None => Color::DarkGray,
}
}
pub fn draw(&self, frame: &mut Frame) {
let chunks = Layout::default()
.direction(Direction::Vertical)
@@ -127,9 +173,17 @@ impl App {
format!("{}: ", &m.sender[..m.sender.len().min(12)])
};
let receipt_str = if m.is_self && m.message_id.is_some() {
self.receipt_indicator(&m.message_id)
} else {
""
};
let receipt_color = self.receipt_color(&m.message_id);
ListItem::new(Line::from(vec![
Span::styled(prefix, style.add_modifier(Modifier::BOLD)),
Span::raw(&m.text),
Span::styled(receipt_str, Style::default().fg(receipt_color)),
]))
})
.collect();
@@ -178,6 +232,7 @@ impl App {
text: format!("Your fingerprint: {}", self.our_fp),
is_system: true,
is_self: false,
message_id: None,
});
return;
}
@@ -205,6 +260,7 @@ impl App {
text: format!("Peer set to {}", fp),
is_system: true,
is_self: false,
message_id: None,
});
self.peer_fp = Some(fp);
return;
@@ -228,6 +284,7 @@ impl App {
text: format!("Switched to group #{}", name),
is_system: true,
is_self: false,
message_id: None,
});
self.peer_fp = Some(format!("#{}", name));
return;
@@ -238,6 +295,7 @@ impl App {
text: "Switched to DM mode. Use /peer <fp>".into(),
is_system: true,
is_self: false,
message_id: None,
});
self.peer_fp = None;
return;
@@ -262,6 +320,7 @@ impl App {
text: "No peer set. Use /peer <fingerprint>".into(),
is_system: true,
is_self: false,
message_id: None,
});
return;
}
@@ -275,11 +334,13 @@ impl App {
text: "Invalid peer fingerprint".into(),
is_system: true,
is_self: false,
message_id: None,
});
return;
}
};
let msg_id = uuid::Uuid::new_v4().to_string();
let our_pub = identity.public_identity();
let mut ratchet = db.load_session(&peer_fp).ok().flatten();
@@ -288,6 +349,7 @@ impl App {
Ok(encrypted) => {
let _ = db.save_session(&peer_fp, state);
WireMessage::Message {
id: msg_id.clone(),
sender_fingerprint: our_pub.fingerprint.to_string(),
ratchet_message: encrypted,
}
@@ -298,6 +360,7 @@ impl App {
text: format!("Encrypt failed: {}", e),
is_system: true,
is_self: false,
message_id: None,
});
return;
}
@@ -312,6 +375,7 @@ impl App {
text: format!("Failed to fetch bundle: {}", e),
is_system: true,
is_self: false,
message_id: None,
});
return;
}
@@ -325,6 +389,7 @@ impl App {
text: format!("X3DH failed: {}", e),
is_system: true,
is_self: false,
message_id: None,
});
return;
}
@@ -337,6 +402,7 @@ impl App {
Ok(encrypted) => {
let _ = db.save_session(&peer_fp, &state);
WireMessage::KeyExchange {
id: msg_id.clone(),
sender_fingerprint: our_pub.fingerprint.to_string(),
sender_identity_encryption_key: *our_pub.encryption.as_bytes(),
ephemeral_public: *x3dh_result.ephemeral_public.as_bytes(),
@@ -350,6 +416,7 @@ impl App {
text: format!("Encrypt failed: {}", e),
is_system: true,
is_self: false,
message_id: None,
});
return;
}
@@ -364,6 +431,7 @@ impl App {
text: format!("Serialize failed: {}", e),
is_system: true,
is_self: false,
message_id: None,
});
return;
}
@@ -371,11 +439,14 @@ impl App {
match client.send_message(&peer, Some(&self.our_fp), &encoded).await {
Ok(_) => {
// Track receipt status
self.receipts.lock().unwrap().insert(msg_id.clone(), ReceiptStatus::Sent);
self.add_message(ChatLine {
sender: self.our_fp[..12].to_string(),
text: text.clone(),
is_system: false,
is_self: true,
message_id: Some(msg_id),
});
}
Err(e) => {
@@ -384,6 +455,7 @@ impl App {
text: format!("Send failed: {}", e),
is_system: true,
is_self: false,
message_id: None,
});
}
}
@@ -398,13 +470,13 @@ impl App {
Ok(resp) => {
if let Ok(data) = resp.json::<serde_json::Value>().await {
if let Some(err) = data.get("error") {
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", err), is_system: true, is_self: false });
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", err), is_system: true, is_self: false, message_id: None });
} else {
self.add_message(ChatLine { sender: "system".into(), text: format!("Group '{}' created", name), is_system: true, is_self: false });
self.add_message(ChatLine { sender: "system".into(), text: format!("Group '{}' created", name), is_system: true, is_self: false, message_id: None });
}
}
}
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None }),
}
}
@@ -417,14 +489,14 @@ impl App {
Ok(resp) => {
if let Ok(data) = resp.json::<serde_json::Value>().await {
if let Some(err) = data.get("error") {
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", err), is_system: true, is_self: false });
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", err), is_system: true, is_self: false, message_id: None });
} else {
let members = data.get("members").and_then(|v| v.as_u64()).unwrap_or(0);
self.add_message(ChatLine { sender: "system".into(), text: format!("Joined '{}' ({} members)", name, members), is_system: true, is_self: false });
self.add_message(ChatLine { sender: "system".into(), text: format!("Joined '{}' ({} members)", name, members), is_system: true, is_self: false, message_id: None });
}
}
}
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None }),
}
}
@@ -435,18 +507,18 @@ impl App {
if let Ok(data) = resp.json::<serde_json::Value>().await {
if let Some(groups) = data.get("groups").and_then(|v| v.as_array()) {
if groups.is_empty() {
self.add_message(ChatLine { sender: "system".into(), text: "No groups".into(), is_system: true, is_self: false });
self.add_message(ChatLine { sender: "system".into(), text: "No groups".into(), is_system: true, is_self: false, message_id: None });
} else {
for g in groups {
let name = g.get("name").and_then(|v| v.as_str()).unwrap_or("?");
let members = g.get("members").and_then(|v| v.as_u64()).unwrap_or(0);
self.add_message(ChatLine { sender: "system".into(), text: format!(" #{} ({} members)", name, members), is_system: true, is_self: false });
self.add_message(ChatLine { sender: "system".into(), text: format!(" #{} ({} members)", name, members), is_system: true, is_self: false, message_id: None });
}
}
}
}
}
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None }),
}
}
@@ -463,9 +535,9 @@ impl App {
let group_data = match client.client.get(&url).send().await {
Ok(resp) => match resp.json::<serde_json::Value>().await {
Ok(d) => d,
Err(e) => { self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false }); return; }
Err(e) => { self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None }); return; }
},
Err(e) => { self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false }); return; }
Err(e) => { self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None }); return; }
};
let my_fp = normfp(&self.our_fp);
@@ -491,6 +563,7 @@ impl App {
Ok(encrypted) => {
let _ = db.save_session(&member_fp, state);
WireMessage::Message {
id: uuid::Uuid::new_v4().to_string(),
sender_fingerprint: our_pub.fingerprint.to_string(),
ratchet_message: encrypted,
}
@@ -513,6 +586,7 @@ impl App {
Ok(encrypted) => {
let _ = db.save_session(&member_fp, &state);
WireMessage::KeyExchange {
id: uuid::Uuid::new_v4().to_string(),
sender_fingerprint: our_pub.fingerprint.to_string(),
sender_identity_encryption_key: *our_pub.encryption.as_bytes(),
ephemeral_public: *x3dh_result.ephemeral_public.as_bytes(),
@@ -536,7 +610,7 @@ impl App {
}
if wire_messages.is_empty() {
self.add_message(ChatLine { sender: "system".into(), text: "No members to send to".into(), is_system: true, is_self: false });
self.add_message(ChatLine { sender: "system".into(), text: "No members to send to".into(), is_system: true, is_self: false, message_id: None });
return;
}
@@ -554,10 +628,11 @@ impl App {
text: text.to_string(),
is_system: false,
is_self: true,
message_id: None,
});
}
Err(e) => {
self.add_message(ChatLine { sender: "system".into(), text: format!("Send failed: {}", e), is_system: true, is_self: false });
self.add_message(ChatLine { sender: "system".into(), text: format!("Send failed: {}", e), is_system: true, is_self: false, message_id: None });
}
}
}
@@ -571,14 +646,14 @@ impl App {
Ok(resp) => {
if let Ok(data) = resp.json::<serde_json::Value>().await {
if let Some(err) = data.get("error") {
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", err), is_system: true, is_self: false });
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", err), is_system: true, is_self: false, message_id: None });
} else {
let alias = data.get("alias").and_then(|v| v.as_str()).unwrap_or(name);
self.add_message(ChatLine { sender: "system".into(), text: format!("Alias @{} registered", alias), is_system: true, is_self: false });
self.add_message(ChatLine { sender: "system".into(), text: format!("Alias @{} registered", alias), is_system: true, is_self: false, message_id: None });
}
}
}
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None }),
}
}
@@ -588,17 +663,17 @@ impl App {
Ok(resp) => {
if let Ok(data) = resp.json::<serde_json::Value>().await {
if let Some(fp) = data.get("fingerprint").and_then(|v| v.as_str()) {
self.add_message(ChatLine { sender: "system".into(), text: format!("@{} → {}", name, fp), is_system: true, is_self: false });
self.add_message(ChatLine { sender: "system".into(), text: format!("@{} → {}", name, fp), is_system: true, is_self: false, message_id: None });
return Some(fp.to_string());
}
if let Some(err) = data.get("error") {
self.add_message(ChatLine { sender: "system".into(), text: format!("Unknown alias @{}: {}", name, err), is_system: true, is_self: false });
self.add_message(ChatLine { sender: "system".into(), text: format!("Unknown alias @{}: {}", name, err), is_system: true, is_self: false, message_id: None });
}
}
None
}
Err(e) => {
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false });
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None });
None
}
}
@@ -611,18 +686,18 @@ impl App {
if let Ok(data) = resp.json::<serde_json::Value>().await {
if let Some(aliases) = data.get("aliases").and_then(|v| v.as_array()) {
if aliases.is_empty() {
self.add_message(ChatLine { sender: "system".into(), text: "No aliases".into(), is_system: true, is_self: false });
self.add_message(ChatLine { sender: "system".into(), text: "No aliases".into(), is_system: true, is_self: false, message_id: None });
} else {
for a in aliases {
let name = a.get("alias").and_then(|v| v.as_str()).unwrap_or("?");
let fp = a.get("fingerprint").and_then(|v| v.as_str()).unwrap_or("?");
self.add_message(ChatLine { sender: "system".into(), text: format!(" @{} → {}...", name, &fp[..fp.len().min(16)]), is_system: true, is_self: false });
self.add_message(ChatLine { sender: "system".into(), text: format!(" @{} → {}...", name, &fp[..fp.len().min(16)]), is_system: true, is_self: false, message_id: None });
}
}
}
}
}
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None }),
}
}
}
@@ -631,15 +706,43 @@ fn normfp(fp: &str) -> String {
fp.chars().filter(|c| c.is_ascii_hexdigit()).collect::<String>().to_lowercase()
}
/// Send a delivery receipt for a message back to its sender.
fn send_receipt(
our_fp: &str,
sender_fp: &str,
message_id: &str,
receipt_type: ReceiptType,
client: &ServerClient,
) {
let receipt = WireMessage::Receipt {
sender_fingerprint: our_fp.to_string(),
message_id: message_id.to_string(),
receipt_type,
};
let encoded = match bincode::serialize(&receipt) {
Ok(e) => e,
Err(_) => return,
};
let client = client.clone();
let to = sender_fp.to_string();
let from = our_fp.to_string();
tokio::spawn(async move {
let _ = client.send_message(&to, Some(&from), &encoded).await;
});
}
/// Process a single incoming raw message (shared by WS and HTTP paths).
fn process_incoming(
raw: &[u8],
identity: &IdentityKeyPair,
db: &LocalDb,
messages: &Arc<Mutex<Vec<ChatLine>>>,
receipts: &Arc<Mutex<HashMap<String, ReceiptStatus>>>,
our_fp: &str,
client: &ServerClient,
) {
match bincode::deserialize::<WireMessage>(raw) {
Ok(wire) => process_wire_message(wire, identity, db, messages),
Ok(wire) => process_wire_message(wire, identity, db, messages, receipts, our_fp, client),
Err(_) => {}
}
}
@@ -649,9 +752,13 @@ fn process_wire_message(
identity: &IdentityKeyPair,
db: &LocalDb,
messages: &Arc<Mutex<Vec<ChatLine>>>,
receipts: &Arc<Mutex<HashMap<String, ReceiptStatus>>>,
our_fp: &str,
client: &ServerClient,
) {
match wire {
WireMessage::KeyExchange {
id,
sender_fingerprint,
sender_identity_encryption_key,
ephemeral_public,
@@ -666,8 +773,8 @@ fn process_wire_message(
Ok(Some(s)) => s,
_ => return,
};
let otpk_secret = if let Some(id) = used_one_time_pre_key_id {
db.take_one_time_pre_key(id).ok().flatten()
let otpk_secret = if let Some(otpk_id) = used_one_time_pre_key_id {
db.take_one_time_pre_key(otpk_id).ok().flatten()
} else {
None
};
@@ -689,12 +796,16 @@ fn process_wire_message(
text,
is_system: false,
is_self: false,
message_id: None,
});
// Send delivery receipt
send_receipt(our_fp, &sender_fingerprint, &id, ReceiptType::Delivered, client);
}
Err(_) => {}
}
}
WireMessage::Message {
id,
sender_fingerprint,
ratchet_message,
} => {
@@ -715,17 +826,43 @@ fn process_wire_message(
text,
is_system: false,
is_self: false,
message_id: None,
});
// Send delivery receipt
send_receipt(our_fp, &sender_fingerprint, &id, ReceiptType::Delivered, client);
}
Err(_) => {}
}
}
WireMessage::Receipt {
sender_fingerprint: _,
message_id,
receipt_type,
} => {
// Update receipt status for the referenced message
let mut r = receipts.lock().unwrap();
let current = r.get(&message_id);
let should_update = match (&receipt_type, current) {
(ReceiptType::Read, _) => true,
(ReceiptType::Delivered, Some(ReceiptStatus::Sent)) => true,
(ReceiptType::Delivered, None) => true,
_ => false,
};
if should_update {
let new_status = match receipt_type {
ReceiptType::Delivered => ReceiptStatus::Delivered,
ReceiptType::Read => ReceiptStatus::Read,
};
r.insert(message_id, new_status);
}
}
}
}
/// Real-time message loop via WebSocket (falls back to HTTP polling).
pub async fn poll_loop(
messages: Arc<Mutex<Vec<ChatLine>>>,
receipts: Arc<Mutex<HashMap<String, ReceiptStatus>>>,
our_fp: String,
identity: IdentityKeyPair,
db: Arc<LocalDb>,
@@ -747,6 +884,7 @@ pub async fn poll_loop(
text: "Real-time connection established".into(),
is_system: true,
is_self: false,
message_id: None,
});
use futures_util::StreamExt;
@@ -754,7 +892,7 @@ pub async fn poll_loop(
while let Some(Ok(msg)) = read.next().await {
if let tokio_tungstenite::tungstenite::Message::Binary(data) = msg {
process_incoming(&data, &identity, &db, &messages);
process_incoming(&data, &identity, &db, &messages, &receipts, &our_fp, &client);
}
}
@@ -763,6 +901,7 @@ pub async fn poll_loop(
text: "Connection lost, reconnecting...".into(),
is_system: true,
is_self: false,
message_id: None,
});
tokio::time::sleep(Duration::from_secs(3)).await;
}
@@ -774,7 +913,7 @@ pub async fn poll_loop(
Err(_) => continue,
};
for raw in &raw_msgs {
process_incoming(raw, &identity, &db, &messages);
process_incoming(raw, &identity, &db, &messages, &receipts, &our_fp, &client);
}
}
}
@@ -799,12 +938,13 @@ pub async fn run_tui(
// Derive a second identity for the poll loop (can't clone IdentityKeyPair)
let poll_identity = poll_seed.derive_identity();
let poll_messages = app.messages.clone();
let poll_receipts = app.receipts.clone();
let poll_client = client.clone();
let poll_db = db.clone();
let poll_fp = our_fp.clone();
tokio::spawn(async move {
poll_loop(poll_messages, poll_fp, poll_identity, poll_db, poll_client).await;
poll_loop(poll_messages, poll_receipts, poll_fp, poll_identity, poll_db, poll_client).await;
});
loop {