v0.0.41: FC-P6-T2 — read receipts when messages are visible

- ChatLine gains sender_fp field for tracking who sent each message
- App gains read_receipts_sent HashSet to avoid duplicate receipts
- After each draw(), visible received messages get a Read receipt sent
- Only fires once per message_id, skips system/self messages
- Sender sees blue ✓✓ (existing display logic already handles Read)
- All ChatLine literals across 6 files updated with sender_fp field

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-29 18:50:47 +04:00
parent 1295f1c937
commit 5bc59376f5
10 changed files with 208 additions and 154 deletions

10
warzone/Cargo.lock generated
View File

@@ -2956,7 +2956,7 @@ dependencies = [
[[package]]
name = "warzone-client"
version = "0.0.39"
version = "0.0.40"
dependencies = [
"anyhow",
"argon2",
@@ -2989,7 +2989,7 @@ dependencies = [
[[package]]
name = "warzone-mule"
version = "0.0.39"
version = "0.0.40"
dependencies = [
"anyhow",
"clap",
@@ -2998,7 +2998,7 @@ dependencies = [
[[package]]
name = "warzone-protocol"
version = "0.0.39"
version = "0.0.40"
dependencies = [
"base64",
"bincode",
@@ -3023,7 +3023,7 @@ dependencies = [
[[package]]
name = "warzone-server"
version = "0.0.39"
version = "0.0.40"
dependencies = [
"anyhow",
"axum",
@@ -3054,7 +3054,7 @@ dependencies = [
[[package]]
name = "warzone-wasm"
version = "0.0.39"
version = "0.0.40"
dependencies = [
"base64",
"bincode",

View File

@@ -9,7 +9,7 @@ members = [
]
[workspace.package]
version = "0.0.40"
version = "0.0.41"
edition = "2021"
license = "MIT"
rust-version = "1.75"

View File

@@ -35,9 +35,9 @@ impl App {
}
if text == "/info" {
if !self.our_eth.is_empty() {
self.add_message(ChatLine { sender: "system".into(), text: format!("Identity: {}", self.our_eth), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Identity: {}", self.our_eth), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
self.add_message(ChatLine { sender: "system".into(), text: format!("Fingerprint: {}", self.our_fp), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Fingerprint: {}", self.our_fp), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
return;
}
if text == "/help" || text == "/?" {
@@ -87,7 +87,7 @@ impl App {
text: line.to_string(),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
}
return;
@@ -109,12 +109,12 @@ 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, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", err), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
} else {
self.add_message(ChatLine { sender: "system".into(), text: "Alias removed".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "Alias removed".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
},
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() }),
}
return;
}
@@ -122,9 +122,9 @@ impl App {
match db.list_contacts() {
Ok(contacts) => {
if contacts.is_empty() {
self.add_message(ChatLine { sender: "system".into(), text: "No contacts yet".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "No contacts yet".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
} else {
self.add_message(ChatLine { sender: "system".into(), text: format!("Contacts ({}):", contacts.len()), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Contacts ({}):", contacts.len()), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
for c in &contacts {
let fp = c.get("fingerprint").and_then(|v| v.as_str()).unwrap_or("?");
let alias = c.get("alias").and_then(|v| v.as_str());
@@ -141,11 +141,11 @@ impl App {
Some(a) => format!(" {} @{} ({}) — {} msgs", status, a, &fp[..fp.len().min(12)], count),
None => format!(" {} {}{} msgs", status, &fp[..fp.len().min(16)], count),
};
self.add_message(ChatLine { sender: "system".into(), text: label, is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: label, is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
}
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() }),
}
return;
}
@@ -153,14 +153,14 @@ impl App {
let peer = if text.starts_with("/h ") { text[3..].trim() } else if text.starts_with("/history ") { text[9..].trim() } else { "" };
let fp = if let Some(ref p) = self.peer_fp { if !p.starts_with('#') { p.as_str() } else { peer } } else { peer };
if fp.is_empty() {
self.add_message(ChatLine { sender: "system".into(), text: "Usage: /history or /h <fingerprint> (or set peer first)".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "Usage: /history or /h <fingerprint> (or set peer first)".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
} else {
match db.get_history(fp, 50) {
Ok(msgs) => {
if msgs.is_empty() {
self.add_message(ChatLine { sender: "system".into(), text: "No history with this peer".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "No history with this peer".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
} else {
self.add_message(ChatLine { sender: "system".into(), text: format!("History ({} messages):", msgs.len()), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("History ({} messages):", msgs.len()), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
for m in &msgs {
let sender = m.get("sender").and_then(|v| v.as_str()).unwrap_or("?");
let txt = m.get("text").and_then(|v| v.as_str()).unwrap_or("");
@@ -170,12 +170,12 @@ impl App {
text: txt.to_string(),
is_system: false,
is_self,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
}
}
}
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() }),
}
}
return;
@@ -184,17 +184,17 @@ impl App {
// Show ethereum address from seed
if let Ok(seed) = crate::keystore::load_seed_raw() {
let eth = warzone_protocol::ethereum::derive_eth_identity(&seed);
self.add_message(ChatLine { sender: "system".into(), text: format!("ETH: {}", eth.address.to_checksum()), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("ETH: {}", eth.address.to_checksum()), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
return;
}
if text == "/seed" {
if let Ok(seed) = crate::keystore::load_seed_raw() {
let mnemonic = warzone_protocol::identity::Seed::from_bytes(seed).to_mnemonic();
self.add_message(ChatLine { sender: "system".into(), text: "Your recovery seed (keep secret!):".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: mnemonic, is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "Your recovery seed (keep secret!):".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: mnemonic, is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
} else {
self.add_message(ChatLine { sender: "system".into(), text: "Failed to load seed".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "Failed to load seed".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
return;
}
@@ -202,10 +202,10 @@ impl App {
if let Ok(seed) = crate::keystore::load_seed_raw() {
match db.create_backup(&seed) {
Ok(path) => {
self.add_message(ChatLine { sender: "system".into(), text: format!("Backup saved: {}", path.display()), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Backup saved: {}", path.display()), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
Err(e) => {
self.add_message(ChatLine { sender: "system".into(), text: format!("Backup failed: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Backup failed: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
}
@@ -224,9 +224,9 @@ impl App {
match warzone_protocol::friends::FriendList::decrypt(&seed, &blob) {
Ok(list) => {
if list.friends.is_empty() {
self.add_message(ChatLine { sender: "system".into(), text: "No friends yet. Use /friend <address> to add.".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "No friends yet. Use /friend <address> to add.".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
} else {
self.add_message(ChatLine { sender: "system".into(), text: format!("Friends ({}):", list.friends.len()), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Friends ({}):", list.friends.len()), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
for f in &list.friends {
// Check presence
let presence_url = format!("{}/v1/presence/{}", client.base_url, normfp(&f.address));
@@ -241,28 +241,28 @@ impl App {
Some(a) => format!(" @{} ({}) — {}", a, &f.address[..f.address.len().min(16)], status),
None => format!(" {}{}", &f.address[..f.address.len().min(16)], status),
};
self.add_message(ChatLine { sender: "system".into(), text: label, is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: label, is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
}
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Failed to decrypt friend list: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Failed to decrypt friend list: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() }),
}
}
}
_ => {
self.add_message(ChatLine { sender: "system".into(), text: "No friends yet. Use /friend <address> to add.".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "No friends yet. Use /friend <address> to add.".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
}
}
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() }),
}
return;
}
if text.starts_with("/friend ") {
let addr = text[8..].trim().to_string();
if addr.is_empty() {
self.add_message(ChatLine { sender: "system".into(), text: "Usage: /friend <address>".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "Usage: /friend <address>".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
return;
}
if let Ok(seed) = crate::keystore::load_seed_raw() {
@@ -287,14 +287,14 @@ impl App {
let encrypted = list.encrypt(&seed);
let blob_b64 = base64::engine::general_purpose::STANDARD.encode(&encrypted);
let _ = client.client.post(&url).json(&serde_json::json!({"data": blob_b64})).send().await;
self.add_message(ChatLine { sender: "system".into(), text: format!("Added {} to friends", addr), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Added {} to friends", addr), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
return;
}
if text.starts_with("/unfriend ") {
let addr = text[10..].trim().to_string();
if addr.is_empty() {
self.add_message(ChatLine { sender: "system".into(), text: "Usage: /unfriend <address>".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "Usage: /unfriend <address>".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
return;
}
if let Ok(seed) = crate::keystore::load_seed_raw() {
@@ -318,7 +318,7 @@ impl App {
let encrypted = list.encrypt(&seed);
let blob_b64 = base64::engine::general_purpose::STANDARD.encode(&encrypted);
let _ = client.client.post(&url).json(&serde_json::json!({"data": blob_b64})).send().await;
self.add_message(ChatLine { sender: "system".into(), text: format!("Removed {} from friends", addr), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Removed {} from friends", addr), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
return;
}
@@ -330,31 +330,31 @@ impl App {
if let Ok(data) = resp.json::<serde_json::Value>().await {
if let Some(devices) = data.get("devices").and_then(|v| v.as_array()) {
if devices.is_empty() {
self.add_message(ChatLine { sender: "system".into(), text: "No active devices".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "No active devices".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
} else {
self.add_message(ChatLine { sender: "system".into(), text: format!("Active devices ({}):", devices.len()), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Active devices ({}):", devices.len()), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
for d in devices {
let id = d.get("device_id").and_then(|v| v.as_str()).unwrap_or("?");
let connected = d.get("connected_at").and_then(|v| v.as_i64()).unwrap_or(0);
let when = chrono::DateTime::from_timestamp(connected, 0)
.map(|dt| dt.format("%Y-%m-%d %H:%M").to_string())
.unwrap_or_else(|| "?".to_string());
self.add_message(ChatLine { sender: "system".into(), text: format!(" {} — connected {}", id, when), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!(" {} — connected {}", id, when), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
} else if let Some(err) = data.get("error") {
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", err), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", err), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
}
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() }),
}
return;
}
if text.starts_with("/kick ") {
let device_id = text[6..].trim();
if device_id.is_empty() {
self.add_message(ChatLine { sender: "system".into(), text: "Usage: /kick <device_id>".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "Usage: /kick <device_id>".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
return;
}
let url = format!("{}/v1/devices/{}/kick", client.base_url, device_id);
@@ -362,13 +362,13 @@ impl App {
Ok(resp) => {
if let Ok(data) = resp.json::<serde_json::Value>().await {
if let Some(kicked) = data.get("kicked").and_then(|v| v.as_str()) {
self.add_message(ChatLine { sender: "system".into(), text: format!("Device {} kicked", kicked), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Device {} kicked", kicked), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
} else if let Some(err) = data.get("error") {
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", err), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", err), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
}
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() }),
}
return;
}
@@ -376,7 +376,7 @@ impl App {
let last = self.last_dm_peer.lock().unwrap().clone();
if let Some(ref peer) = last {
self.peer_fp = Some(peer.clone());
self.add_message(ChatLine { sender: "system".into(), text: format!("→ switched to {}", &peer[..peer.len().min(16)]), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("→ switched to {}", &peer[..peer.len().min(16)]), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
// If there's a message after /r, mutate text and fall through to send
let reply_msg = if text.starts_with("/reply ") {
text[7..].trim().to_string()
@@ -390,7 +390,7 @@ impl App {
}
text = reply_msg; // Fall through to send logic below
} else {
self.add_message(ChatLine { sender: "system".into(), text: "No one to reply to".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "No one to reply to".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
return;
}
}
@@ -414,7 +414,7 @@ impl App {
raw
};
if normfp(&fp) == normfp(&self.our_fp) {
self.add_message(ChatLine { sender: "system".into(), text: "Cannot set yourself as peer".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "Cannot set yourself as peer".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
return;
}
// Resolve peer ETH for display
@@ -435,7 +435,7 @@ impl App {
text: format!("Peer set to {}", display),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
self.peer_fp = Some(fp);
return;
@@ -459,7 +459,7 @@ impl App {
text: format!("Switched to group #{}", name),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
self.peer_fp = Some(format!("#{}", name));
return;
@@ -470,7 +470,7 @@ impl App {
text: "Switched to DM mode. Use /peer <fp>".into(),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
self.peer_fp = None;
return;
@@ -486,7 +486,7 @@ impl App {
self.group_leave(&name, client).await;
self.peer_fp = None;
} else {
self.add_message(ChatLine { sender: "system".into(), text: "Not in a group. Use /g <name> first".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "Not in a group. Use /g <name> first".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
return;
@@ -498,7 +498,7 @@ impl App {
let target = text[7..].trim().to_string();
self.group_kick(&name, &target, client).await;
} else {
self.add_message(ChatLine { sender: "system".into(), text: "Not in a group".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "Not in a group".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
return;
@@ -509,7 +509,7 @@ impl App {
let name = peer[1..].to_string();
self.group_members(&name, client).await;
} else {
self.add_message(ChatLine { sender: "system".into(), text: "Not in a group".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "Not in a group".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
return;
@@ -540,7 +540,7 @@ impl App {
let peer = match peer {
Some(p) if !p.starts_with('#') => p,
_ => {
self.add_message(ChatLine { sender: "system".into(), text: "No peer to call. Use /call <fp> or set a peer first.".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "No peer to call. Use /call <fp> or set a peer first.".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
return;
}
};
@@ -559,7 +559,7 @@ impl App {
let encoded = match bincode::serialize(&wire) {
Ok(e) => e,
Err(e) => {
self.add_message(ChatLine { sender: "system".into(), text: format!("Call failed: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Call failed: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
return;
}
};
@@ -570,7 +570,7 @@ impl App {
.or(Some(&peer))
.map(|s| if s.len() > 16 { format!("{}...", &s[..16]) } else { s.to_string() })
.unwrap_or_default();
self.add_message(ChatLine { sender: "system".into(), text: format!("📞 Calling {}...", display), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("📞 Calling {}...", display), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
self.call_state = Some(super::types::CallInfo {
peer_fp: peer_fp_clean.clone(),
peer_display: display.clone(),
@@ -579,7 +579,7 @@ impl App {
});
}
Err(e) => {
self.add_message(ChatLine { sender: "system".into(), text: format!("Call failed: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Call failed: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
return;
@@ -589,7 +589,7 @@ impl App {
let peer = match self.last_dm_peer.lock().unwrap().clone() {
Some(p) => p,
None => {
self.add_message(ChatLine { sender: "system".into(), text: "No incoming call to accept".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "No incoming call to accept".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
return;
}
};
@@ -605,7 +605,7 @@ impl App {
};
if let Ok(encoded) = bincode::serialize(&wire) {
let _ = client.send_message(&normfp(&peer), Some(&self.our_fp), &encoded).await;
self.add_message(ChatLine { sender: "system".into(), text: "✓ Call accepted".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "✓ Call accepted".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
self.call_state = Some(super::types::CallInfo {
peer_fp: normfp(&peer),
peer_display: peer[..peer.len().min(16)].to_string(),
@@ -620,7 +620,7 @@ impl App {
let peer = match self.last_dm_peer.lock().unwrap().clone() {
Some(p) => p,
None => {
self.add_message(ChatLine { sender: "system".into(), text: "No incoming call to reject".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "No incoming call to reject".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
return;
}
};
@@ -636,7 +636,7 @@ impl App {
};
if let Ok(encoded) = bincode::serialize(&wire) {
let _ = client.send_message(&normfp(&peer), Some(&self.our_fp), &encoded).await;
self.add_message(ChatLine { sender: "system".into(), text: "✗ Call rejected".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "✗ Call rejected".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
self.call_state = None;
}
return;
@@ -647,7 +647,7 @@ impl App {
let peer = match peer {
Some(p) if !p.starts_with('#') => p,
_ => {
self.add_message(ChatLine { sender: "system".into(), text: "No active call".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "No active call".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
return;
}
};
@@ -663,7 +663,7 @@ impl App {
};
if let Ok(encoded) = bincode::serialize(&wire) {
let _ = client.send_message(&normfp(&peer), Some(&self.our_fp), &encoded).await;
self.add_message(ChatLine { sender: "system".into(), text: "Call ended".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "Call ended".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
self.call_state = None;
}
return;
@@ -690,7 +690,7 @@ impl App {
text: "No peer set. Use /peer <fingerprint>".into(),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -703,7 +703,7 @@ impl App {
text: "Cannot send messages to yourself".into(),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -716,7 +716,7 @@ impl App {
text: "Invalid peer fingerprint".into(),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -754,11 +754,11 @@ impl App {
text: text.clone(),
is_system: false,
is_self: true,
message_id: Some(msg_id), timestamp: Local::now(),
message_id: Some(msg_id), sender_fp: None, timestamp: Local::now(),
});
}
Err(e) => {
self.add_message(ChatLine { sender: "system".into(), text: format!("Send failed: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Send failed: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
return;
@@ -784,7 +784,7 @@ impl App {
text: format!("Encrypt failed: {}", e),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -799,7 +799,7 @@ impl App {
text: format!("Failed to fetch bundle: {}", e),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -813,7 +813,7 @@ impl App {
text: format!("X3DH failed: {}", e),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -840,7 +840,7 @@ impl App {
text: format!("Encrypt failed: {}", e),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -855,7 +855,7 @@ impl App {
text: format!("Serialize failed: {}", e),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -873,7 +873,7 @@ impl App {
text: text.clone(),
is_system: false,
is_self: true,
message_id: Some(msg_id), timestamp: Local::now(),
message_id: Some(msg_id), sender_fp: None, timestamp: Local::now(),
});
}
Err(e) => {
@@ -882,7 +882,7 @@ impl App {
text: format!("Send failed: {}", e),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
}
}
@@ -897,13 +897,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, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", err), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
} else {
self.add_message(ChatLine { sender: "system".into(), text: format!("Group '{}' created", name), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Group '{}' created", name), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
}
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() }),
}
}
@@ -916,14 +916,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, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", err), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
} 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, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Joined '{}' ({} members)", name, members), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
}
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() }),
}
}
@@ -934,18 +934,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, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "No groups".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
} 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, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!(" #{} ({} members)", name, members), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
}
}
}
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() }),
}
}
@@ -958,13 +958,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, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", err), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
} else {
self.add_message(ChatLine { sender: "system".into(), text: format!("Left group '{}'", name), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Left group '{}'", name), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
}
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() }),
}
}
@@ -977,14 +977,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, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", err), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
} else {
let kicked = data.get("kicked").and_then(|v| v.as_str()).unwrap_or("?");
self.add_message(ChatLine { sender: "system".into(), text: format!("Kicked {} from '{}'", kicked, name), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Kicked {} from '{}'", kicked, name), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
}
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() }),
}
}
@@ -994,7 +994,7 @@ impl App {
Ok(resp) => {
if let Ok(data) = resp.json::<serde_json::Value>().await {
if let Some(members) = data.get("members").and_then(|v| v.as_array()) {
self.add_message(ChatLine { sender: "system".into(), text: format!("Members of #{}:", name), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Members of #{}:", name), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
for m in members {
let fp = m.get("fingerprint").and_then(|v| v.as_str()).unwrap_or("?");
let alias = m.get("alias").and_then(|v| v.as_str());
@@ -1003,12 +1003,12 @@ impl App {
Some(a) => format!(" @{} ({}{})", a, &fp[..fp.len().min(12)], if creator { "" } else { "" }),
None => format!(" {}...{}", &fp[..fp.len().min(12)], if creator { "" } else { "" }),
};
self.add_message(ChatLine { sender: "system".into(), text: label, is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: label, is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
}
}
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() }),
}
}
@@ -1025,9 +1025,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, message_id: None, timestamp: Local::now() }); return; }
Err(e) => { self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() }); return; }
},
Err(e) => { self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }); return; }
Err(e) => { self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() }); return; }
};
let my_fp = normfp(&self.our_fp);
@@ -1100,7 +1100,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, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "No members to send to".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
return;
}
@@ -1118,11 +1118,11 @@ impl App {
text: text.to_string(),
is_system: false,
is_self: true,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
}
Err(e) => {
self.add_message(ChatLine { sender: "system".into(), text: format!("Send failed: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Send failed: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
}
@@ -1136,14 +1136,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, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", err), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
} 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, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Alias @{} registered", alias), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
}
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() }),
}
}
@@ -1153,17 +1153,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, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("@{} → {}", name, fp), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
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, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Unknown alias @{}: {}", name, err), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
None
}
Err(e) => {
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
None
}
}
@@ -1180,17 +1180,17 @@ impl App {
let formatted: String = fp.chars().enumerate()
.flat_map(|(i, c)| if i > 0 && i % 4 == 0 { vec![':', c] } else { vec![c] })
.collect();
self.add_message(ChatLine { sender: "system".into(), text: format!("{} → {}", addr, formatted), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("{} → {}", addr, formatted), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
return Some(fp.to_string());
}
if let Some(err) = data.get("error") {
self.add_message(ChatLine { sender: "system".into(), text: format!("Cannot resolve {}: {}", addr, err), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Cannot resolve {}: {}", addr, err), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
None
}
Err(e) => {
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
None
}
}
@@ -1203,18 +1203,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, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: "No aliases".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
} 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, message_id: None, timestamp: Local::now() });
self.add_message(ChatLine { sender: "system".into(), text: format!(" @{} → {}...", name, &fp[..fp.len().min(16)]), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() });
}
}
}
}
}
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }),
Err(e) => self.add_message(ChatLine { sender: "system".into(), text: format!("Error: {}", e), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now() }),
}
}
}

View File

@@ -345,6 +345,7 @@ mod tests {
is_system: false,
is_self: false,
message_id: None,
sender_fp: None,
timestamp: Local::now(),
});
@@ -372,6 +373,7 @@ mod tests {
is_system: false,
is_self: false,
message_id: None,
sender_fp: None,
timestamp: Local::now(),
});
}
@@ -398,6 +400,7 @@ mod tests {
is_system: false,
is_self: false,
message_id: None,
sender_fp: None,
timestamp: Local::now(),
});
}

View File

@@ -25,7 +25,7 @@ impl App {
self.add_message(ChatLine {
sender: "system".into(),
text: format!("File not found: {}", path_str),
is_system: true, is_self: false, message_id: None, timestamp: Local::now(),
is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -36,7 +36,7 @@ impl App {
self.add_message(ChatLine {
sender: "system".into(),
text: format!("Cannot read file: {}", e),
is_system: true, is_self: false, message_id: None, timestamp: Local::now(),
is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -47,7 +47,7 @@ impl App {
self.add_message(ChatLine {
sender: "system".into(),
text: format!("File too large: {} bytes (max {} bytes)", file_size, MAX_FILE_SIZE),
is_system: true, is_self: false, message_id: None, timestamp: Local::now(),
is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -58,7 +58,7 @@ impl App {
self.add_message(ChatLine {
sender: "system".into(),
text: format!("Failed to read file: {}", e),
is_system: true, is_self: false, message_id: None, timestamp: Local::now(),
is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -83,7 +83,7 @@ impl App {
self.add_message(ChatLine {
sender: "system".into(),
text: "Set a peer or group first".into(),
is_system: true, is_self: false, message_id: None, timestamp: Local::now(),
is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -95,7 +95,7 @@ impl App {
self.add_message(ChatLine {
sender: "system".into(),
text: format!("Sending '{}' to group #{}...", filename, group_name),
is_system: true, is_self: false, message_id: None, timestamp: Local::now(),
is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now(),
});
// Get members
@@ -147,7 +147,7 @@ impl App {
self.add_message(ChatLine {
sender: "system".into(),
text: format!("File '{}' sent to group #{}", filename, group_name),
is_system: true, is_self: false, message_id: None, timestamp: Local::now(),
is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
};
@@ -158,7 +158,7 @@ impl App {
self.add_message(ChatLine {
sender: "system".into(),
text: "Invalid peer fingerprint".into(),
is_system: true, is_self: false, message_id: None, timestamp: Local::now(),
is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -170,7 +170,7 @@ impl App {
self.add_message(ChatLine {
sender: "system".into(),
text: format!("Sending file '{}' ({} bytes, {} chunks)...", filename, file_size, total_chunks),
is_system: true, is_self: false, message_id: None, timestamp: Local::now(),
is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now(),
});
// Send FileHeader (unencrypted metadata — the chunks carry ratchet-encrypted data)
@@ -189,7 +189,7 @@ impl App {
self.add_message(ChatLine {
sender: "system".into(),
text: format!("Serialize header failed: {}", e),
is_system: true, is_self: false, message_id: None, timestamp: Local::now(),
is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -199,7 +199,7 @@ impl App {
self.add_message(ChatLine {
sender: "system".into(),
text: format!("Failed to send file header: {}", e),
is_system: true, is_self: false, message_id: None, timestamp: Local::now(),
is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -222,7 +222,7 @@ impl App {
self.add_message(ChatLine {
sender: "system".into(),
text: format!("Serialize chunk failed: {}", e),
is_system: true, is_self: false, message_id: None, timestamp: Local::now(),
is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -232,7 +232,7 @@ impl App {
self.add_message(ChatLine {
sender: "system".into(),
text: format!("Encrypt chunk {} failed: {}", i, e),
is_system: true, is_self: false, message_id: None, timestamp: Local::now(),
is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -241,7 +241,7 @@ impl App {
self.add_message(ChatLine {
sender: "system".into(),
text: "No ratchet session. Send a text message first to establish one.".into(),
is_system: true, is_self: false, message_id: None, timestamp: Local::now(),
is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
};
@@ -261,7 +261,7 @@ impl App {
self.add_message(ChatLine {
sender: "system".into(),
text: format!("Serialize chunk {} failed: {}", i, e),
is_system: true, is_self: false, message_id: None, timestamp: Local::now(),
is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -271,7 +271,7 @@ impl App {
self.add_message(ChatLine {
sender: "system".into(),
text: format!("Failed to send chunk {}/{}: {}", i + 1, total_chunks, e),
is_system: true, is_self: false, message_id: None, timestamp: Local::now(),
is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now(),
});
return;
}
@@ -279,14 +279,14 @@ impl App {
self.add_message(ChatLine {
sender: "system".into(),
text: format!("Sent chunk [{}/{}] of {}", i + 1, total_chunks, filename),
is_system: true, is_self: false, message_id: None, timestamp: Local::now(),
is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: Local::now(),
});
}
self.add_message(ChatLine {
sender: self.our_fp[..12.min(self.our_fp.len())].to_string(),
text: format!("Sent file: {} ({} bytes)", filename, file_size),
is_system: false, is_self: true, message_id: None, timestamp: Local::now(),
is_system: false, is_self: true, message_id: None, sender_fp: None, timestamp: Local::now(),
});
}
}

View File

@@ -83,6 +83,7 @@ pub async fn run_tui(
is_system: true,
is_self: false,
message_id: None,
sender_fp: None,
timestamp: chrono::Local::now(),
});
@@ -91,13 +92,13 @@ pub async fn run_tui(
if let Ok(data) = resp.json::<serde_json::Value>().await {
if let Some(bots) = data.get("bots").and_then(|v| v.as_array()) {
if !bots.is_empty() {
app.add_message(types::ChatLine { sender: "system".into(), text: "Available bots:".into(), is_system: true, is_self: false, message_id: None, timestamp: chrono::Local::now() });
app.add_message(types::ChatLine { sender: "system".into(), text: "Available bots:".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: chrono::Local::now() });
for b in bots {
let name = b.get("name").and_then(|v| v.as_str()).unwrap_or("?");
let desc = b.get("description").and_then(|v| v.as_str()).unwrap_or("");
app.add_message(types::ChatLine { sender: "system".into(), text: format!(" @{} — {}", name, desc), is_system: true, is_self: false, message_id: None, timestamp: chrono::Local::now() });
app.add_message(types::ChatLine { sender: "system".into(), text: format!(" @{} — {}", name, desc), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: chrono::Local::now() });
}
app.add_message(types::ChatLine { sender: "system".into(), text: "Message a bot: /peer @botname".into(), is_system: true, is_self: false, message_id: None, timestamp: chrono::Local::now() });
app.add_message(types::ChatLine { sender: "system".into(), text: "Message a bot: /peer @botname".into(), is_system: true, is_self: false, message_id: None, sender_fp: None, timestamp: chrono::Local::now() });
}
}
}
@@ -126,6 +127,7 @@ pub async fn run_tui(
is_system: true,
is_self: false,
message_id: None,
sender_fp: None,
timestamp: chrono::Local::now(),
});
}
@@ -140,6 +142,38 @@ pub async fn run_tui(
loop {
terminal.draw(|frame| app.draw(frame))?;
// Send Read receipts for visible messages
{
let msgs = app.messages.lock().unwrap();
let total = msgs.len();
let visible_end = total.saturating_sub(app.scroll_offset);
let visible_height = 20; // approximate
let visible_start = visible_end.saturating_sub(visible_height);
let mut sent = app.read_receipts_sent.lock().unwrap();
for msg in &msgs[visible_start..visible_end] {
if msg.is_system || msg.is_self { continue; }
if let (Some(ref msg_id), Some(ref sfp)) = (&msg.message_id, &msg.sender_fp) {
if sent.contains(msg_id) { continue; }
sent.insert(msg_id.clone());
// Fire-and-forget Read receipt
let receipt = warzone_protocol::message::WireMessage::Receipt {
sender_fingerprint: app.our_fp.clone(),
message_id: msg_id.clone(),
receipt_type: warzone_protocol::message::ReceiptType::Read,
};
if let Ok(encoded) = bincode::serialize(&receipt) {
let client = client.clone();
let to = sfp.clone();
let from = app.our_fp.clone();
tokio::spawn(async move {
let _ = client.send_message(&to, Some(&from), &encoded).await;
});
}
}
}
}
if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if key.code == KeyCode::Enter {

View File

@@ -179,7 +179,7 @@ fn process_wire_message(
text,
is_system: false,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: Some(id.clone()), sender_fp: Some(sender_fingerprint.clone()), timestamp: Local::now(),
});
send_receipt(our_fp, &sender_fingerprint, &id, ReceiptType::Delivered, client);
// Terminal bell for incoming DM
@@ -196,7 +196,7 @@ fn process_wire_message(
),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
tracing::warn!("Session auto-recovery: cleared session for {} after decrypt error: {}", sender_fingerprint, e);
}
@@ -228,7 +228,7 @@ fn process_wire_message(
text,
is_system: false,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: Some(id.clone()), sender_fp: Some(sender_fingerprint.clone()), timestamp: Local::now(),
});
send_receipt(our_fp, &sender_fingerprint, &id, ReceiptType::Delivered, client);
// Terminal bell for incoming DM
@@ -245,7 +245,7 @@ fn process_wire_message(
),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
tracing::warn!("Session auto-recovery: cleared session for {} after decrypt error: {}", sender_fingerprint, e);
}
@@ -290,7 +290,7 @@ fn process_wire_message(
),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
let transfer = PendingFileTransfer {
@@ -351,7 +351,7 @@ fn process_wire_message(
),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
// Check if all chunks received
@@ -377,7 +377,7 @@ fn process_wire_message(
),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
} else {
// Save to data_dir/downloads/
@@ -394,7 +394,7 @@ fn process_wire_message(
),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
}
Err(e) => {
@@ -403,7 +403,7 @@ fn process_wire_message(
text: format!("Failed to save file: {}", e),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
}
}
@@ -450,6 +450,7 @@ fn process_wire_message(
is_system: false,
is_self: false,
message_id: None,
sender_fp: None,
timestamp: Local::now(),
});
}
@@ -465,6 +466,7 @@ fn process_wire_message(
is_system: true,
is_self: false,
message_id: None,
sender_fp: None,
timestamp: Local::now(),
});
}
@@ -481,6 +483,7 @@ fn process_wire_message(
is_system: true,
is_self: false,
message_id: None,
sender_fp: None,
timestamp: Local::now(),
});
}
@@ -510,6 +513,7 @@ fn process_wire_message(
is_system: true,
is_self: false,
message_id: None,
sender_fp: None,
timestamp: Local::now(),
});
}
@@ -529,7 +533,7 @@ fn process_wire_message(
text: format!("\u{1f4de} Incoming call from {} \u{2014} /accept or /reject", sender_short),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
// Terminal bell for incoming call
print!("\x07");
@@ -540,7 +544,7 @@ fn process_wire_message(
text: format!("\u{2713} {} accepted the call", sender_short),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
}
CallSignalType::Hangup => {
@@ -549,7 +553,7 @@ fn process_wire_message(
text: "Call ended".into(),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
}
CallSignalType::Reject => {
@@ -558,7 +562,7 @@ fn process_wire_message(
text: format!("{} rejected the call", sender_short),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
}
CallSignalType::Ringing => {
@@ -567,7 +571,7 @@ fn process_wire_message(
text: "Ringing...".into(),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
}
CallSignalType::Busy => {
@@ -576,7 +580,7 @@ fn process_wire_message(
text: format!("{} is busy", sender_short),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
}
_ => {
@@ -585,7 +589,7 @@ fn process_wire_message(
text: format!("\u{1f4de} Call signal: {:?}", signal_type),
is_system: false,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
}
}
@@ -626,7 +630,7 @@ pub async fn poll_loop(
text: "Real-time connection established".into(),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
use futures_util::StreamExt;
@@ -652,6 +656,7 @@ pub async fn poll_loop(
is_system: true,
is_self: false,
message_id: None,
sender_fp: None,
timestamp: Local::now(),
});
print!("\x07");
@@ -664,6 +669,7 @@ pub async fn poll_loop(
is_system: false,
is_self: false,
message_id: None,
sender_fp: None,
timestamp: Local::now(),
});
print!("\x07");
@@ -680,7 +686,7 @@ pub async fn poll_loop(
text: "Connection lost, reconnecting...".into(),
is_system: true,
is_self: false,
message_id: None, timestamp: Local::now(),
message_id: None, sender_fp: None, timestamp: Local::now(),
});
tokio::time::sleep(Duration::from_secs(3)).await;
}

View File

@@ -67,6 +67,8 @@ pub struct App {
pub connected: Arc<AtomicBool>,
/// Current call state: None=idle, Some(state)=active
pub call_state: Option<CallInfo>,
/// Message IDs for which we've already sent a Read receipt (avoid duplicates).
pub read_receipts_sent: Arc<Mutex<std::collections::HashSet<String>>>,
}
#[derive(Clone)]
@@ -77,6 +79,8 @@ pub struct ChatLine {
pub is_self: bool,
/// Message ID (for sent messages, used to track receipts).
pub message_id: Option<String>,
/// Sender's full fingerprint (for sending read receipts back).
pub sender_fp: Option<String>,
/// When this message was created/received.
pub timestamp: DateTime<Local>,
}
@@ -99,6 +103,7 @@ impl App {
is_system: true,
is_self: false,
message_id: None,
sender_fp: None,
timestamp: Local::now(),
}]));
@@ -109,6 +114,7 @@ impl App {
is_system: true,
is_self: false,
message_id: None,
sender_fp: None,
timestamp: Local::now(),
});
} else {
@@ -118,6 +124,7 @@ impl App {
is_system: true,
is_self: false,
message_id: None,
sender_fp: None,
timestamp: Local::now(),
});
}
@@ -128,6 +135,7 @@ impl App {
is_system: true,
is_self: false,
message_id: None,
sender_fp: None,
timestamp: Local::now(),
});
@@ -147,6 +155,7 @@ impl App {
scroll_offset: 0,
connected: Arc::new(AtomicBool::new(false)),
call_state: None,
read_receipts_sent: Arc::new(Mutex::new(std::collections::HashSet::new())),
}
}
@@ -210,6 +219,7 @@ mod tests {
is_system: false,
is_self: false,
message_id: None,
sender_fp: None,
timestamp: Local::now(),
};
// Timestamp should be within the last second
@@ -227,6 +237,7 @@ mod tests {
is_system: false,
is_self: false,
message_id: None,
sender_fp: None,
timestamp: Local::now(),
});
let new_count = app.messages.lock().unwrap().len();

View File

@@ -1,6 +1,6 @@
[package]
name = "warzone-protocol"
version = "0.0.40"
version = "0.0.41"
edition = "2021"
license = "MIT"
description = "Core crypto & wire protocol for featherChat (Warzone messenger)"

View File

@@ -50,7 +50,7 @@ async fn pwa_manifest() -> impl IntoResponse {
async fn service_worker() -> impl IntoResponse {
([(header::CONTENT_TYPE, "application/javascript")], r##"
const CACHE = 'wz-v22';
const CACHE = 'wz-v23';
const SHELL = ['/', '/wasm/warzone_wasm.js', '/wasm/warzone_wasm_bg.wasm', '/icon.svg', '/manifest.json'];
self.addEventListener('install', e => {
@@ -287,7 +287,7 @@ let pollTimer = null;
let ws = null; // WebSocket connection
let wasmReady = false;
const VERSION = '0.0.40';
const VERSION = '0.0.41';
let DEBUG = true; // toggle with /debug command
// ── Receipt tracking ──