").text(s == null ? "" : String(s)).html(); };
+ var vlId = function() { return $("#matrix-vl-overlay").data("matrixNodeId"); };
+ var vlCurId = function() { return $("#matrix-vl-overlay").data("vlCurrentId"); };
+ var vlCall = function(body) {
+ return $.ajax({
+ url: "matrix-chat/verification", type: "POST",
+ contentType: "application/json", data: JSON.stringify(body),
+ });
+ };
+ var vlClearTimers = function() {
+ if(vlListTimer) { clearInterval(vlListTimer); vlListTimer = null; }
+ if(vlTickTimer) { clearInterval(vlTickTimer); vlTickTimer = null; }
+ if(vlDetailTimer) { clearInterval(vlDetailTimer); vlDetailTimer = null; }
+ };
+ var vlDur = function(ms) {
+ var s = Math.max(0, Math.round(ms / 1000));
+ if(s < 60) { return s + "s"; }
+ var m = Math.floor(s / 60), r = s % 60;
+ return m + "m " + (r < 10 ? "0" : "") + r + "s";
+ };
+ var vlState = function(icon, color, html) {
+ $("#matrix-vl-d-state").html('
');
+ };
+
+ var vlTick = function() {
+ var now = Date.now();
+ $("#matrix-vl-items .matrix-vl-item").each(function() {
+ var $it = $(this);
+ var seen = parseInt($it.attr("data-seen"), 10);
+ var expires = $it.attr("data-expires");
+ $it.find(".matrix-vl-age").text(isNaN(seen) ? "" : "age " + vlDur(now - seen));
+ if(expires) {
+ var left = parseInt(expires, 10) - now;
+ $it.find(".matrix-vl-exp").text(left > 0 ? "expires in " + vlDur(left) : "expired");
+ }
+ });
+ };
+ var vlRenderList = function(data) {
+ if(data.result !== "ok") {
+ $("#matrix-vl-items").html('
');
+ $("#matrix-vl-empty,#matrix-vl-more").hide();
+ return;
+ }
+ var now = Date.now();
+ var $items = $("#matrix-vl-items").empty();
+ (data.verifications || []).forEach(function(v) {
+ var typeLabel = v.type === "room" ? "Room verification" : "Device verification";
+ var sub = "from " + vlEsc(v.userId || "unknown");
+ if(v.type === "device" && v.deviceId) { sub += " · device " + vlEsc(v.deviceId); }
+ else if(v.type === "room" && v.roomId) { sub += " · in room " + vlEsc(v.roomId); }
+ if(v.isSelfVerification) { sub += " · your own session"; }
+ $items.append($('
')
+ .attr("data-vid", v.verificationId)
+ .attr("data-seen", now - (v.ageMs || 0))
+ .attr("data-expires", (v.expiresInMs != null) ? (now + v.expiresInMs) : "")
+ .html('
' + vlEsc(typeLabel) + '
'
+ + '
' + sub + '
'
+ + '
'));
+ });
+ $("#matrix-vl-empty").toggle(!(data.verifications || []).length);
+ if(data.hidden > 0) {
+ $("#matrix-vl-more").text(data.hidden + " older request" + (data.hidden === 1 ? "" : "s") + " hidden.").show();
+ } else {
+ $("#matrix-vl-more").hide();
+ }
+ vlTick();
+ };
+ var vlLoadList = function() {
+ vlCall({ id: vlId(), action: "list" })
+ .done(vlRenderList)
+ .fail(function() { vlRenderList({ result: "error", message: "Request failed — is Node-RED still running?" }); });
+ };
+ var vlShowList = function() {
+ vlClearTimers();
+ $("#matrix-vl-overlay").data("vlConfirmed", false);
+ $("#matrix-vl-detailview").hide();
+ $("#matrix-vl-listview").show();
+ $("#matrix-vl-back,#matrix-vl-cancel").hide();
+ $("#matrix-vl-items").html('
Loading…
');
+ $("#matrix-vl-empty,#matrix-vl-more").hide();
+ vlLoadList();
+ vlListTimer = setInterval(vlLoadList, 5000);
+ vlTickTimer = setInterval(vlTick, 1000);
+ };
+
+ var vlRenderSas = function(sas) {
+ var $sas = $("#matrix-vl-d-sas").empty();
+ if(sas.emoji && sas.emoji.length) {
+ sas.emoji.forEach(function(pair) {
+ $sas.append('
' + vlEsc(pair[0])
+ + '
' + vlEsc(pair[1]) + '
');
+ });
+ } else if(sas.decimal) {
+ $sas.append('
' + vlEsc(sas.decimal.join(" ")) + '
');
+ }
+ };
+ var vlRenderDetail = function(v) {
+ var typeLabel = v.type === "room" ? "Room verification" : "Device verification";
+ var head = vlEsc(typeLabel);
+ if(v.userId) { head += " — " + vlEsc(v.userId); }
+ if(v.deviceId) { head += " (device " + vlEsc(v.deviceId) + ")"; }
+ $("#matrix-vl-d-head").html(head);
+
+ if(v.phase === "done") {
+ vlClearTimers();
+ vlState("fa-check-circle", "#3a9a4e", "
Verified. This session is now verified.");
+ $("#matrix-vl-d-sas,#matrix-vl-d-actions").hide();
+ $("#matrix-vl-cancel").hide();
+ return;
+ }
+ if(v.phase === "cancelled") {
+ vlClearTimers();
+ var why = v.cancellationCode ? (" (" + vlEsc(v.cancellationCode) + ")") : "";
+ vlState("fa-times-circle", "#c9302c", "Verification was cancelled" + why + ".");
+ $("#matrix-vl-d-sas,#matrix-vl-d-actions").hide();
+ $("#matrix-vl-cancel").hide();
+ return;
+ }
+ if(v.phase === "gone") {
+ vlClearTimers();
+ vlState("fa-exclamation-triangle", "#888", "This verification is no longer available.");
+ $("#matrix-vl-d-sas,#matrix-vl-d-actions").hide();
+ $("#matrix-vl-cancel").hide();
+ return;
+ }
+ if($("#matrix-vl-overlay").data("vlConfirmed")) {
+ $("#matrix-vl-d-sas,#matrix-vl-d-actions").hide();
+ vlState("fa-spinner fa-spin", "#888", "Waiting for the other device to confirm…");
+ return;
+ }
+ if(v.sas && (v.sas.emoji || v.sas.decimal)) {
+ vlState("fa-key", "#d18a1b", "Compare these emoji with the other device, then choose below.");
+ vlRenderSas(v.sas);
+ $("#matrix-vl-d-sas,#matrix-vl-d-actions").show();
+ return;
+ }
+ $("#matrix-vl-d-sas,#matrix-vl-d-actions").hide();
+ vlState("fa-spinner fa-spin", "#888", "Waiting for the verification to start…");
+ };
+ var vlPollDetail = function() {
+ vlCall({ id: vlId(), action: "advance", verificationId: vlCurId() })
+ .done(function(data) {
+ if(data.result !== "ok") {
+ vlState("fa-exclamation-triangle", "#c9302c", vlEsc(data.message));
+ return;
+ }
+ vlRenderDetail(data.verification);
+ })
+ .fail(function() { vlState("fa-exclamation-triangle", "#c9302c", "Request failed — is Node-RED still running?"); });
+ };
+ var vlShowDetail = function(vid) {
+ vlClearTimers();
+ $("#matrix-vl-overlay").data("vlCurrentId", vid).data("vlConfirmed", false);
+ $("#matrix-vl-listview").hide();
+ $("#matrix-vl-detailview").show();
+ $("#matrix-vl-d-sas,#matrix-vl-d-actions,#matrix-vl-d-result").hide();
+ $("#matrix-vl-d-head").text("");
+ vlState("fa-spinner fa-spin", "#888", "Starting verification…");
+ $("#matrix-vl-back,#matrix-vl-cancel").show();
+ vlPollDetail();
+ vlDetailTimer = setInterval(vlPollDetail, 1500);
+ };
+
+ var vlClose = function() { vlClearTimers(); $("#matrix-vl-overlay").fadeOut(120); };
+
+ $("#matrix-vl-x,#matrix-vl-close").on("click", vlClose);
+ $("#matrix-vl-overlay").on("mousedown", function(e) { if(e.target === this) { vlClose(); } });
+ $(document).on("keydown.matrixvl", function(e) {
+ if(e.key === "Escape" && $("#matrix-vl-overlay").is(":visible")) { vlClose(); }
+ });
+ $("#matrix-vl-back").on("click", vlShowList);
+ $("#matrix-vl-items").on("click", ".matrix-vl-item", function() {
+ vlShowDetail($(this).attr("data-vid"));
+ });
+ $("#matrix-vl-cancel").on("click", function() {
+ vlCall({ id: vlId(), action: "cancel", verificationId: vlCurId() }).always(vlPollDetail);
+ });
+ $("#matrix-vl-d-confirm").on("click", function() {
+ $("#matrix-vl-overlay").data("vlConfirmed", true);
+ $("#matrix-vl-d-sas,#matrix-vl-d-actions").hide();
+ vlState("fa-spinner fa-spin", "#888", "Confirming…");
+ vlCall({ id: vlId(), action: "confirm", verificationId: vlCurId() })
+ .done(function(data) {
+ if(data.result !== "ok") {
+ $("#matrix-vl-overlay").data("vlConfirmed", false);
+ vlState("fa-exclamation-triangle", "#c9302c", vlEsc(data.message));
+ $("#matrix-vl-d-actions").show();
+ }
+ });
+ });
+ $("#matrix-vl-d-mismatch").on("click", function() {
+ $("#matrix-vl-d-actions").hide();
+ vlState("fa-spinner fa-spin", "#888", "Cancelling…");
+ vlCall({ id: vlId(), action: "mismatch", verificationId: vlCurId() }).always(vlPollDetail);
+ });
+
+ $("#matrix-vl-overlay").data("vlShowListFn", vlShowList);
+ $("#matrix-vl-overlay").data("vlShowDetailFn", vlShowDetail);
+ }
+
+ $("#matrix-verification-list-btn").on("click", function() {
+ $("#matrix-vl-overlay").data("matrixNodeId", nodeId).fadeIn(120);
+ $("#matrix-vl-overlay").data("vlShowListFn")();
+ });
+
+ // --- Sessions (modal) ---
+ // Built once and reused; the node id is stored on the overlay.
+ if (!document.getElementById("matrix-ss-overlay")) {
+ $('').appendTo("head");
+
+ $('
'
+ + '
Sessions '
+ + '×
'
+ + '
'
+ + '
'
+ + '
Current session
'
+ + '
'
+ + '
'
+ + '
Other sessions
'
+ + '
'
+ + '
No other sessions.
'
+ + '
'
+ + '
'
+ + '
'
+ + '
'
+ + '
'
+ + '
Session details
'
+ + '
'
+ + '
'
+ + 'Verify this session
'
+ + '
'
+ + '
Remove this session '
+ + '
'
+ + '
Removing a session signs it out. '
+ + 'Enter the account password to confirm.
'
+ + '
'
+ + '
Confirm removal
'
+ + '
'
+ + '
'
+ + '
'
+ + '
'
+ + '
').appendTo(document.body);
+
+ var ssEsc = function(s) { return $("
").text(s == null ? "" : String(s)).html(); };
+ var ssId = function() { return $("#matrix-ss-overlay").data("matrixNodeId"); };
+ var ssCall = function(body) {
+ return $.ajax({
+ url: "matrix-chat/sessions", type: "POST",
+ contentType: "application/json", data: JSON.stringify(body),
+ });
+ };
+ var ssRelative = function(ts) {
+ if(!ts) { return "activity unknown"; }
+ var days = Math.floor((Date.now() - ts) / 86400000);
+ if(days <= 0) { return "active today"; }
+ if(days === 1) { return "active yesterday"; }
+ if(days < 90) { return "active " + days + " days ago"; }
+ return "inactive for 90+ days";
+ };
+ var ssFullDate = function(ts) {
+ return ts ? new Date(ts).toLocaleString() : "unknown";
+ };
+ var ssShield = function(verified) {
+ return '
';
+ };
+ var ssCard = function(d, isCurrent) {
+ var sub = (d.verified ? 'Verified' : 'Not verified')
+ + ' · ' + ssRelative(d.lastSeenTs)
+ + (d.lastSeenIp ? (' · ' + d.lastSeenIp) : '');
+ return $('
')
+ .data("device", d).data("isCurrent", isCurrent)
+ .html(ssShield(d.verified)
+ + '
'
+ + ssEsc(d.displayName || d.deviceId) + '
'
+ + '
' + ssEsc(sub) + '
'
+ + '
');
+ };
+ var ssResult = function(ok, text) {
+ $("#matrix-ss-d-result").removeClass("ok err").addClass(ok ? "ok" : "err").text(text).show();
+ };
+ var ssRenderList = function(data) {
+ if(data.result !== "ok") {
+ $("#matrix-ss-current").html('
' + ssEsc(data.message) + '
');
+ $("#matrix-ss-currentmsg,#matrix-ss-others").empty();
+ $("#matrix-ss-others-empty,#matrix-ss-more").hide();
+ return;
+ }
+ $("#matrix-ss-current").empty().append(ssCard(data.current, true));
+ $("#matrix-ss-currentmsg").html(data.current.verified
+ ? '
Verified session '
+ + 'This session is cross-signed and ready for secure messaging.
'
+ : '
Not verified '
+ + 'This session is not cross-signed. Use the Set up secure backup & cross-signing '
+ + 'button to verify it.
');
+ var $others = $("#matrix-ss-others").empty();
+ (data.others || []).forEach(function(d) { $others.append(ssCard(d, false)); });
+ $("#matrix-ss-others-empty").toggle(!(data.others || []).length);
+ if(data.hidden > 0) {
+ $("#matrix-ss-more").text(data.hidden + " more session" + (data.hidden === 1 ? "" : "s") + " hidden.").show();
+ } else {
+ $("#matrix-ss-more").hide();
+ }
+ };
+ var ssShowList = function() {
+ $("#matrix-ss-detailview").hide();
+ $("#matrix-ss-listview").show();
+ $("#matrix-ss-back").hide();
+ $("#matrix-ss-current").html('
Loading…
');
+ $("#matrix-ss-currentmsg,#matrix-ss-others").empty();
+ $("#matrix-ss-others-empty,#matrix-ss-more").hide();
+ ssCall({ id: ssId(), action: "list" })
+ .done(ssRenderList)
+ .fail(function() { ssRenderList({ result: "error", message: "Request failed — is Node-RED still running?" }); });
+ };
+ var ssDetailRow = function(k, v) {
+ return '
' + ssEsc(k) + ' ' + ssEsc(v) + '
';
+ };
+ var ssShowDetail = function(d, isCurrent) {
+ $("#matrix-ss-overlay").data("ssDevice", d).data("ssIsCurrent", isCurrent);
+ $("#matrix-ss-listview").hide();
+ $("#matrix-ss-detailview").show();
+ $("#matrix-ss-back").show();
+ $("#matrix-ss-d-result,#matrix-ss-d-removeconfirm").hide();
+ $("#matrix-ss-d-name").text(d.displayName || d.deviceId);
+ $("#matrix-ss-d-status").html(d.verified
+ ? '
Verified session '
+ + 'This session is ready for secure messaging.
'
+ : '
Not verified '
+ + (isCurrent
+ ? 'This session is not cross-signed. Use the Set up secure backup & cross-signing button to verify it.'
+ : 'Verify this session to confirm it is trusted.')
+ + '
');
+ $("#matrix-ss-d-details").html(
+ ssDetailRow("Session ID", d.deviceId)
+ + ssDetailRow("Last activity", ssFullDate(d.lastSeenTs))
+ + ssDetailRow("IP address", d.lastSeenIp || "unknown"));
+ $("#matrix-ss-d-verifywrap").toggle(!isCurrent && !d.verified);
+ $("#matrix-ss-d-removewrap").toggle(!isCurrent);
+ };
+ var ssClose = function() { $("#matrix-ss-overlay").fadeOut(120); };
+
+ $("#matrix-ss-x,#matrix-ss-close").on("click", ssClose);
+ $("#matrix-ss-overlay").on("mousedown", function(e) { if(e.target === this) { ssClose(); } });
+ $(document).on("keydown.matrixss", function(e) {
+ if(e.key === "Escape" && $("#matrix-ss-overlay").is(":visible")) { ssClose(); }
+ });
+ $("#matrix-ss-back").on("click", ssShowList);
+ $("#matrix-ss-current,#matrix-ss-others").on("click", ".matrix-ss-item", function() {
+ ssShowDetail($(this).data("device"), $(this).data("isCurrent"));
+ });
+ $("#matrix-ss-d-rename").on("click", function() {
+ var d = $("#matrix-ss-overlay").data("ssDevice");
+ var name = prompt("Session display name:", d.displayName || "");
+ if(name === null) { return; }
+ ssCall({ id: ssId(), action: "rename", deviceId: d.deviceId, displayName: name })
+ .done(function(r) {
+ if(r.result !== "ok") { ssResult(false, r.message); return; }
+ d.displayName = name;
+ $("#matrix-ss-d-name").text(name || d.deviceId);
+ ssResult(true, "Session renamed.");
+ })
+ .fail(function() { ssResult(false, "Request failed."); });
+ });
+ $("#matrix-ss-d-verify").on("click", function() {
+ var d = $("#matrix-ss-overlay").data("ssDevice");
+ $("#matrix-ss-d-result").removeClass("ok err").html('
Starting verification…').show();
+ ssCall({ id: ssId(), action: "verify", deviceId: d.deviceId })
+ .done(function(r) {
+ if(r.result !== "ok" || !r.verificationId) {
+ ssResult(false, r.message || "Could not start verification.");
+ return;
+ }
+ // hand off to the verification modal's detail view
+ $("#matrix-ss-overlay").fadeOut(120);
+ $("#matrix-vl-overlay").data("matrixNodeId", ssId()).fadeIn(120);
+ $("#matrix-vl-overlay").data("vlShowDetailFn")(r.verificationId);
+ })
+ .fail(function() { ssResult(false, "Request failed."); });
+ });
+ $("#matrix-ss-d-removelink").on("click", function() {
+ $("#matrix-ss-d-password").val("");
+ $("#matrix-ss-d-removeconfirm").show();
+ });
+ $("#matrix-ss-d-removeconfirmbtn").on("click", function() {
+ var d = $("#matrix-ss-overlay").data("ssDevice");
+ $("#matrix-ss-d-result").removeClass("ok err").html('
Removing session…').show();
+ ssCall({ id: ssId(), action: "remove", deviceId: d.deviceId, password: $("#matrix-ss-d-password").val() })
+ .done(function(r) {
+ if(r.result !== "ok") { ssResult(false, r.message); return; }
+ ssShowList();
+ })
+ .fail(function() { ssResult(false, "Request failed."); });
+ });
+
+ $("#matrix-ss-overlay").data("ssShowListFn", ssShowList);
+ }
+
+ $("#matrix-sessions-btn").on("click", function() {
+ $("#matrix-ss-overlay").data("matrixNodeId", nodeId).fadeIn(120);
+ $("#matrix-ss-overlay").data("ssShowListFn")();
+ });
+
// --- Login: fetch a fresh access token & device id ---
$("#matrix-login-btn").on("click", function() {
function prettyPrintJson(json) {
@@ -372,6 +905,24 @@
Sets up cross-signing so the bot's own device shows as verified. The server
configuration must be deployed and connected first.
+
+
+ Verification
+ Pending verification requests
+
+
+ Review and complete incoming device verification requests without building a flow.
+ The server configuration must be deployed and connected first.
+
+
+
+ Sessions
+ Manage sessions
+
+
+ View the account's logged-in sessions, verify them, or remove ones you don't recognize.
+ The server configuration must be deployed and connected first.
+