const $alerts = document.getElementById("alerts"); const $accounts = document.getElementById("accounts"); const $instance = document.getElementById("instance"); const $toast = document.getElementById("toast"); const $incomingPop = document.getElementById("incoming-pop"); const $incomingPopName = document.getElementById("incoming-pop-name"); const $incomingPopGo = document.getElementById("incoming-pop-go"); // Electron has no window.prompt(), so renames happen through an inline input. // `editing` survives re-renders so the field isn't wiped mid-typing. let editing = null; let lastState = { accounts: [], alerts: [] }; // Belt & braces alongside overflow-x:clip — nothing may ever scroll the // sidebar sideways (a focused input once did, which clipped all content). function resetScroll() { for (const el of [document.scrollingElement, document.body, $accounts]) { if (el && el.scrollLeft) el.scrollLeft = 0; } } /* ---------------- alerts ---------------- */ function renderAlerts(alerts) { $alerts.innerHTML = ""; if (!alerts.length) { const e = document.createElement("div"); e.className = "empty"; e.textContent = "No waiting chats"; $alerts.append(e); return; } for (const a of alerts) { const el = document.createElement("div"); el.className = "alert"; const b = document.createElement("b"); b.textContent = a.label; const s = document.createElement("span"); s.textContent = a.kind === "reply" ? a.text || "New message" : a.queue || "waiting"; el.append(b, s); el.onclick = () => api.focusAlert(a.accountId); $alerts.append(el); } } /* ---------------- rename editor ---------------- */ function startEdit(id) { editing = id; render(lastState); } function commitEdit(id, value) { if (editing !== id) return; editing = null; api.renameAccount(id, value); } // Native popups (Chromium autofill, macOS spelling/substitution) anchor to text // inputs and float OVER the window — an orphaned one is what once appeared as a // white box across the footer. Disable everything that can summon one. function plainInput() { const input = document.createElement("input"); input.autocomplete = "off"; input.spellcheck = false; input.setAttribute("autocapitalize", "off"); input.setAttribute("autocorrect", "off"); return input; } function buildEditor(a) { const input = plainInput(); input.className = "rename"; input.value = a.label || a.agentName || ""; input.placeholder = a.agentName || "Account name"; input.onclick = (e) => e.stopPropagation(); input.onkeydown = (e) => { e.stopPropagation(); if (e.key === "Enter") commitEdit(a.id, input.value); if (e.key === "Escape") { editing = null; render(lastState); } }; input.onblur = () => commitEdit(a.id, input.value); setTimeout(() => { input.focus({ preventScroll: true }); // focus scrolling is what broke alignment input.select(); resetScroll(); }, 0); return input; } /* ---------------- account rows ---------------- */ function dotClass(a) { if (a.loading) return "dot loading"; if (a.stayOut && a.signedIn !== true) return "dot off"; // deliberate sign-out, not an error if (a.signedIn === false) return "dot err"; if (a.stateType === "routable") return "dot ready"; if (a.stateType === "offline") return "dot off"; if (a.agentName) return "dot ready"; return "dot"; } function stateText(a) { if (a.stayOut && a.signedIn !== true) return "signed out"; if (a.signedIn === false) return "sign in"; return a.stateName || ""; } function buildRow(a, activeId) { const el = document.createElement("div"); el.className = "acct" + (a.id === activeId ? " active" : ""); el.onclick = () => api.activate(a.id); el.oncontextmenu = (e) => { e.preventDefault(); api.showAccountMenu(a.id); }; const dot = document.createElement("span"); dot.className = dotClass(a); el.append(dot); if (editing === a.id) { el.append(buildEditor(a)); return el; } const label = document.createElement("span"); label.className = "label"; label.textContent = a.display; if (!a.label && !a.agentName) label.classList.add("placeholder"); label.title = (a.agentName ? `Signed in as ${a.agentName}` : "Not signed in yet") + ` — ${a.instanceLabel || ""}`; label.ondblclick = (e) => { e.stopPropagation(); startEdit(a.id); }; el.append(label); // Which Connect instance this account lives on (US / EU). if (a.instanceShort) { const inst = document.createElement("span"); inst.className = "inst"; inst.textContent = a.instanceShort; inst.title = a.instanceLabel; el.append(inst); } const st = stateText(a); if (st) { const chip = document.createElement("span"); chip.className = "state"; chip.textContent = st; el.append(chip); } if (a.waiting) { const badge = document.createElement("span"); badge.className = "badge"; badge.textContent = a.waiting; el.append(badge); } const tools = document.createElement("span"); tools.className = "tools"; for (const [glyph, title, fn] of [ ["✎", "Rename", () => startEdit(a.id)], ["⟳", "Reload", () => api.reload(a.id)], ["✕", "Remove account", () => api.removeAccount(a.id)], ]) { const b = document.createElement("button"); b.textContent = glyph; b.title = title; b.onclick = (e) => { e.stopPropagation(); fn(); }; tools.append(b); } el.append(tools); return el; } /* ---------------- render ---------------- */ function renderIncomingPop(alerts) { const waiting = (alerts || []).filter((a) => a.kind !== "reply"); if (!waiting.length) { $incomingPop.hidden = true; $incomingPopGo.onclick = null; return; } $incomingPop.hidden = false; $incomingPopName.textContent = waiting.map((a) => a.label).join(" · "); $incomingPopGo.onclick = (e) => { e.stopPropagation(); api.focusAlert(waiting[0].accountId); }; } function render(state) { lastState = state; $instance.textContent = (state.instances || []).map((i) => i.label).join(" · "); renderAlerts(state.alerts); renderIncomingPop(state.alerts); $accounts.innerHTML = ""; for (const a of state.accounts) $accounts.append(buildRow(a, state.activeId)); resetScroll(); } api.onState(render); api.onError(({ message }) => console.warn("account error:", message)); api.onMenuAction((a) => { if (a.type === "rename") startEdit(a.id); if (a.type === "creds") openCreds(); }); /* ---------------- toast ---------------- */ let toastTimer; api.onToast(({ text }) => { $toast.textContent = text; $toast.hidden = false; clearTimeout(toastTimer); toastTimer = setTimeout(() => ($toast.hidden = true), 6000); }); /* ---------------- top-level controls ---------------- */ // Every account belongs to one Connect instance, so adding starts by picking // which one — then straight into naming, as before. const $add = document.getElementById("add"); $add.onclick = async (e) => { e.stopPropagation(); const instances = lastState.instances || []; const create = async (instId) => { const id = await api.addAccount(instId); if (id) startEdit(id); }; if (instances.length <= 1) return void create(instances[0]?.id); const instId = await api.showInstanceMenu(); if (instId) create(instId); }; document.getElementById("all-available").onclick = () => api.setStatusAll("routable"); document.getElementById("all-offline").onclick = () => api.setStatusAll("offline"); document.getElementById("all-signin").onclick = () => api.signInAll(); document.getElementById("all-signout").onclick = () => api.signOutAll(); /* ---------------- saved sign-ins panel ---------------- */ const $creds = document.getElementById("creds"); function closeCreds() { $creds.hidden = true; $creds.innerHTML = ""; // never leave typed passwords sitting in the DOM } function showToast(text, ms = 6000) { $toast.textContent = text; $toast.hidden = false; clearTimeout(toastTimer); toastTimer = setTimeout(() => ($toast.hidden = true), ms); } async function openCreds() { const rows = await api.credsList(); $creds.innerHTML = ""; const head = document.createElement("div"); head.className = "creds-head"; head.textContent = "Saved sign-ins"; const note = document.createElement("div"); note.className = "creds-note"; note.textContent = "Stored encrypted on this computer only (OS keychain). Filled only on the official " + "Amazon Connect sign-in page for each account's instance. Leave a password blank to keep " + "the one already saved."; $creds.append(head, note); const cleared = new Set(); const inputs = new Map(); // id -> {u, p, status} // Collects one row (or every row) into the shape creds:save expects. const entryFor = (id) => { const { u, p } = inputs.get(id); return { id, username: u.value, password: p.value || null, clear: cleared.has(id) }; }; const allEntries = () => [...inputs.keys()].map(entryFor); const flash = (id, text) => { const s = inputs.get(id)?.status; if (!s) return; s.textContent = text; clearTimeout(s._t); s._t = setTimeout(() => (s.textContent = ""), 4000); }; // After saving, the password box is empty again but a secret now exists — // reflect that so the row doesn't look unsaved. const markSaved = (id) => { const { p } = inputs.get(id); p.value = ""; p.placeholder = "•••••• saved — blank keeps it"; }; const saveOne = async (id) => { const res = await api.credsSave([entryFor(id)]); if (!res?.ok) return showToast(res?.error || "Saving failed.", 8000); if (cleared.has(id)) { cleared.delete(id); inputs.get(id).row.classList.remove("cleared"); inputs.get(id).p.placeholder = "password"; flash(id, "forgotten"); } else { markSaved(id); flash(id, "saved ✓"); } return true; }; for (const r of rows) { const row = document.createElement("div"); row.className = "creds-row"; const name = document.createElement("div"); name.className = "creds-name"; name.textContent = `${r.display} · ${r.instanceShort}`; const u = plainInput(); u.className = "rename"; u.placeholder = "login username / email"; u.value = r.username; const p = plainInput(); p.className = "rename"; p.type = "password"; p.placeholder = r.hasPassword ? "•••••• saved — blank keeps it" : "password"; const bar = document.createElement("div"); bar.className = "creds-rowbar"; const bSave = document.createElement("button"); bSave.type = "button"; bSave.textContent = "Save"; bSave.title = "Save this account's sign-in"; bSave.onclick = () => saveOne(r.id); const bSignIn = document.createElement("button"); bSignIn.type = "button"; bSignIn.className = "primary"; bSignIn.textContent = "Sign in"; bSignIn.title = "Save, then sign this account in with these credentials"; bSignIn.onclick = async () => { if (!(await saveOne(r.id))) return; flash(r.id, "signing in…"); api.signIn(r.id); }; const forget = document.createElement("button"); forget.type = "button"; forget.className = "creds-forget"; forget.textContent = "forget"; forget.title = "Mark this saved sign-in for deletion, then Save"; forget.onclick = () => { if (cleared.has(r.id)) { cleared.delete(r.id); row.classList.remove("cleared"); } else { cleared.add(r.id); row.classList.add("cleared"); } }; const status = document.createElement("span"); status.className = "creds-status"; bar.append(bSave, bSignIn, status, forget); inputs.set(r.id, { u, p, status, row }); const line = document.createElement("div"); line.className = "creds-line"; line.append(u, p); row.append(name, line, bar); $creds.append(row); } const actions = document.createElement("div"); actions.className = "creds-actions"; const saveAll = document.createElement("button"); saveAll.type = "button"; saveAll.textContent = "Save all"; saveAll.onclick = async () => { const res = await api.credsSave(allEntries()); if (!res?.ok) return showToast(res?.error || "Saving failed.", 8000); for (const id of inputs.keys()) { if (cleared.has(id)) continue; markSaved(id); } cleared.clear(); for (const { row } of inputs.values()) row.classList.remove("cleared"); showToast(`Saved sign-ins for ${res.saved} account(s).`); }; const signInAll = document.createElement("button"); signInAll.type = "button"; signInAll.className = "primary"; signInAll.textContent = "Sign in all"; signInAll.onclick = async () => { const res = await api.credsSave(allEntries()); if (!res?.ok) return showToast(res?.error || "Saving failed.", 8000); closeCreds(); api.signInAll(); }; const close = document.createElement("button"); close.type = "button"; close.textContent = "Close"; close.onclick = closeCreds; actions.append(saveAll, signInAll, close); $creds.append(actions); $creds.hidden = false; } document.getElementById("signins").onclick = openCreds; addEventListener("keydown", (e) => { if (e.key === "Escape" && !$creds.hidden) closeCreds(); }); api.getState();