From fc3f79379e30d9c11381c5c311270c77c724abda Mon Sep 17 00:00:00 2001 From: maverick Date: Wed, 22 Jul 2026 02:20:14 +1000 Subject: [PATCH] Fix New chat button: replace window.prompt with in-app modal - window.prompt/alert are suppressed in Brave and installed PWAs, so the button appeared to do nothing. Replace with a proper modal (inline error, Enter/Escape, click-outside to close). - Mobile nav: sidebar was fully hidden (button unreachable); now sidebar shows by default and a back button toggles between list and thread. - Bump SW cache v1->v2 so the new shell actually ships past the cache. Co-Authored-By: Claude Opus 4.8 (1M context) --- public/index.html | 25 ++++++++++++- public/js/app.js | 90 ++++++++++++++++++++++++++++++++++++++--------- public/sw.js | 2 +- 3 files changed, 98 insertions(+), 19 deletions(-) diff --git a/public/index.html b/public/index.html index d104441..7a44cbb 100644 --- a/public/index.html +++ b/public/index.html @@ -69,6 +69,7 @@ linear-gradient(rgba(11,20,26,.95), rgba(11,20,26,.95)); } .empty { margin: auto; color: var(--muted); } .chat-head { display: flex; gap: 12px; align-items: center; padding: 12px 16px; background: var(--panel); border-bottom: 1px solid #0a0f13; } + .back-btn { display: none; background: transparent; color: var(--text); font-size: 26px; line-height: 1; padding: 0 6px; } .thread { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 6px; } .bubble { max-width: 65%; padding: 8px 10px 6px; border-radius: 10px; font-size: 14.5px; line-height: 1.35; position: relative; word-wrap: break-word; } .bubble.in { background: var(--bubble-in); align-self: flex-start; border-top-left-radius: 2px; } @@ -84,10 +85,32 @@ .attach { cursor: pointer; font-size: 20px; user-select: none; } .send { background: var(--accent); color: #04140f; font-weight: 600; border-radius: 20px; } + /* ---- modal ---- */ + .modal-overlay { + position: fixed; inset: 0; background: rgba(0,0,0,.6); + display: grid; place-items: center; z-index: 100; padding: 20px; + } + .modal-card { + background: var(--panel); border-radius: 14px; padding: 24px; + width: 100%; max-width: 360px; display: flex; flex-direction: column; gap: 14px; + box-shadow: 0 20px 60px rgba(0,0,0,.5); + } + .modal-title { margin: 0; font-size: 18px; } + .modal-input { + padding: 12px 14px; border-radius: 10px; border: 1px solid var(--panel-2); + background: var(--panel-2); color: var(--text); font-size: 15px; + } + .modal-input:focus { outline: 2px solid var(--accent); } + .modal-err { color: #f15c6d; font-size: 13px; min-height: 16px; } + .modal-actions { display: flex; gap: 10px; justify-content: flex-end; } + .modal-actions button { padding: 10px 16px; } + @media (max-width: 720px) { .shell { grid-template-columns: 1fr; } - .sidebar { display: none; } + /* Sidebar shows by default; hide it once a chat is open so the thread is full-width. */ .shell.chat-open .sidebar { display: none; } + .shell:not(.chat-open) .main { display: none; } + .back-btn { display: block; } } diff --git a/public/js/app.js b/public/js/app.js index f74a8c2..9a58fca 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -149,33 +149,89 @@ async function refreshContacts() { } } -async function startNewChat() { - const username = prompt("Username to message:"); - if (!username) return; - try { - const user = await api.lookup(username.trim().toLowerCase()); - const contact = { - userId: user.userId, - username: user.username, - displayName: user.displayName, - deviceIds: [], - }; - await contacts.put(contact); - await refreshContacts(); - await openChat(contact); - } catch (ex) { - alert("Could not find user: " + ex.message); - } +function startNewChat() { + openModal({ + title: "New chat", + placeholder: "Enter a username", + action: "Start chat", + onSubmit: async (value, setError, close) => { + const uname = value.trim().toLowerCase(); + if (!uname) return setError("Enter a username"); + if (uname === state.me.username) return setError("That's you 🙂"); + try { + const user = await api.lookup(uname); + const contact = { + userId: user.userId, + username: user.username, + displayName: user.displayName, + deviceIds: [], + }; + await contacts.put(contact); + await refreshContacts(); + close(); + await openChat(contact); + } catch (ex) { + setError(ex.message === "not found" ? "No user found: @" + uname : ex.message); + } + }, + }); +} + +// Lightweight in-app modal (replaces window.prompt, which Brave/PWAs suppress). +function openModal({ title, placeholder, action, onSubmit }) { + const input = el("input", { className: "modal-input", placeholder, autocomplete: "off" }); + const err = el("div", { className: "modal-err" }); + const cancel = el("button", { type: "button", className: "ghost", textContent: "Cancel" }); + const submit = el("button", { type: "button", className: "primary", textContent: action }); + const card = el("div", { className: "modal-card" }, + el("h2", { className: "modal-title", textContent: title }), + input, + err, + el("div", { className: "modal-actions" }, cancel, submit) + ); + const overlay = el("div", { className: "modal-overlay" }, card); + document.body.append(overlay); + setTimeout(() => input.focus(), 0); + + const close = () => overlay.remove(); + const setError = (m) => { err.textContent = m; }; + const go = async () => { + submit.disabled = true; + setError(""); + try { + await onSubmit(input.value, setError, close); + } finally { + submit.disabled = false; + } + }; + cancel.onclick = close; + submit.onclick = go; + overlay.onclick = (e) => { if (e.target === overlay) close(); }; + input.onkeydown = (e) => { + if (e.key === "Enter") { e.preventDefault(); go(); } + if (e.key === "Escape") close(); + }; } // ---------- chat view ---------- async function openChat(contact) { state.activeContact = contact; + $(".shell")?.classList.add("chat-open"); const main = $("#main"); main.innerHTML = ""; + const back = el("button", { + className: "back-btn", + textContent: "‹", + title: "Back", + onclick: () => { + state.activeContact = null; + $(".shell")?.classList.remove("chat-open"); + }, + }); const head = el("div", { className: "chat-head" }); head.append( + back, el("div", { className: "avatar", textContent: (contact.displayName || contact.username)[0].toUpperCase() }), el("div", {}, el("div", { className: "chat-name", textContent: contact.displayName || contact.username }), diff --git a/public/sw.js b/public/sw.js index 99c7109..84ae8b9 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1,7 +1,7 @@ // Service worker: offline app shell + Web Push wake-up. // It never touches message plaintext — decryption happens only in the page. -const CACHE = "cipher-v1"; +const CACHE = "cipher-v2"; const SHELL = [ "/", "/index.html",