v0.0.17: fix /r reply in TUI, /p shortcut, /eth, /unalias

TUI fixes:
- /r and /reply now work: tracks last_dm_peer from received messages
- /r switches peer to last DM sender, then type normally
- /p @alias works as shortcut for /peer @alias
- /eth shows Ethereum address in TUI
- /unalias removes your alias

Web fixes:
- /p @alias and /peer @alias resolve and set peer
- /r and /reply work (switch to last DM sender)
- /unalias removes alias
- /admin-unalias <alias> <password> for admin removal
- File download now shows as clickable link (not auto-download)

Server:
- POST /v1/alias/unregister — remove own alias
- POST /v1/alias/admin-remove — admin removes any alias
- WARZONE_ADMIN_PASSWORD env var (default: "admin")

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-27 19:50:00 +04:00
parent f4eac7b2aa
commit a4405b4976
5 changed files with 35 additions and 16 deletions

View File

@@ -125,6 +125,12 @@ pub fn save_seed(seed: &Seed) -> anyhow::Result<()> {
Ok(())
}
/// Load raw seed bytes (for deriving eth address etc).
pub fn load_seed_raw() -> anyhow::Result<[u8; 32]> {
let seed = load_seed()?;
Ok(seed.0)
}
/// Load seed, decrypting if necessary.
pub fn load_seed() -> anyhow::Result<Seed> {
let path = seed_path();

View File

@@ -52,7 +52,7 @@ pub struct App {
pub peer_fp: Option<String>,
pub server_url: String,
pub should_quit: bool,
pub last_dm_peer: Option<String>,
pub last_dm_peer: Arc<Mutex<Option<String>>>,
/// Track receipt status for messages we sent, keyed by message ID.
pub receipts: Arc<Mutex<HashMap<String, ReceiptStatus>>>,
/// Pending incoming file transfers, keyed by file ID.
@@ -112,7 +112,7 @@ impl App {
peer_fp,
server_url,
should_quit: false,
last_dm_peer: None,
last_dm_peer: Arc::new(Mutex::new(None)),
receipts: Arc::new(Mutex::new(HashMap::new())),
pending_files: Arc::new(Mutex::new(HashMap::new())),
}
@@ -285,9 +285,17 @@ impl App {
}
return;
}
if text == "/eth" {
// Show ethereum address from seed
if let Ok(seed) = crate::keystore::load_seed_raw() {
let eth = warzone_protocol::ethereum::derive_eth_identity(&seed);
self.add_message(ChatLine { sender: "system".into(), text: format!("ETH: {}", eth.address.to_checksum()), is_system: true, is_self: false, message_id: None });
}
return;
}
if text == "/r" || text == "/reply" {
// Just switch to last DM peer
if let Some(ref peer) = self.last_dm_peer.clone() {
let last = self.last_dm_peer.lock().unwrap().clone();
if let Some(ref peer) = last {
self.peer_fp = Some(peer.clone());
self.add_message(ChatLine { sender: "system".into(), text: format!("→ switched to {}", &peer[..peer.len().min(16)]), is_system: true, is_self: false, message_id: None });
} else {
@@ -1109,9 +1117,10 @@ fn process_incoming(
pending_files: &Arc<Mutex<HashMap<String, PendingFileTransfer>>>,
our_fp: &str,
client: &ServerClient,
last_dm_peer: &Arc<Mutex<Option<String>>>,
) {
match bincode::deserialize::<WireMessage>(raw) {
Ok(wire) => process_wire_message(wire, identity, db, messages, receipts, pending_files, our_fp, client),
Ok(wire) => process_wire_message(wire, identity, db, messages, receipts, pending_files, our_fp, client, last_dm_peer),
Err(_) => {}
}
}
@@ -1125,6 +1134,7 @@ fn process_wire_message(
pending_files: &Arc<Mutex<HashMap<String, PendingFileTransfer>>>,
our_fp: &str,
client: &ServerClient,
last_dm_peer: &Arc<Mutex<Option<String>>>,
) {
match wire {
WireMessage::KeyExchange {
@@ -1161,6 +1171,7 @@ fn process_wire_message(
Ok(plaintext) => {
let text = String::from_utf8_lossy(&plaintext).to_string();
let _ = db.save_session(&sender_fp, &state);
*last_dm_peer.lock().unwrap() = Some(sender_fingerprint.clone());
messages.lock().unwrap().push(ChatLine {
sender: sender_fingerprint[..sender_fingerprint.len().min(12)].to_string(),
text,
@@ -1168,7 +1179,6 @@ fn process_wire_message(
is_self: false,
message_id: None,
});
// Send delivery receipt
send_receipt(our_fp, &sender_fingerprint, &id, ReceiptType::Delivered, client);
}
Err(_) => {}
@@ -1191,6 +1201,7 @@ fn process_wire_message(
Ok(plaintext) => {
let text = String::from_utf8_lossy(&plaintext).to_string();
let _ = db.save_session(&sender_fp, &state);
*last_dm_peer.lock().unwrap() = Some(sender_fingerprint.clone());
messages.lock().unwrap().push(ChatLine {
sender: sender_fingerprint[..sender_fingerprint.len().min(12)].to_string(),
text,
@@ -1414,6 +1425,7 @@ pub async fn poll_loop(
identity: IdentityKeyPair,
db: Arc<LocalDb>,
client: ServerClient,
last_dm_peer: Arc<Mutex<Option<String>>>,
) {
let fp = normfp(&our_fp);
@@ -1439,7 +1451,7 @@ pub async fn poll_loop(
while let Some(Ok(msg)) = read.next().await {
if let tokio_tungstenite::tungstenite::Message::Binary(data) = msg {
process_incoming(&data, &identity, &db, &messages, &receipts, &pending_files, &our_fp, &client);
process_incoming(&data, &identity, &db, &messages, &receipts, &pending_files, &our_fp, &client, &last_dm_peer);
}
}
@@ -1460,7 +1472,7 @@ pub async fn poll_loop(
Err(_) => continue,
};
for raw in &raw_msgs {
process_incoming(raw, &identity, &db, &messages, &receipts, &pending_files, &our_fp, &client);
process_incoming(raw, &identity, &db, &messages, &receipts, &pending_files, &our_fp, &client, &last_dm_peer);
}
}
}
@@ -1487,12 +1499,13 @@ pub async fn run_tui(
let poll_messages = app.messages.clone();
let poll_receipts = app.receipts.clone();
let poll_pending_files = app.pending_files.clone();
let poll_last_dm = app.last_dm_peer.clone();
let poll_client = client.clone();
let poll_db = db.clone();
let poll_fp = our_fp.clone();
tokio::spawn(async move {
poll_loop(poll_messages, poll_receipts, poll_pending_files, poll_fp, poll_identity, poll_db, poll_client).await;
poll_loop(poll_messages, poll_receipts, poll_pending_files, poll_fp, poll_identity, poll_db, poll_client, poll_last_dm).await;
});
loop {

View File

@@ -237,7 +237,7 @@ let pollTimer = null;
let ws = null; // WebSocket connection
let wasmReady = false;
const VERSION = '0.0.16';
const VERSION = '0.0.17';
let DEBUG = true; // toggle with /debug command
// ── Receipt tracking ──