/*
* Conversations page, as a router view. Always-open search over titles AND
* message contents (GET /api/conversations?q=), a per-row ⋮ menu, and a bulk
* select/delete mode. Mounted into the shell's #viewOutlet — the sidebar, dialogs,
* and auth are already up, so this view renders only its main region and routes
* navigation through ctx.
*
* Converted from the standalone conversations.js: client-owned markup, DOM lookups
* scoped to `root` via data-* hooks (so the hidden chat shell's ids never
* collide), soft-nav via ctx.navigate, and a destroy() that clears the search
* timer and the global `selecting` body class.
*/
import { convRow, setRowState } from "../convrow.js";
var MENU_SVG =
' ';
var SEARCH_SVG =
' ';
var CLEAR_SVG =
' ';
var TEMPLATE =
'" +
'
";
var LIST_CACHE = "kloe:conversations";
function readListCache() {
try {
return JSON.parse(sessionStorage.getItem(LIST_CACHE) || "null");
} catch (_) {
return null;
}
}
function writeListCache(list) {
try {
// Without the live state: it goes stale the moment the page closes, and a
// cached row that claims to be generating would be a lie until the fetch
// lands. (See sidebar.js — same reasoning, same shape.)
var plain = (list || []).map(function (c) {
return Object.assign({}, c, { state: "idle" });
});
sessionStorage.setItem(LIST_CACHE, JSON.stringify(plain));
} catch (_) {}
}
export function mount(root, _params, ctx) {
root.innerHTML = TEMPLATE;
var q = function (sel) {
return root.querySelector(sel);
};
var rows = q("[data-rows]"),
search = q("[data-search]"),
clearSearch = q("[data-clearsearch]");
var selectBtn = q("[data-selectbtn]"),
deleteBtn = q("[data-deletebtn]"),
selCount = q("[data-selcount]"),
selectAllBtn = q("[data-selectall]"),
newBtn = q("[data-newbtn]");
var conversations = []; // full list (for the sidebar recents + unfiltered rows)
var selectMode = false;
var searchTimer = null,
searchSeq = 0;
q("[data-menu]").addEventListener("click", function () {
ctx.toggleRail();
});
function checks() {
return Array.prototype.slice.call(rows.querySelectorAll(".chatcheck"));
}
function selectedIds() {
return checks()
.filter(function (c) {
return c.checked;
})
.map(function (c) {
return c.closest(".chatrow").dataset.id;
});
}
function allChecked() {
var cs = checks();
return (
cs.length > 0 &&
cs.every(function (c) {
return c.checked;
})
);
}
function updateDeleteBtn() {
var n = selectedIds().length;
deleteBtn.hidden = n === 0;
deleteBtn.textContent = "Delete";
selCount.textContent = n + " selected";
selectAllBtn.textContent = allChecked() ? "Deselect all" : "Select all";
}
function setSelectMode(on) {
selectMode = on;
document.body.classList.toggle("selecting", on);
selectBtn.textContent = on ? "Cancel" : "Select";
newBtn.hidden = on; // "New" is out of the way while selecting
selectAllBtn.hidden = !on; // "Select all" only makes sense while selecting
if (!on)
checks().forEach(function (c) {
c.checked = false;
});
updateDeleteBtn();
}
async function reloadAll() {
await loadSidebar();
loadMain(search.value.trim());
}
async function deleteIds(ids) {
if (!ids.length) return;
var plural = ids.length > 1 ? "s" : "";
var ok = await ctx.dialogs.confirm({
title: "Delete " + ids.length + " conversation" + plural + "?",
body: "This can't be undone.",
ok: "Delete",
danger: true,
});
if (!ok) return;
await Promise.all(
ids.map(function (id) {
return fetch("/api/conversations/" + encodeURIComponent(id), { method: "DELETE" }).catch(
function () {},
);
}),
);
setSelectMode(false);
reloadAll();
}
function renderRows(list, query) {
rows.innerHTML = "";
if (!list.length) {
var e = document.createElement("div");
e.className = "chatsempty";
e.textContent = query ? "No conversations match “" + query + "”" : "No conversations yet";
rows.appendChild(e);
return;
}
list.forEach(function (c) {
rows.appendChild(
convRow(c, {
dialogs: ctx.dialogs,
reload: reloadAll,
snippet: true,
selectable: true,
onCheck: updateDeleteBtn,
onClick: function (c, ref) {
if (selectMode) {
ref.checkbox.checked = !ref.checkbox.checked;
updateDeleteBtn();
} else ctx.navigate("/c/" + encodeURIComponent(c.id));
},
extra: function (_c, ref) {
return [
{
label: "Select",
onClick: function () {
if (!selectMode) setSelectMode(true);
ref.checkbox.checked = true;
updateDeleteBtn();
},
},
];
},
}),
);
});
}
async function loadSidebar() {
try {
var r = await fetch("/api/conversations");
conversations = (await r.json()).conversations || [];
writeListCache(conversations);
} catch (_) {
return; // a blip shouldn't blank the list; keep what we last had
}
ctx.sidebar.render(conversations);
}
// Conversations start and finish generating while this page just sits there,
// so re-read the list on a slow beat. Only the state is repainted: rebuilding
// the rows would drop select-mode checkboxes and fight the search results.
async function refreshStates() {
await loadSidebar();
var state = Object.create(null);
conversations.forEach(function (c) {
state[c.id] = c.state;
});
Array.prototype.forEach.call(rows.children, function (row) {
if (row.dataset.id) setRowState(row, state[row.dataset.id]);
});
}
async function loadMain(query) {
if (!query) {
renderRows(conversations, "");
return;
}
var mine = ++searchSeq;
try {
var r = await fetch("/api/conversations?q=" + encodeURIComponent(query));
var list = (await r.json()).conversations || [];
if (mine === searchSeq) renderRows(list, query);
} catch (_) {
if (mine === searchSeq)
rows.innerHTML = 'Failed to load conversations
';
}
}
selectBtn.addEventListener("click", function () {
setSelectMode(!selectMode);
});
selectAllBtn.addEventListener("click", function () {
var check = !allChecked();
checks().forEach(function (c) {
c.checked = check;
});
updateDeleteBtn();
});
deleteBtn.addEventListener("click", function () {
deleteIds(selectedIds());
});
function syncClear() {
clearSearch.hidden = search.value.length === 0;
}
search.addEventListener("input", function () {
syncClear();
clearTimeout(searchTimer);
var query = search.value.trim();
searchTimer = setTimeout(function () {
loadMain(query);
}, 160);
});
clearSearch.addEventListener("click", function () {
search.value = "";
syncClear();
search.focus();
loadMain("");
});
// Paint cached rows immediately, then revalidate; the sidebar refresh overwrites
// once /api/conversations lands.
var sidebarLoaded = loadSidebar();
var cached = readListCache();
if (cached && cached.length) {
conversations = cached;
loadMain("");
}
sidebarLoaded.then(function () {
loadMain("");
});
search.focus();
var POLL_MS = 5000;
var pollTimer = setInterval(function () {
if (!document.hidden) void refreshStates();
}, POLL_MS);
return {
destroy: function () {
clearInterval(pollTimer);
clearTimeout(searchTimer);
searchSeq++; // invalidate any in-flight search render
document.body.classList.remove("selecting");
},
};
}