Reimplement compose and add tiling windows
This commit is contained in:
321
core/static/js/compose-panel-send.js
Normal file
321
core/static/js/compose-panel-send.js
Normal file
@@ -0,0 +1,321 @@
|
||||
(function () {
|
||||
if (window.GIAComposePanelSend) {
|
||||
return;
|
||||
}
|
||||
|
||||
const core = window.GIAComposePanelCore;
|
||||
if (!core) {
|
||||
return;
|
||||
}
|
||||
|
||||
const createController = function (config) {
|
||||
const panel = config.panel;
|
||||
const panelId = config.panelId;
|
||||
const state = config.state;
|
||||
const thread = config.thread;
|
||||
const form = config.form;
|
||||
const textarea = config.textarea;
|
||||
const statusBox = config.statusBox;
|
||||
const manualConfirm = config.manualConfirm;
|
||||
const armInput = config.armInput;
|
||||
const confirmInput = config.confirmInput;
|
||||
const sendButton = config.sendButton;
|
||||
const sendCapable = config.sendCapable;
|
||||
const csrfToken = config.csrfToken;
|
||||
const queryParams = config.queryParams;
|
||||
const poll = config.poll;
|
||||
const clearReplyTarget = config.clearReplyTarget;
|
||||
const autosize = config.autosize;
|
||||
const flashCompose = config.flashCompose;
|
||||
const setStatus = config.setStatus;
|
||||
|
||||
let transientCancelButton = null;
|
||||
let persistentCancelWrap = null;
|
||||
|
||||
const postFormJson = async function (url, params) {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"X-CSRFToken": csrfToken,
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: params.toString(),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Request failed");
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const cancelSendRequest = function (commandId) {
|
||||
return postFormJson(
|
||||
String(panel.dataset.cancelSendUrl || ""),
|
||||
queryParams({ command_id: String(commandId || "") })
|
||||
);
|
||||
};
|
||||
|
||||
const hideTransientCancelButton = function () {
|
||||
if (!transientCancelButton) {
|
||||
return;
|
||||
}
|
||||
transientCancelButton.remove();
|
||||
transientCancelButton = null;
|
||||
};
|
||||
|
||||
const showTransientCancelButton = function () {
|
||||
if (!statusBox || transientCancelButton) {
|
||||
return;
|
||||
}
|
||||
transientCancelButton = core.createNode(
|
||||
"button",
|
||||
"button is-danger is-light is-small compose-cancel-send-btn",
|
||||
"Cancel Send"
|
||||
);
|
||||
transientCancelButton.type = "button";
|
||||
transientCancelButton.addEventListener("click", async function () {
|
||||
try {
|
||||
await cancelSendRequest("");
|
||||
} catch (_err) {
|
||||
// Ignore cancel failures.
|
||||
} finally {
|
||||
hideTransientCancelButton();
|
||||
}
|
||||
});
|
||||
statusBox.appendChild(transientCancelButton);
|
||||
};
|
||||
|
||||
const hidePersistentCancelButton = function () {
|
||||
if (!persistentCancelWrap) {
|
||||
return;
|
||||
}
|
||||
persistentCancelWrap.remove();
|
||||
persistentCancelWrap = null;
|
||||
};
|
||||
|
||||
const stopPendingCommandPolling = function () {
|
||||
if (state.pendingCommandPoll) {
|
||||
clearInterval(state.pendingCommandPoll);
|
||||
state.pendingCommandPoll = null;
|
||||
}
|
||||
state.pendingCommandId = "";
|
||||
state.pendingCommandAttempts = 0;
|
||||
state.pendingCommandStartedAt = 0;
|
||||
state.pendingCommandInFlight = false;
|
||||
};
|
||||
|
||||
const showPersistentCancelButton = function (commandId) {
|
||||
hidePersistentCancelButton();
|
||||
if (!statusBox) {
|
||||
return;
|
||||
}
|
||||
persistentCancelWrap = core.createNode("div", "compose-persistent-cancel");
|
||||
const button = core.createNode(
|
||||
"button",
|
||||
"button is-danger is-light is-small compose-persistent-cancel-btn",
|
||||
"Cancel Queued Send"
|
||||
);
|
||||
button.type = "button";
|
||||
button.addEventListener("click", async function () {
|
||||
try {
|
||||
await cancelSendRequest(commandId);
|
||||
stopPendingCommandPolling();
|
||||
hidePersistentCancelButton();
|
||||
setStatus("Send cancelled.", "warning");
|
||||
await poll(true);
|
||||
} catch (_err) {
|
||||
hidePersistentCancelButton();
|
||||
}
|
||||
});
|
||||
persistentCancelWrap.appendChild(button);
|
||||
statusBox.appendChild(persistentCancelWrap);
|
||||
};
|
||||
|
||||
const pollPendingCommandResult = async function (commandId) {
|
||||
const url = new URL(
|
||||
String(panel.dataset.commandResultUrl || ""),
|
||||
window.location.origin
|
||||
);
|
||||
url.searchParams.set("service", thread.dataset.service || "");
|
||||
url.searchParams.set("command_id", commandId);
|
||||
url.searchParams.set("format", "json");
|
||||
const response = await fetch(url.toString(), {
|
||||
credentials: "same-origin",
|
||||
headers: { "HX-Request": "true" },
|
||||
});
|
||||
if (!response.ok || response.status === 204) {
|
||||
return null;
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const startPendingCommandPolling = function (commandId) {
|
||||
if (!commandId) {
|
||||
return;
|
||||
}
|
||||
stopPendingCommandPolling();
|
||||
state.pendingCommandId = commandId;
|
||||
state.pendingCommandStartedAt = Date.now();
|
||||
showPersistentCancelButton(commandId);
|
||||
state.pendingCommandPoll = setInterval(async function () {
|
||||
if (state.pendingCommandInFlight) {
|
||||
return;
|
||||
}
|
||||
state.pendingCommandAttempts += 1;
|
||||
if (
|
||||
state.pendingCommandAttempts > 14
|
||||
|| (Date.now() - state.pendingCommandStartedAt) > 45000
|
||||
) {
|
||||
stopPendingCommandPolling();
|
||||
hidePersistentCancelButton();
|
||||
setStatus(
|
||||
"Send timed out waiting for runtime result. Please retry.",
|
||||
"warning"
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
state.pendingCommandInFlight = true;
|
||||
const payload = await pollPendingCommandResult(commandId);
|
||||
if (!payload || payload.pending !== false) {
|
||||
return;
|
||||
}
|
||||
const result = payload.result || {};
|
||||
stopPendingCommandPolling();
|
||||
hidePersistentCancelButton();
|
||||
if (result.ok) {
|
||||
setStatus("", "success");
|
||||
textarea.value = "";
|
||||
clearReplyTarget();
|
||||
autosize();
|
||||
flashCompose("is-send-success");
|
||||
await poll(true);
|
||||
return;
|
||||
}
|
||||
setStatus(String(result.error || "Send failed."), "danger");
|
||||
flashCompose("is-send-fail");
|
||||
await poll(true);
|
||||
} catch (_err) {
|
||||
// Ignore transient failures; the next poll can recover.
|
||||
} finally {
|
||||
state.pendingCommandInFlight = false;
|
||||
}
|
||||
}, 3500);
|
||||
};
|
||||
|
||||
const updateManualSafety = function () {
|
||||
const confirmed = !!(manualConfirm && manualConfirm.checked);
|
||||
if (armInput) {
|
||||
armInput.value = confirmed ? "1" : "0";
|
||||
}
|
||||
if (confirmInput) {
|
||||
confirmInput.value = confirmed ? "1" : "0";
|
||||
}
|
||||
if (sendButton) {
|
||||
sendButton.disabled = !sendCapable || !confirmed;
|
||||
}
|
||||
};
|
||||
|
||||
const bindSendEvents = function () {
|
||||
textarea.addEventListener("keydown", function (event) {
|
||||
if (event.key !== "Enter" || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (sendButton && sendButton.disabled) {
|
||||
setStatus("Enable send confirmation before sending.", "warning");
|
||||
return;
|
||||
}
|
||||
form.requestSubmit();
|
||||
});
|
||||
|
||||
form.addEventListener("submit", function () {
|
||||
if (sendButton && sendButton.disabled) {
|
||||
return;
|
||||
}
|
||||
showTransientCancelButton();
|
||||
});
|
||||
|
||||
form.addEventListener("htmx:afterRequest", function () {
|
||||
hideTransientCancelButton();
|
||||
textarea.focus();
|
||||
});
|
||||
};
|
||||
|
||||
const bindDocumentEvents = function () {
|
||||
state.eventHandler = function (event) {
|
||||
const detail = (event && event.detail) || {};
|
||||
const sourcePanelId = String(detail.panel_id || "");
|
||||
if (sourcePanelId && sourcePanelId !== panelId) {
|
||||
return;
|
||||
}
|
||||
poll(true);
|
||||
};
|
||||
document.body.addEventListener("composeMessageSent", state.eventHandler);
|
||||
|
||||
state.sendResultHandler = function (event) {
|
||||
const detail = (event && event.detail) || {};
|
||||
const sourcePanelId = String(detail.panel_id || "");
|
||||
if (sourcePanelId && sourcePanelId !== panelId) {
|
||||
return;
|
||||
}
|
||||
hideTransientCancelButton();
|
||||
if (detail.ok) {
|
||||
flashCompose("is-send-success");
|
||||
textarea.value = "";
|
||||
clearReplyTarget();
|
||||
autosize();
|
||||
poll(true);
|
||||
} else {
|
||||
flashCompose("is-send-fail");
|
||||
if (detail.message) {
|
||||
setStatus(detail.message, detail.level || "danger");
|
||||
}
|
||||
}
|
||||
textarea.focus();
|
||||
};
|
||||
document.body.addEventListener("composeSendResult", state.sendResultHandler);
|
||||
|
||||
state.commandIdHandler = function (event) {
|
||||
const detail = (event && event.detail) || {};
|
||||
const commandId = String(
|
||||
detail.command_id
|
||||
|| (detail.composeSendCommandId && detail.composeSendCommandId.command_id)
|
||||
|| ""
|
||||
).trim();
|
||||
if (commandId) {
|
||||
startPendingCommandPolling(commandId);
|
||||
}
|
||||
};
|
||||
document.body.addEventListener(
|
||||
"composeSendCommandId",
|
||||
state.commandIdHandler
|
||||
);
|
||||
};
|
||||
|
||||
const init = function () {
|
||||
bindSendEvents();
|
||||
bindDocumentEvents();
|
||||
if (manualConfirm) {
|
||||
manualConfirm.addEventListener("change", updateManualSafety);
|
||||
manualConfirm.dispatchEvent(new Event("change"));
|
||||
} else {
|
||||
updateManualSafety();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
init: init,
|
||||
resetForContextSwitch: function () {
|
||||
stopPendingCommandPolling();
|
||||
hidePersistentCancelButton();
|
||||
hideTransientCancelButton();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
window.GIAComposePanelSend = {
|
||||
createController: createController,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user