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:
@@ -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-v18';
|
||||
const CACHE = 'wz-v19';
|
||||
const SHELL = ['/', '/wasm/warzone_wasm.js', '/wasm/warzone_wasm_bg.wasm', '/icon.svg', '/manifest.json'];
|
||||
|
||||
self.addEventListener('install', e => {
|
||||
@@ -204,6 +204,12 @@ const WEB_HTML: &str = r##"<!DOCTYPE html>
|
||||
.incoming-call { animation: pulse 1.5s infinite; }
|
||||
@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) {
|
||||
.msg { font-size: 0.8em; }
|
||||
#chat-header input { width: 180px; }
|
||||
@@ -281,7 +287,7 @@ let pollTimer = null;
|
||||
let ws = null; // WebSocket connection
|
||||
let wasmReady = false;
|
||||
|
||||
const VERSION = '0.0.36';
|
||||
const VERSION = '0.0.37';
|
||||
let DEBUG = true; // toggle with /debug command
|
||||
|
||||
// ── Receipt tracking ──
|
||||
@@ -584,22 +590,67 @@ function connectWebSocket() {
|
||||
if (json.type === 'bot_message') {
|
||||
const botName = json.from_name || json.from || 'bot';
|
||||
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';
|
||||
addMsg('@' + botName, msgText, false, null, useHtml);
|
||||
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;
|
||||
}
|
||||
if (json.type === 'bot_edit') {
|
||||
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;
|
||||
}
|
||||
if (json.type === 'bot_document') {
|
||||
@@ -735,23 +786,67 @@ async function handleIncomingMessage(bytes) {
|
||||
if (json.type === 'bot_message') {
|
||||
const botName = json.from_name || json.from || 'bot';
|
||||
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';
|
||||
addMsg('@' + botName, msgText, false, null, useHtml);
|
||||
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;
|
||||
}
|
||||
if (json.type === 'bot_edit') {
|
||||
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;
|
||||
}
|
||||
if (json.type === 'bot_document') {
|
||||
|
||||
Reference in New Issue
Block a user