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:
10
warzone/Cargo.lock
generated
10
warzone/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -9,7 +9,7 @@ members = [
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.0.40"
|
||||
version = "0.0.41"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
rust-version = "1.75"
|
||||
|
||||
@@ -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() }),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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 ──
|
||||
|
||||
Reference in New Issue
Block a user