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