feat: TUI /call, /accept, /reject, /hangup commands (FC-P2-T1+T2+T3)

- /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) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-29 16:19:01 +04:00
parent e9182fdb41
commit 3429f518b1

View File

@@ -69,6 +69,10 @@ impl App {
" /gkick <fp> Kick member from group",
" /gmembers List group members",
" /file <path> 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 <fp> 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;