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:
10
warzone/Cargo.lock
generated
10
warzone/Cargo.lock
generated
@@ -2956,7 +2956,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-client"
|
name = "warzone-client"
|
||||||
version = "0.0.36"
|
version = "0.0.37"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
@@ -2989,7 +2989,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-mule"
|
name = "warzone-mule"
|
||||||
version = "0.0.36"
|
version = "0.0.37"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
@@ -2998,7 +2998,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-protocol"
|
name = "warzone-protocol"
|
||||||
version = "0.0.36"
|
version = "0.0.37"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bincode",
|
"bincode",
|
||||||
@@ -3023,7 +3023,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-server"
|
name = "warzone-server"
|
||||||
version = "0.0.36"
|
version = "0.0.37"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -3053,7 +3053,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "warzone-wasm"
|
name = "warzone-wasm"
|
||||||
version = "0.0.36"
|
version = "0.0.37"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bincode",
|
"bincode",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.0.36"
|
version = "0.0.37"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
rust-version = "1.75"
|
rust-version = "1.75"
|
||||||
|
|||||||
@@ -549,6 +549,12 @@ impl App {
|
|||||||
.map(|s| if s.len() > 16 { format!("{}...", &s[..16]) } else { s.to_string() })
|
.map(|s| if s.len() > 16 { format!("{}...", &s[..16]) } else { s.to_string() })
|
||||||
.unwrap_or_default();
|
.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, 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) => {
|
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, timestamp: Local::now() });
|
||||||
@@ -578,6 +584,12 @@ impl App {
|
|||||||
if let Ok(encoded) = bincode::serialize(&wire) {
|
if let Ok(encoded) = bincode::serialize(&wire) {
|
||||||
let _ = client.send_message(&normfp(&peer), Some(&self.our_fp), &encoded).await;
|
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, 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;
|
return;
|
||||||
}
|
}
|
||||||
@@ -603,6 +615,7 @@ impl App {
|
|||||||
if let Ok(encoded) = bincode::serialize(&wire) {
|
if let Ok(encoded) = bincode::serialize(&wire) {
|
||||||
let _ = client.send_message(&normfp(&peer), Some(&self.our_fp), &encoded).await;
|
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, timestamp: Local::now() });
|
||||||
|
self.call_state = None;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -629,6 +642,7 @@ impl App {
|
|||||||
if let Ok(encoded) = bincode::serialize(&wire) {
|
if let Ok(encoded) = bincode::serialize(&wire) {
|
||||||
let _ = client.send_message(&normfp(&peer), Some(&self.our_fp), &encoded).await;
|
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, timestamp: Local::now() });
|
||||||
|
self.call_state = None;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ use ratatui::text::{Line, Span};
|
|||||||
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap};
|
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
|
|
||||||
|
use chrono::Local;
|
||||||
|
|
||||||
use super::types::{App, ReceiptStatus};
|
use super::types::{App, ReceiptStatus};
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@@ -65,6 +67,28 @@ impl App {
|
|||||||
} else {
|
} else {
|
||||||
format!("{}", &self.our_eth[..self.our_eth.len().min(12)])
|
format!("{}", &self.our_eth[..self.our_eth.len().min(12)])
|
||||||
};
|
};
|
||||||
|
// Call indicator
|
||||||
|
let call_span = match &self.call_state {
|
||||||
|
Some(info) => {
|
||||||
|
let label = match info.state {
|
||||||
|
super::types::CallPhase::Calling => format!(" \u{1f4de} Calling {}...", &info.peer_display[..info.peer_display.len().min(12)]),
|
||||||
|
super::types::CallPhase::Ringing => format!(" \u{1f4de} Incoming from {}", &info.peer_display[..info.peer_display.len().min(12)]),
|
||||||
|
super::types::CallPhase::Active => {
|
||||||
|
let elapsed = Local::now().signed_duration_since(info.started_at);
|
||||||
|
let mins = elapsed.num_minutes();
|
||||||
|
let secs = elapsed.num_seconds() % 60;
|
||||||
|
format!(" \u{1f50a} {}:{:02}", mins, secs)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let color = match info.state {
|
||||||
|
super::types::CallPhase::Calling => Color::Yellow,
|
||||||
|
super::types::CallPhase::Ringing => Color::Magenta,
|
||||||
|
super::types::CallPhase::Active => Color::Green,
|
||||||
|
};
|
||||||
|
Span::styled(label, Style::default().fg(color))
|
||||||
|
}
|
||||||
|
None => Span::raw(""),
|
||||||
|
};
|
||||||
let header = Paragraph::new(Line::from(vec![
|
let header = Paragraph::new(Line::from(vec![
|
||||||
Span::styled("WZ ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
Span::styled("WZ ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
|
||||||
Span::styled(identity_display, Style::default().fg(Color::Green)),
|
Span::styled(identity_display, Style::default().fg(Color::Green)),
|
||||||
@@ -75,6 +99,7 @@ impl App {
|
|||||||
Style::default().fg(Color::DarkGray),
|
Style::default().fg(Color::DarkGray),
|
||||||
),
|
),
|
||||||
Span::styled(conn_indicator, Style::default().fg(conn_color)),
|
Span::styled(conn_indicator, Style::default().fg(conn_color)),
|
||||||
|
call_span,
|
||||||
]));
|
]));
|
||||||
frame.render_widget(header, chunks[0]);
|
frame.render_widget(header, chunks[0]);
|
||||||
|
|
||||||
|
|||||||
@@ -496,14 +496,75 @@ fn process_wire_message(
|
|||||||
payload: _,
|
payload: _,
|
||||||
target: _,
|
target: _,
|
||||||
} => {
|
} => {
|
||||||
let type_str = format!("{:?}", signal_type);
|
use warzone_protocol::message::CallSignalType;
|
||||||
messages.lock().unwrap().push(ChatLine {
|
let sender_short = { cache_eth_lookup(&sender_fingerprint, client, eth_cache); display_sender(&sender_fingerprint, eth_cache) };
|
||||||
sender: { cache_eth_lookup(&sender_fingerprint, client, eth_cache); display_sender(&sender_fingerprint, eth_cache) },
|
match signal_type {
|
||||||
text: format!("\u{1f4de} Call signal: {}", type_str),
|
CallSignalType::Offer => {
|
||||||
is_system: false,
|
messages.lock().unwrap().push(ChatLine {
|
||||||
is_self: false,
|
sender: "system".into(),
|
||||||
message_id: None, timestamp: Local::now(),
|
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(),
|
||||||
|
});
|
||||||
|
// Terminal bell for incoming call
|
||||||
|
print!("\x07");
|
||||||
|
}
|
||||||
|
CallSignalType::Answer => {
|
||||||
|
messages.lock().unwrap().push(ChatLine {
|
||||||
|
sender: "system".into(),
|
||||||
|
text: format!("\u{2713} {} accepted the call", sender_short),
|
||||||
|
is_system: true,
|
||||||
|
is_self: false,
|
||||||
|
message_id: None, timestamp: Local::now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
CallSignalType::Hangup => {
|
||||||
|
messages.lock().unwrap().push(ChatLine {
|
||||||
|
sender: "system".into(),
|
||||||
|
text: "Call ended".into(),
|
||||||
|
is_system: true,
|
||||||
|
is_self: false,
|
||||||
|
message_id: None, timestamp: Local::now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
CallSignalType::Reject => {
|
||||||
|
messages.lock().unwrap().push(ChatLine {
|
||||||
|
sender: "system".into(),
|
||||||
|
text: format!("{} rejected the call", sender_short),
|
||||||
|
is_system: true,
|
||||||
|
is_self: false,
|
||||||
|
message_id: None, timestamp: Local::now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
CallSignalType::Ringing => {
|
||||||
|
messages.lock().unwrap().push(ChatLine {
|
||||||
|
sender: "system".into(),
|
||||||
|
text: "Ringing...".into(),
|
||||||
|
is_system: true,
|
||||||
|
is_self: false,
|
||||||
|
message_id: None, timestamp: Local::now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
CallSignalType::Busy => {
|
||||||
|
messages.lock().unwrap().push(ChatLine {
|
||||||
|
sender: "system".into(),
|
||||||
|
text: format!("{} is busy", sender_short),
|
||||||
|
is_system: true,
|
||||||
|
is_self: false,
|
||||||
|
message_id: None, timestamp: Local::now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
messages.lock().unwrap().push(ChatLine {
|
||||||
|
sender: sender_short,
|
||||||
|
text: format!("\u{1f4de} Call signal: {:?}", signal_type),
|
||||||
|
is_system: false,
|
||||||
|
is_self: false,
|
||||||
|
message_id: None, timestamp: Local::now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -545,8 +606,44 @@ pub async fn poll_loop(
|
|||||||
let (_, mut read) = ws_stream.split();
|
let (_, mut read) = ws_stream.split();
|
||||||
|
|
||||||
while let Some(Ok(msg)) = read.next().await {
|
while let Some(Ok(msg)) = read.next().await {
|
||||||
if let tokio_tungstenite::tungstenite::Message::Binary(data) = msg {
|
match msg {
|
||||||
process_incoming(&data, &identity, &db, &messages, &receipts, &pending_files, &our_fp, &client, ð_cache, &last_dm_peer);
|
tokio_tungstenite::tungstenite::Message::Binary(data) => {
|
||||||
|
process_incoming(&data, &identity, &db, &messages, &receipts, &pending_files, &our_fp, &client, ð_cache, &last_dm_peer);
|
||||||
|
}
|
||||||
|
tokio_tungstenite::tungstenite::Message::Text(text) => {
|
||||||
|
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&text) {
|
||||||
|
if json.get("type").and_then(|v| v.as_str()) == Some("missed_call") {
|
||||||
|
let data = json.get("data").cloned().unwrap_or_default();
|
||||||
|
let caller = data.get("caller_fp").and_then(|v| v.as_str()).unwrap_or("unknown");
|
||||||
|
let ts = data.get("timestamp").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||||
|
let when = chrono::DateTime::from_timestamp(ts, 0)
|
||||||
|
.map(|dt| dt.with_timezone(&Local).format("%H:%M").to_string())
|
||||||
|
.unwrap_or_else(|| "?".to_string());
|
||||||
|
messages.lock().unwrap().push(ChatLine {
|
||||||
|
sender: "system".into(),
|
||||||
|
text: format!("\u{1f4de} Missed call from {} at {}", &caller[..caller.len().min(12)], when),
|
||||||
|
is_system: true,
|
||||||
|
is_self: false,
|
||||||
|
message_id: None,
|
||||||
|
timestamp: Local::now(),
|
||||||
|
});
|
||||||
|
print!("\x07");
|
||||||
|
} else if json.get("type").and_then(|v| v.as_str()) == Some("bot_message") {
|
||||||
|
let from = json.get("from_name").or(json.get("from")).and_then(|v| v.as_str()).unwrap_or("bot");
|
||||||
|
let text_content = json.get("text").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
messages.lock().unwrap().push(ChatLine {
|
||||||
|
sender: format!("@{}", from),
|
||||||
|
text: text_content.to_string(),
|
||||||
|
is_system: false,
|
||||||
|
is_self: false,
|
||||||
|
message_id: None,
|
||||||
|
timestamp: Local::now(),
|
||||||
|
});
|
||||||
|
print!("\x07");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,22 @@ pub enum ReceiptStatus {
|
|||||||
Read,
|
Read,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Active call information.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct CallInfo {
|
||||||
|
pub peer_fp: String,
|
||||||
|
pub peer_display: String,
|
||||||
|
pub state: CallPhase,
|
||||||
|
pub started_at: DateTime<Local>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq)]
|
||||||
|
pub enum CallPhase {
|
||||||
|
Calling, // we initiated, waiting for answer
|
||||||
|
Ringing, // incoming call, waiting for user to accept/reject
|
||||||
|
Active, // call connected
|
||||||
|
}
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
pub input: String,
|
pub input: String,
|
||||||
pub messages: Arc<Mutex<Vec<ChatLine>>>,
|
pub messages: Arc<Mutex<Vec<ChatLine>>>,
|
||||||
@@ -49,6 +65,8 @@ pub struct App {
|
|||||||
pub scroll_offset: usize,
|
pub scroll_offset: usize,
|
||||||
/// Whether the WebSocket connection is active.
|
/// Whether the WebSocket connection is active.
|
||||||
pub connected: Arc<AtomicBool>,
|
pub connected: Arc<AtomicBool>,
|
||||||
|
/// Current call state: None=idle, Some(state)=active
|
||||||
|
pub call_state: Option<CallInfo>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -128,6 +146,7 @@ impl App {
|
|||||||
peer_eth: None,
|
peer_eth: None,
|
||||||
scroll_offset: 0,
|
scroll_offset: 0,
|
||||||
connected: Arc::new(AtomicBool::new(false)),
|
connected: Arc::new(AtomicBool::new(false)),
|
||||||
|
call_state: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "warzone-protocol"
|
name = "warzone-protocol"
|
||||||
version = "0.0.36"
|
version = "0.0.37"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "Core crypto & wire protocol for featherChat (Warzone messenger)"
|
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 {
|
async fn service_worker() -> impl IntoResponse {
|
||||||
([(header::CONTENT_TYPE, "application/javascript")], r##"
|
([(header::CONTENT_TYPE, "application/javascript")], r##"
|
||||||
const CACHE = 'wz-v18';
|
const CACHE = 'wz-v19';
|
||||||
const SHELL = ['/', '/wasm/warzone_wasm.js', '/wasm/warzone_wasm_bg.wasm', '/icon.svg', '/manifest.json'];
|
const SHELL = ['/', '/wasm/warzone_wasm.js', '/wasm/warzone_wasm_bg.wasm', '/icon.svg', '/manifest.json'];
|
||||||
|
|
||||||
self.addEventListener('install', e => {
|
self.addEventListener('install', e => {
|
||||||
@@ -204,6 +204,12 @@ const WEB_HTML: &str = r##"<!DOCTYPE html>
|
|||||||
.incoming-call { animation: pulse 1.5s infinite; }
|
.incoming-call { animation: pulse 1.5s infinite; }
|
||||||
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
|
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
|
||||||
|
|
||||||
|
.inline-kbd { margin: 4px 0; display: flex; flex-wrap: wrap; gap: 4px; }
|
||||||
|
.inline-kbd-row { display: flex; gap: 4px; width: 100%; }
|
||||||
|
.kbd-btn { padding: 4px 12px; background: #1a1a3e; border: 1px solid #333; border-radius: 4px;
|
||||||
|
color: #4fc3f7; cursor: pointer; font-family: inherit; font-size: 0.8em; flex: 1; text-align: center; }
|
||||||
|
.kbd-btn:hover { background: #252550; border-color: #4fc3f7; }
|
||||||
|
|
||||||
@media (max-width: 500px) {
|
@media (max-width: 500px) {
|
||||||
.msg { font-size: 0.8em; }
|
.msg { font-size: 0.8em; }
|
||||||
#chat-header input { width: 180px; }
|
#chat-header input { width: 180px; }
|
||||||
@@ -281,7 +287,7 @@ let pollTimer = null;
|
|||||||
let ws = null; // WebSocket connection
|
let ws = null; // WebSocket connection
|
||||||
let wasmReady = false;
|
let wasmReady = false;
|
||||||
|
|
||||||
const VERSION = '0.0.36';
|
const VERSION = '0.0.37';
|
||||||
let DEBUG = true; // toggle with /debug command
|
let DEBUG = true; // toggle with /debug command
|
||||||
|
|
||||||
// ── Receipt tracking ──
|
// ── Receipt tracking ──
|
||||||
@@ -584,22 +590,67 @@ function connectWebSocket() {
|
|||||||
if (json.type === 'bot_message') {
|
if (json.type === 'bot_message') {
|
||||||
const botName = json.from_name || json.from || 'bot';
|
const botName = json.from_name || json.from || 'bot';
|
||||||
let msgText = json.text || '';
|
let msgText = json.text || '';
|
||||||
if (json.reply_markup && json.reply_markup.inline_keyboard) {
|
|
||||||
msgText += '\\n';
|
|
||||||
for (const row of json.reply_markup.inline_keyboard) {
|
|
||||||
for (const btn of row) {
|
|
||||||
msgText += ' [' + btn.text + '] ';
|
|
||||||
}
|
|
||||||
msgText += '\\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const useHtml = json.parse_mode === 'HTML';
|
const useHtml = json.parse_mode === 'HTML';
|
||||||
addMsg('@' + botName, msgText, false, null, useHtml);
|
addMsg('@' + botName, msgText, false, null, useHtml);
|
||||||
lastDmPeer = json.from ? normFP(json.from) : '';
|
lastDmPeer = json.from ? normFP(json.from) : '';
|
||||||
|
// Render inline keyboard if present
|
||||||
|
if (json.reply_markup && json.reply_markup.inline_keyboard) {
|
||||||
|
const kbdDiv = document.createElement('div');
|
||||||
|
kbdDiv.className = 'inline-kbd';
|
||||||
|
for (const row of json.reply_markup.inline_keyboard) {
|
||||||
|
const rowDiv = document.createElement('div');
|
||||||
|
rowDiv.className = 'inline-kbd-row';
|
||||||
|
for (const btn of row) {
|
||||||
|
const btnEl = document.createElement('button');
|
||||||
|
btnEl.className = 'kbd-btn';
|
||||||
|
btnEl.textContent = btn.text;
|
||||||
|
btnEl.onclick = async function() {
|
||||||
|
const cbData = btn.callback_data || btn.text;
|
||||||
|
const botFp = json.from ? normFP(json.from) : '';
|
||||||
|
if (botFp) {
|
||||||
|
const cbMsg = {type:'bot_message',id:Date.now().toString(),from:normFP(myFingerprint),from_name:myEthAddress||myFingerprint.slice(0,19),text:cbData,timestamp:Math.floor(Date.now()/1000),is_callback:true};
|
||||||
|
await fetch(SERVER+'/v1/messages/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({to:botFp,from:normFP(myFingerprint),message:Array.from(new TextEncoder().encode(JSON.stringify(cbMsg)))})});
|
||||||
|
addSys('Sent: ' + btn.text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rowDiv.appendChild(btnEl);
|
||||||
|
}
|
||||||
|
kbdDiv.appendChild(rowDiv);
|
||||||
|
}
|
||||||
|
$messages.appendChild(kbdDiv);
|
||||||
|
$messages.scrollTop = $messages.scrollHeight;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (json.type === 'bot_edit') {
|
if (json.type === 'bot_edit') {
|
||||||
addSys('[bot updated: ' + (json.text || '') + ']');
|
addSys('[bot updated: ' + (json.text || '') + ']');
|
||||||
|
// Render inline keyboard if present
|
||||||
|
if (json.reply_markup && json.reply_markup.inline_keyboard) {
|
||||||
|
const kbdDiv = document.createElement('div');
|
||||||
|
kbdDiv.className = 'inline-kbd';
|
||||||
|
for (const row of json.reply_markup.inline_keyboard) {
|
||||||
|
const rowDiv = document.createElement('div');
|
||||||
|
rowDiv.className = 'inline-kbd-row';
|
||||||
|
for (const btn of row) {
|
||||||
|
const btnEl = document.createElement('button');
|
||||||
|
btnEl.className = 'kbd-btn';
|
||||||
|
btnEl.textContent = btn.text;
|
||||||
|
btnEl.onclick = async function() {
|
||||||
|
const cbData = btn.callback_data || btn.text;
|
||||||
|
const botFp = json.from ? normFP(json.from) : '';
|
||||||
|
if (botFp) {
|
||||||
|
const cbMsg = {type:'bot_message',id:Date.now().toString(),from:normFP(myFingerprint),from_name:myEthAddress||myFingerprint.slice(0,19),text:cbData,timestamp:Math.floor(Date.now()/1000),is_callback:true};
|
||||||
|
await fetch(SERVER+'/v1/messages/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({to:botFp,from:normFP(myFingerprint),message:Array.from(new TextEncoder().encode(JSON.stringify(cbMsg)))})});
|
||||||
|
addSys('Sent: ' + btn.text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rowDiv.appendChild(btnEl);
|
||||||
|
}
|
||||||
|
kbdDiv.appendChild(rowDiv);
|
||||||
|
}
|
||||||
|
$messages.appendChild(kbdDiv);
|
||||||
|
$messages.scrollTop = $messages.scrollHeight;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (json.type === 'bot_document') {
|
if (json.type === 'bot_document') {
|
||||||
@@ -735,23 +786,67 @@ async function handleIncomingMessage(bytes) {
|
|||||||
if (json.type === 'bot_message') {
|
if (json.type === 'bot_message') {
|
||||||
const botName = json.from_name || json.from || 'bot';
|
const botName = json.from_name || json.from || 'bot';
|
||||||
let msgText = json.text || '';
|
let msgText = json.text || '';
|
||||||
// Handle inline keyboard if present
|
|
||||||
if (json.reply_markup && json.reply_markup.inline_keyboard) {
|
|
||||||
msgText += '\\n';
|
|
||||||
for (const row of json.reply_markup.inline_keyboard) {
|
|
||||||
for (const btn of row) {
|
|
||||||
msgText += ' [' + btn.text + '] ';
|
|
||||||
}
|
|
||||||
msgText += '\\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const useHtml = json.parse_mode === 'HTML';
|
const useHtml = json.parse_mode === 'HTML';
|
||||||
addMsg('@' + botName, msgText, false, null, useHtml);
|
addMsg('@' + botName, msgText, false, null, useHtml);
|
||||||
lastDmPeer = json.from ? normFP(json.from) : '';
|
lastDmPeer = json.from ? normFP(json.from) : '';
|
||||||
|
// Render inline keyboard if present
|
||||||
|
if (json.reply_markup && json.reply_markup.inline_keyboard) {
|
||||||
|
const kbdDiv = document.createElement('div');
|
||||||
|
kbdDiv.className = 'inline-kbd';
|
||||||
|
for (const row of json.reply_markup.inline_keyboard) {
|
||||||
|
const rowDiv = document.createElement('div');
|
||||||
|
rowDiv.className = 'inline-kbd-row';
|
||||||
|
for (const btn of row) {
|
||||||
|
const btnEl = document.createElement('button');
|
||||||
|
btnEl.className = 'kbd-btn';
|
||||||
|
btnEl.textContent = btn.text;
|
||||||
|
btnEl.onclick = async function() {
|
||||||
|
const cbData = btn.callback_data || btn.text;
|
||||||
|
const botFp = json.from ? normFP(json.from) : '';
|
||||||
|
if (botFp) {
|
||||||
|
const cbMsg = {type:'bot_message',id:Date.now().toString(),from:normFP(myFingerprint),from_name:myEthAddress||myFingerprint.slice(0,19),text:cbData,timestamp:Math.floor(Date.now()/1000),is_callback:true};
|
||||||
|
await fetch(SERVER+'/v1/messages/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({to:botFp,from:normFP(myFingerprint),message:Array.from(new TextEncoder().encode(JSON.stringify(cbMsg)))})});
|
||||||
|
addSys('Sent: ' + btn.text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rowDiv.appendChild(btnEl);
|
||||||
|
}
|
||||||
|
kbdDiv.appendChild(rowDiv);
|
||||||
|
}
|
||||||
|
$messages.appendChild(kbdDiv);
|
||||||
|
$messages.scrollTop = $messages.scrollHeight;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (json.type === 'bot_edit') {
|
if (json.type === 'bot_edit') {
|
||||||
addSys('[bot updated message: ' + (json.text || '') + ']');
|
addSys('[bot updated message: ' + (json.text || '') + ']');
|
||||||
|
// Render inline keyboard if present
|
||||||
|
if (json.reply_markup && json.reply_markup.inline_keyboard) {
|
||||||
|
const kbdDiv = document.createElement('div');
|
||||||
|
kbdDiv.className = 'inline-kbd';
|
||||||
|
for (const row of json.reply_markup.inline_keyboard) {
|
||||||
|
const rowDiv = document.createElement('div');
|
||||||
|
rowDiv.className = 'inline-kbd-row';
|
||||||
|
for (const btn of row) {
|
||||||
|
const btnEl = document.createElement('button');
|
||||||
|
btnEl.className = 'kbd-btn';
|
||||||
|
btnEl.textContent = btn.text;
|
||||||
|
btnEl.onclick = async function() {
|
||||||
|
const cbData = btn.callback_data || btn.text;
|
||||||
|
const botFp = json.from ? normFP(json.from) : '';
|
||||||
|
if (botFp) {
|
||||||
|
const cbMsg = {type:'bot_message',id:Date.now().toString(),from:normFP(myFingerprint),from_name:myEthAddress||myFingerprint.slice(0,19),text:cbData,timestamp:Math.floor(Date.now()/1000),is_callback:true};
|
||||||
|
await fetch(SERVER+'/v1/messages/send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({to:botFp,from:normFP(myFingerprint),message:Array.from(new TextEncoder().encode(JSON.stringify(cbMsg)))})});
|
||||||
|
addSys('Sent: ' + btn.text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
rowDiv.appendChild(btnEl);
|
||||||
|
}
|
||||||
|
kbdDiv.appendChild(rowDiv);
|
||||||
|
}
|
||||||
|
$messages.appendChild(kbdDiv);
|
||||||
|
$messages.scrollTop = $messages.scrollHeight;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (json.type === 'bot_document') {
|
if (json.type === 'bot_document') {
|
||||||
|
|||||||
Reference in New Issue
Block a user