From 3429f518b176357492d7c0d719e39961595c39b0 Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Sun, 29 Mar 2026 16:19:01 +0400 Subject: [PATCH] feat: TUI /call, /accept, /reject, /hangup commands (FC-P2-T1+T2+T3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /call [fp|@alias|0x...] — send CallSignal::Offer to peer - /accept — answer incoming call (uses last_dm_peer) - /reject — reject incoming call - /hangup — end active call - All four added to /help text Co-Authored-By: Claude Opus 4.6 (1M context) --- .../crates/warzone-client/src/tui/commands.rs | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/warzone/crates/warzone-client/src/tui/commands.rs b/warzone/crates/warzone-client/src/tui/commands.rs index b77a84b..eeedb6c 100644 --- a/warzone/crates/warzone-client/src/tui/commands.rs +++ b/warzone/crates/warzone-client/src/tui/commands.rs @@ -69,6 +69,10 @@ impl App { " /gkick Kick member from group", " /gmembers List group members", " /file Send a file (max 10MB)", + " /call [fp|@alias] Call current peer (or specified peer)", + " /accept Accept incoming call", + " /reject Reject incoming call", + " /hangup End current call", " /quit, /q Exit", "", "Navigation:", @@ -488,6 +492,147 @@ impl App { } return; } + if text == "/call" || text.starts_with("/call ") { + let target = if text.starts_with("/call ") { + let arg = text[6..].trim(); + if arg.starts_with('@') { + match self.resolve_alias(&arg[1..], client).await { + Some(fp) => Some(fp), + None => return, + } + } else if arg.starts_with("0x") || arg.starts_with("0X") { + match self.resolve_address(arg, client).await { + Some(fp) => Some(fp), + None => return, + } + } else if !arg.is_empty() { + Some(arg.to_string()) + } else { + None + } + } else { + None + }; + + let peer = target.or_else(|| self.peer_fp.clone()); + 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 or set a peer first.".into(), is_system: true, is_self: false, message_id: None, timestamp: Local::now() }); + return; + } + }; + + let peer_fp_clean = normfp(&peer); + let msg_id = uuid::Uuid::new_v4().to_string(); + let our_pub = identity.public_identity(); + + let wire = warzone_protocol::message::WireMessage::CallSignal { + id: msg_id.clone(), + sender_fingerprint: our_pub.fingerprint.to_string(), + signal_type: warzone_protocol::message::CallSignalType::Offer, + payload: String::new(), + target: peer_fp_clean.clone(), + }; + 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() }); + return; + } + }; + + match client.send_message(&peer_fp_clean, Some(&self.our_fp), &encoded).await { + Ok(_) => { + let display = self.peer_eth.as_deref() + .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() }); + } + 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() }); + } + } + return; + } + + if text == "/accept" { + 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() }); + return; + } + }; + + let msg_id = uuid::Uuid::new_v4().to_string(); + let our_pub = identity.public_identity(); + let wire = warzone_protocol::message::WireMessage::CallSignal { + id: msg_id, + sender_fingerprint: our_pub.fingerprint.to_string(), + signal_type: warzone_protocol::message::CallSignalType::Answer, + payload: String::new(), + target: normfp(&peer), + }; + 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() }); + } + return; + } + + if text == "/reject" { + 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() }); + return; + } + }; + + let msg_id = uuid::Uuid::new_v4().to_string(); + let our_pub = identity.public_identity(); + let wire = warzone_protocol::message::WireMessage::CallSignal { + id: msg_id, + sender_fingerprint: our_pub.fingerprint.to_string(), + signal_type: warzone_protocol::message::CallSignalType::Reject, + payload: String::new(), + target: normfp(&peer), + }; + 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() }); + } + return; + } + + if text == "/hangup" { + let peer = self.peer_fp.clone().or_else(|| self.last_dm_peer.lock().unwrap().clone()); + 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() }); + return; + } + }; + + let msg_id = uuid::Uuid::new_v4().to_string(); + let our_pub = identity.public_identity(); + let wire = warzone_protocol::message::WireMessage::CallSignal { + id: msg_id, + sender_fingerprint: our_pub.fingerprint.to_string(), + signal_type: warzone_protocol::message::CallSignalType::Hangup, + payload: String::new(), + target: normfp(&peer), + }; + 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() }); + } + return; + } + if text.starts_with("/file ") { let path_str = text[6..].trim(); self.handle_file_send(path_str, identity, db, client).await;