v0.0.37: TUI call state UI, missed calls, inline keyboards in web

FC-P2-T4: TUI call state machine
- CallInfo struct + CallPhase enum (Calling/Ringing/Active)
- Header shows call indicator: yellow "Calling...", magenta "Incoming", green timer
- /call sets Calling, /accept sets Active, /reject+/hangup clears
- Incoming signals show contextual messages (Offer→prompt, Answer→connected, etc.)

FC-P2-T5: Missed call display in TUI
- WS Text frames parsed for missed_call + bot_message JSON
- Missed calls: "📞 Missed call from X at HH:MM" + terminal bell
- Bot messages rendered as @botname: text

FC-P8-T5: Inline keyboard buttons in web
- CSS styled keyboard buttons (.kbd-btn)
- Bot messages with reply_markup render clickable button rows
- Click sends callback_data back to bot as bot_message
- Works in both WS text handler and handleIncomingMessage fallback

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-29 16:44:14 +04:00
parent 3429f518b1
commit a368ab24d2
8 changed files with 288 additions and 38 deletions

View File

@@ -549,6 +549,12 @@ impl App {
.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.call_state = Some(super::types::CallInfo {
peer_fp: peer_fp_clean.clone(),
peer_display: display.clone(),
state: super::types::CallPhase::Calling,
started_at: 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() });
@@ -578,6 +584,12 @@ 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.call_state = Some(super::types::CallInfo {
peer_fp: normfp(&peer),
peer_display: peer[..peer.len().min(16)].to_string(),
state: super::types::CallPhase::Active,
started_at: Local::now(),
});
}
return;
}
@@ -603,6 +615,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.call_state = None;
}
return;
}
@@ -629,6 +642,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.call_state = None;
}
return;
}