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) <noreply@anthropic.com>
This commit is contained in:
maverick 2026-07-22 02:20:14 +10:00
parent afe4fa08f8
commit fc3f79379e
3 changed files with 98 additions and 19 deletions

View file

@ -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; }
}
</style>
</head>

View file

@ -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 }),

View file

@ -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",