v0.0.30: markdown rendering in web, fix scrolling

Web:
- Markdown renderer: **bold**, *italic*, `code`, ```code blocks```,
  # headers, [links](url), > blockquotes, - lists
- All message text rendered as markdown (bot messages look great now)
- Fixed scroll: overflow-y: scroll + min-height: 0 on messages container
- CSS for code blocks, pre, headers, blockquotes, lists
- Styled: code=cyan bg, pre=dark bg+border, bold=white, italic=amber

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-29 12:36:00 +04:00
parent 8b37bd4323
commit 6fee73fc4d
4 changed files with 49 additions and 11 deletions

10
warzone/Cargo.lock generated
View File

@@ -2956,7 +2956,7 @@ dependencies = [
[[package]]
name = "warzone-client"
version = "0.0.29"
version = "0.0.30"
dependencies = [
"anyhow",
"argon2",
@@ -2989,7 +2989,7 @@ dependencies = [
[[package]]
name = "warzone-mule"
version = "0.0.29"
version = "0.0.30"
dependencies = [
"anyhow",
"clap",
@@ -2998,7 +2998,7 @@ dependencies = [
[[package]]
name = "warzone-protocol"
version = "0.0.29"
version = "0.0.30"
dependencies = [
"base64",
"bincode",
@@ -3023,7 +3023,7 @@ dependencies = [
[[package]]
name = "warzone-server"
version = "0.0.29"
version = "0.0.30"
dependencies = [
"anyhow",
"axum",
@@ -3053,7 +3053,7 @@ dependencies = [
[[package]]
name = "warzone-wasm"
version = "0.0.29"
version = "0.0.30"
dependencies = [
"base64",
"bincode",

View File

@@ -9,7 +9,7 @@ members = [
]
[workspace.package]
version = "0.0.29"
version = "0.0.30"
edition = "2021"
license = "MIT"
rust-version = "1.75"

View File

@@ -1,6 +1,6 @@
[package]
name = "warzone-protocol"
version = "0.0.29"
version = "0.0.30"
edition = "2021"
license = "MIT"
description = "Core crypto & wire protocol for featherChat (Warzone messenger)"

View File

@@ -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-v11';
const CACHE = 'wz-v12';
const SHELL = ['/', '/wasm/warzone_wasm.js', '/wasm/warzone_wasm_bg.wasm', '/icon.svg', '/manifest.json'];
self.addEventListener('install', e => {
@@ -154,8 +154,18 @@ const WEB_HTML: &str = r##"<!DOCTYPE html>
#chat-header input { background: #1a1a2e; border: 1px solid #333; color: #e6a23c; padding: 2px 6px;
border-radius: 3px; font-family: inherit; font-size: 0.85em; width: 280px; }
#messages { flex: 1; overflow-y: auto; padding: 8px 10px; -webkit-overflow-scrolling: touch; }
#messages { flex: 1; overflow-y: scroll; padding: 8px 10px; -webkit-overflow-scrolling: touch; min-height: 0; }
.msg { padding: 2px 0; font-size: 0.85em; white-space: pre-wrap; word-wrap: break-word; }
.msg code { background: #1a1a3e; padding: 1px 4px; border-radius: 3px; font-size: 0.95em; color: #4fc3f7; }
.msg pre { background: #0d0d20; padding: 8px; border-radius: 4px; margin: 4px 0; overflow-x: auto; border: 1px solid #222; }
.msg pre code { background: none; padding: 0; }
.msg strong, .msg b { color: #fff; }
.msg em, .msg i { color: #e6a23c; }
.msg a { color: #4fc3f7; }
.msg blockquote { border-left: 3px solid #444; padding-left: 8px; color: #888; margin: 4px 0; }
.msg ul, .msg ol { padding-left: 20px; margin: 4px 0; }
.msg h1, .msg h2, .msg h3 { color: #fff; margin: 6px 0 2px; }
.msg h1 { font-size: 1.2em; } .msg h2 { font-size: 1.1em; } .msg h3 { font-size: 1em; }
.msg .ts { color: #333; margin-right: 4px; }
.msg .from-self { color: #4ade80; font-weight: bold; }
.msg .from-sys { color: #5e9ca0; font-style: italic; }
@@ -241,7 +251,7 @@ let pollTimer = null;
let ws = null; // WebSocket connection
let wasmReady = false;
const VERSION = '0.0.29';
const VERSION = '0.0.30';
let DEBUG = true; // toggle with /debug command
// ── Receipt tracking ──
@@ -737,6 +747,34 @@ function esc(s) {
return d.innerHTML;
}
function renderMd(text) {
let s = esc(text);
// Code blocks: ```...```
s = s.replace(/```(\w*)\n?([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
// Inline code: `...`
s = s.replace(/`([^`]+)`/g, '<code>$1</code>');
// Bold: **...**
s = s.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
// Italic: *...*
s = s.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, '<em>$1</em>');
// Headers: ### ... (at line start)
s = s.replace(/^### (.+)$/gm, '<h3>$1</h3>');
s = s.replace(/^## (.+)$/gm, '<h2>$1</h2>');
s = s.replace(/^# (.+)$/gm, '<h1>$1</h1>');
// Links: [text](url)
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
// Blockquotes: > ...
s = s.replace(/^&gt; (.+)$/gm, '<blockquote>$1</blockquote>');
// Unordered lists: - ...
s = s.replace(/^- (.+)$/gm, '<li>$1</li>');
s = s.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>');
// Line breaks
s = s.replace(/\n/g, '<br>');
// Clean up br inside pre
s = s.replace(/<pre><code>([\s\S]*?)<\/code><\/pre>/g, (m, code) => '<pre><code>' + code.replace(/<br>/g, '\n') + '</code></pre>');
return s;
}
const PEER_COLORS = ['#e6a23c','#f56c9d','#67c7eb','#b39ddb','#ff8a65','#81c784','#ce93d8','#4fc3f7','#ffb74d','#aed581','#f06292','#4dd0e1'];
function peerColor(name) {
@@ -813,7 +851,7 @@ function addMsg(from, text, isSelf, messageId, rawHtml) {
const status = (sentMsgReceipts[messageId] && sentMsgReceipts[messageId].status) || 'sent';
receiptHtml = ' <span class="receipt" style="color:' + receiptColor(status) + '"> ' + receiptIndicator(status) + '</span>';
}
const bodyHtml = rawHtml ? text : makeAddressClickable(esc(text));
const bodyHtml = rawHtml ? text : makeAddressClickable(renderMd(text));
d.innerHTML = '<span class="ts">' + ts() + '</span> ' + lock + '<span style="color:' + color + ';font-weight:bold">' + makeAddressClickable(esc(from)) + '</span>: ' + bodyHtml + receiptHtml;
// Attach click handler for .addr spans
d.querySelectorAll('.addr').forEach(el => {