Fully implement WhatsApp, Signal and XMPP multiplexing

This commit is contained in:
2026-02-16 19:19:32 +00:00
parent 3f82c27ab9
commit 658ab10647
9 changed files with 659 additions and 111 deletions

View File

@@ -242,7 +242,7 @@
data-engage-preview-url="{{ compose_engage_preview_url }}"
data-engage-send-url="{{ compose_engage_send_url }}">
{% for msg in serialized_messages %}
<div class="compose-row {% if msg.outgoing %}is-out{% else %}is-in{% endif %}" data-ts="{{ msg.ts }}">
<div class="compose-row {% if msg.outgoing %}is-out{% else %}is-in{% endif %}" data-ts="{{ msg.ts }}" data-message-id="{{ msg.id }}">
{% if msg.gap_fragments %}
{% with gap=msg.gap_fragments.0 %}
<p
@@ -1263,6 +1263,7 @@
lightboxKeyHandler: null,
lightboxImages: [],
lightboxIndex: -1,
seenMessageIds: new Set(),
};
window.giaComposePanels[panelId] = panelState;
const triggerButtons = Array.from(panel.querySelectorAll(".js-ai-trigger"));
@@ -1389,6 +1390,11 @@
};
let lastTs = toInt(thread.dataset.lastTs);
panelState.seenMessageIds = new Set(
Array.from(thread.querySelectorAll(".compose-row"))
.map(function (row) { return String(row.dataset.messageId || "").trim(); })
.filter(Boolean)
);
let glanceState = {
gap: null,
metrics: [],
@@ -1757,13 +1763,42 @@
row.appendChild(chip);
};
const insertRowByTs = function (row) {
const newTs = toInt(row && row.dataset ? row.dataset.ts : 0);
const rows = Array.from(thread.querySelectorAll(".compose-row"));
if (!rows.length) {
thread.appendChild(row);
return;
}
for (let index = rows.length - 1; index >= 0; index -= 1) {
const existing = rows[index];
const existingTs = toInt(existing.dataset ? existing.dataset.ts : 0);
if (existingTs <= newTs) {
if (existing.nextSibling) {
thread.insertBefore(row, existing.nextSibling);
} else {
thread.appendChild(row);
}
return;
}
}
thread.insertBefore(row, rows[0]);
};
const appendBubble = function (msg) {
console.log("[appendBubble]", {id: msg.id, ts: msg.ts, author: msg.author, source_label: msg.source_label, source_service: msg.source_service, outgoing: msg.outgoing});
const messageId = String(msg && msg.id ? msg.id : "").trim();
if (messageId && panelState.seenMessageIds.has(messageId)) {
return;
}
const row = document.createElement("div");
const outgoing = !!msg.outgoing;
row.className = "compose-row " + (outgoing ? "is-out" : "is-in");
row.dataset.ts = String(msg.ts || 0);
row.dataset.minute = minuteBucketFromTs(msg.ts || 0);
if (messageId) {
row.dataset.messageId = messageId;
panelState.seenMessageIds.add(messageId);
}
appendLatencyChip(row, msg);
const bubble = document.createElement("article");
@@ -1771,7 +1806,6 @@
// Add source badge for client-side rendered messages
if (msg.source_label) {
console.log("[appendBubble] rendering source badge:", msg.source_label);
const badgeWrap = document.createElement("div");
badgeWrap.className = "compose-source-badge-wrap";
const badge = document.createElement("span");
@@ -1868,7 +1902,8 @@
if (emptyWrap) {
emptyWrap.remove();
}
thread.appendChild(row);
row.appendChild(bubble);
insertRowByTs(row);
wireImageFallbacks(row);
updateGlanceFromMessage(msg);
};
@@ -2033,18 +2068,15 @@
}
params.set("limit", thread.dataset.limit || "60");
params.set("after_ts", String(lastTs));
console.log("[poll] fetching messages: service=" + params.get("service") + " after_ts=" + lastTs);
const response = await fetch(thread.dataset.pollUrl + "?" + params.toString(), {
method: "GET",
credentials: "same-origin",
headers: { Accept: "application/json" }
});
if (!response.ok) {
console.log("[poll] response not ok:", response.status);
return;
}
const payload = await response.json();
console.log("[poll] received payload with " + (payload.messages ? payload.messages.length : 0) + " messages");
appendMessages(payload.messages || [], forceScroll);
if (payload.typing) {
applyTyping(payload.typing);
@@ -2283,6 +2315,7 @@
thread.innerHTML = '<p class="compose-empty">Loading messages...</p>';
lastTs = 0;
thread.dataset.lastTs = "0";
panelState.seenMessageIds = new Set();
glanceState = { gap: null, metrics: [] };
renderGlanceItems([]);
poll(true);
@@ -3083,10 +3116,15 @@
// HTMX will dispatch a `composeSendCommandId` event with detail {command_id: "..."}.
panelState.pendingCommandId = null;
panelState.pendingCommandPoll = null;
panelState.pendingCommandAttempts = 0;
panelState.pendingCommandStartedAt = 0;
panelState.pendingCommandInFlight = false;
const startPendingCommandPolling = function (commandId) {
if (!commandId) return;
panelState.pendingCommandId = commandId;
panelState.pendingCommandAttempts = 0;
panelState.pendingCommandStartedAt = Date.now();
// Show persistent cancel UI
showPersistentCancelButton(commandId);
// Poll for result every 1500ms
@@ -3094,12 +3132,29 @@
clearInterval(panelState.pendingCommandPoll);
}
panelState.pendingCommandPoll = setInterval(async function () {
if (panelState.pendingCommandInFlight) {
return;
}
panelState.pendingCommandAttempts += 1;
const elapsedMs = Date.now() - (panelState.pendingCommandStartedAt || Date.now());
if (panelState.pendingCommandAttempts > 14 || elapsedMs > 45000) {
stopPendingCommandPolling();
hidePersistentCancelButton();
setStatus('Send timed out waiting for runtime result. Please retry.', 'warning');
return;
}
try {
panelState.pendingCommandInFlight = true;
const url = new URL('{% url "compose_command_result" %}', window.location.origin);
url.searchParams.set('service', thread.dataset.service || '');
url.searchParams.set('command_id', commandId);
const resp = await fetch(url.toString(), { credentials: 'same-origin' });
url.searchParams.set('format', 'json');
const resp = await fetch(url.toString(), {
credentials: 'same-origin',
headers: { 'HX-Request': 'true' },
});
if (!resp.ok) return;
if (resp.status === 204) return;
const payload = await resp.json();
if (payload && payload.pending === false) {
// Stop polling
@@ -3123,8 +3178,10 @@
}
} catch (e) {
// ignore transient network errors
} finally {
panelState.pendingCommandInFlight = false;
}
}, 1500);
}, 3500);
};
const stopPendingCommandPolling = function () {
@@ -3133,6 +3190,9 @@
panelState.pendingCommandPoll = null;
}
panelState.pendingCommandId = null;
panelState.pendingCommandAttempts = 0;
panelState.pendingCommandStartedAt = 0;
panelState.pendingCommandInFlight = false;
};
const persistentCancelContainerId = panelId + '-persistent-cancel';