Fully implement WhatsApp, Signal and XMPP multiplexing
This commit is contained in:
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user