Reimplement compose and add tiling windows

This commit is contained in:
2026-03-12 22:03:30 +00:00
parent 79766d279d
commit 6ceff63b71
126 changed files with 5111 additions and 10796 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,259 +0,0 @@
// Author: Grzegorz Tężycki
$(document).ready(function(){
// In web storage is saved structure like that:
// localStorage['django_tables2_column_shifter'] = {
// 'table_class_container1' : {
// 'id' : 'on',
// 'col1' : 'off',
// 'col2' : 'on',
// 'col3' : 'on',
// },
// 'table_class_container2' : {
// 'id' : 'on',
// 'col1' : 'on'
// },
// }
// main name for key in web storage
var COLUMN_SHIFTER_STORAGE_ACCESOR = "django_tables2_column_shifter";
// Return storage structure for shifter
// If structure does'n exist in web storage
// will be return empty object
var get_column_shifter_storage = function(){
var storage = localStorage.getItem(COLUMN_SHIFTER_STORAGE_ACCESOR);
if (storage === null) {
storage = {
"drilldown-table": {
"date": "off",
"time": "off",
"id": "off",
"host": "off",
"ident": "off",
"channel": "off",
"net": "off",
"num": "off",
"channel_nsfw": "off",
"channel_category": "off",
"channel_category_id": "off",
"channel_category_nsfw": "off",
"channel_id": "off",
"guild_member_count": "off",
"bot": "off",
"msg_id": "off",
"user": "off",
"net_id": "off",
"user_id": "off",
"nick_id": "off",
"status": "off",
"num_users": "off",
"num_chans": "off",
"exemption": "off",
// "version_sentiment": "off",
"sentiment": "off",
"num": "off",
"online": "off",
"mtype": "off",
"realname": "off",
"server": "off",
"mtype": "off",
"hidden": "off",
"filename": "off",
"file_md5": "off",
"file_ext": "off",
"file_size": "off",
"lang_code": "off",
"tokens": "off",
"rule_id": "off",
"index": "off",
"meta": "off",
"match_ts": "off",
"batch_id": "off"
//"lang_name": "off",
// "words_noun": "off",
// "words_adj": "off",
// "words_verb": "off",
// "words_adv": "off"
},
};
} else {
storage = JSON.parse(storage);
}
return storage;
};
// Save structure in web storage
var set_column_shifter_storage = function(storage){
var json_storage = JSON.stringify(storage)
localStorage.setItem(COLUMN_SHIFTER_STORAGE_ACCESOR, json_storage);
};
// Remember state for single button
var save_btn_state = function($btn){
// Take css class for container with table
var table_class_container = $btn.data("table-class-container");
// Take html object with table
var $table_class_container = $("#" + table_class_container);
// Take single button statne ("on" / "off")
var state = $btn.data("state");
// td-class is a real column name in table
var td_class = $btn.data("td-class");
var storage = get_column_shifter_storage();
// Table id
var id = $table_class_container.attr("id");
// Checking if the ID is already in storage
if (id in storage) {
data = storage[id]
} else {
data = {}
storage[id] = data;
}
// Save state for table column in storage
data[td_class] = state;
set_column_shifter_storage(storage);
};
// Load states for buttons from storage for single tabel
var load_states = function($table_class_container) {
var storage = get_column_shifter_storage();
// Table id
var id = $table_class_container.attr("id");
var data = {};
// Checking if the ID is already in storage
if (id in storage) {
data = storage[id]
// For each shifter button set state
$table_class_container.find(".btn-shift-column").each(function(){
var $btn = $(this);
var td_class = $btn.data("td-class");
// If name of column is in store then get state
// and set state
if (td_class in data) {
var state = data[td_class]
set_btn_state($btn, state);
}
});
}
};
// Show table content and hide spiner
var show_table_content = function($table_class_container){
$table_class_container.find("#loader").hide();
$table_class_container.find("#table-container").show();
};
// Load buttons states for all button in page
var load_state_for_all_containters = function(){
$(".column-shifter-container").each(function(){
$table_class_container = $(this);
// Load states for all buttons in single container
load_states($table_class_container);
// When states was loaded then table must be show and
// loader (spiner) must be hide
show_table_content($table_class_container);
});
};
// change visibility column for single button
// if button has state "on" then show column
// else then column will be hide
shift_column = function( $btn ){
// button state
var state = $btn.data("state");
// td-class is a real column name in table
var td_class = $btn.data("td-class");
var table_class_container = $btn.data("table-class-container");
var $table_class_container = $("#" + table_class_container);
var $table = $table_class_container.find("table");
var $cels = $table.find("." + td_class);
if ( state === "on" ) {
$cels.show();
} else {
$cels.hide();
}
};
// Shift visibility for all columns
shift_columns = function(){
var cols = $(".btn-shift-column");
var i, len = cols.length;
for (i=0; i < len; i++) {
shift_column($(cols[i]));
}
};
// Set icon imgae visibility for button state
var set_icon_for_state = function( $btn, state ) {
if (state === "on") {
$btn.find("span.uncheck").hide();
$btn.find("span.check").show();
} else {
$btn.find("span.check").hide();
$btn.find("span.uncheck").show();
}
};
// Set state for single button
var set_btn_state = function($btn, state){
$btn.data('state', state);
set_icon_for_state($btn, state);
}
// Change state for single button
var change_btn_state = function($btn){
var state = $btn.data("state");
if (state === "on") {
state = "off"
} else {
state = "on"
}
set_btn_state($btn, state);
};
// Run show/hide when click on button
$(".btn-shift-column").on("click", function(event){
var $btn = $(this);
event.stopPropagation();
change_btn_state($btn);
shift_column($btn);
save_btn_state($btn);
});
// Load saved states for all tables
load_state_for_all_containters();
// show or hide columns based on data from web storage
shift_columns();
// Add API method for retrieving non-visible cols for table
// Pass the 0-based index of the table or leave the parameter
// empty to return the hidden cols for the 1st table found
$.django_tables2_column_shifter_hidden = function(idx) {
if(idx==undefined) {
idx = 0;
}
return $('#table-container').eq(idx).find('.btn-shift-column').filter(function(z) {
return $(this).data('state')=='off'
}).map(function(z) {
return $(this).data('td-class')
}).toArray();
}
const event = new Event('restore-scroll');
document.dispatchEvent(event);
const event2 = new Event('load-widget-results');
document.dispatchEvent(event2);
});

View File

@@ -0,0 +1,139 @@
(function () {
if (window.GIAComposePanelCore) {
return;
}
const PANEL_SELECTOR = ".compose-shell[data-compose-panel='1']";
window.giaComposePanels = window.giaComposePanels || {};
const collectPanels = function (root) {
const panels = [];
if (!root) {
return panels;
}
if (root.matches && root.matches(PANEL_SELECTOR)) {
panels.push(root);
}
if (root.querySelectorAll) {
root.querySelectorAll(PANEL_SELECTOR).forEach(function (panel) {
panels.push(panel);
});
}
return panels;
};
const toInt = function (value) {
const parsed = parseInt(value || "0", 10);
return Number.isFinite(parsed) ? parsed : 0;
};
const parseJsonSafe = function (value, fallback) {
try {
return JSON.parse(String(value || ""));
} catch (_err) {
return fallback;
}
};
const createNode = function (tagName, className, text) {
const node = document.createElement(tagName);
if (className) {
node.className = className;
}
if (text !== undefined && text !== null) {
node.textContent = String(text);
}
return node;
};
const normalizeSnippet = function (value) {
const compact = String(value || "").replace(/\s+/g, " ").trim();
if (!compact) {
return "(no text)";
}
if (compact.length <= 120) {
return compact;
}
return compact.slice(0, 117).trimEnd() + "...";
};
const titleCase = function (value) {
const raw = String(value || "").trim().toLowerCase();
if (!raw) {
return "";
}
if (raw === "whatsapp") {
return "WhatsApp";
}
if (raw === "xmpp") {
return "XMPP";
}
return raw.charAt(0).toUpperCase() + raw.slice(1);
};
const normalizeIdentifierForService = function (service, identifier) {
const serviceKey = String(service || "").trim().toLowerCase();
const raw = String(identifier || "").trim();
if (serviceKey === "whatsapp" && raw.includes("@")) {
return raw.split("@", 1)[0].trim();
}
return raw;
};
const buildComposeUrl = function (renderMode, service, identifier, personId) {
const serviceKey = String(service || "").trim().toLowerCase();
const identifierValue = normalizeIdentifierForService(serviceKey, identifier);
if (!serviceKey || !identifierValue) {
return "";
}
const params = new URLSearchParams();
params.set("service", serviceKey);
params.set("identifier", identifierValue);
if (personId) {
params.set("person", String(personId || "").trim());
}
return (renderMode === "page" ? "/compose/page/" : "/compose/widget/")
+ "?"
+ params.toString();
};
const parseServiceMap = function (optionNode) {
const fallbackService = String(
(optionNode && optionNode.dataset && optionNode.dataset.service) || ""
).trim().toLowerCase();
const fallbackIdentifier = String((optionNode && optionNode.value) || "").trim();
const fallback = {};
if (fallbackService && fallbackIdentifier) {
fallback[fallbackService] = fallbackIdentifier;
}
if (!optionNode || !optionNode.dataset) {
return fallback;
}
const parsed = parseJsonSafe(optionNode.dataset.serviceMap || "{}", fallback);
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return fallback;
}
const normalized = {};
Object.keys(parsed).forEach(function (key) {
const serviceKey = String(key || "").trim().toLowerCase();
const identifierValue = String(parsed[key] || "").trim();
if (serviceKey && identifierValue) {
normalized[serviceKey] = identifierValue;
}
});
return Object.keys(normalized).length ? normalized : fallback;
};
window.GIAComposePanelCore = {
PANEL_SELECTOR: PANEL_SELECTOR,
buildComposeUrl: buildComposeUrl,
collectPanels: collectPanels,
createNode: createNode,
normalizeIdentifierForService: normalizeIdentifierForService,
normalizeSnippet: normalizeSnippet,
parseJsonSafe: parseJsonSafe,
parseServiceMap: parseServiceMap,
titleCase: titleCase,
toInt: toInt,
};
})();

View 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,
};
})();

View File

@@ -0,0 +1,504 @@
(function () {
if (window.GIAComposePanelThread) {
return;
}
const core = window.GIAComposePanelCore;
if (!core) {
return;
}
const createController = function (config) {
const panel = config.panel;
const state = config.state;
const thread = config.thread;
const textarea = config.textarea;
const typingNode = config.typingNode;
const hiddenReplyTo = config.hiddenReplyTo;
const replyBanner = config.replyBanner;
const replyBannerText = config.replyBannerText;
const replyClearBtn = config.replyClearBtn;
const platformSelect = config.platformSelect;
const contactSelect = config.contactSelect;
const hiddenService = config.hiddenService;
const hiddenIdentifier = config.hiddenIdentifier;
const hiddenPerson = config.hiddenPerson;
const metaLine = config.metaLine;
const renderMode = config.renderMode;
let lastTs = core.toInt(thread.dataset.lastTs);
let beforeContextReset = null;
const nearBottom = function () {
return thread.scrollHeight - thread.scrollTop - thread.clientHeight < 120;
};
const scrollToBottom = function (force) {
if (force || nearBottom()) {
thread.scrollTop = thread.scrollHeight;
}
};
const queryParams = function (extraParams) {
const params = new URLSearchParams();
params.set("service", thread.dataset.service || "");
params.set("identifier", thread.dataset.identifier || "");
if (thread.dataset.person) {
params.set("person", thread.dataset.person);
}
params.set("limit", thread.dataset.limit || "60");
const extras =
extraParams && typeof extraParams === "object" ? extraParams : {};
Object.keys(extras).forEach(function (key) {
const value = extras[key];
if (value === undefined || value === null || value === "") {
return;
}
params.set(String(key), String(value));
});
return params;
};
const ensureEmptyState = function (messageText) {
if (thread.querySelector(".compose-row")) {
const empty = thread.querySelector(".compose-empty");
if (empty) {
empty.remove();
}
return;
}
let empty = thread.querySelector(".compose-empty");
if (!empty) {
empty = core.createNode("p", "compose-empty");
thread.appendChild(empty);
}
empty.textContent = String(
messageText || "No stored messages for this contact yet."
);
};
const rowByMessageId = function (messageId) {
const targetId = String(messageId || "").trim();
if (!targetId) {
return null;
}
return thread.querySelector(
'.compose-row[data-message-id="' + targetId + '"]'
);
};
const clearReplySelectionClass = function () {
thread
.querySelectorAll(".compose-row.compose-reply-selected")
.forEach(function (row) {
row.classList.remove("compose-reply-selected");
});
};
const flashReplyTarget = function (row) {
if (!row) {
return;
}
row.classList.remove("is-target-flash");
void row.offsetWidth;
row.classList.add("is-target-flash");
window.setTimeout(function () {
row.classList.remove("is-target-flash");
}, 1000);
};
const clearReplyTarget = function () {
if (hiddenReplyTo) {
hiddenReplyTo.value = "";
}
if (replyBanner) {
replyBanner.classList.add("is-hidden");
}
if (replyBannerText) {
replyBannerText.textContent = "";
}
clearReplySelectionClass();
};
const setReplyTarget = function (messageId, preview) {
const targetId = String(messageId || "").trim();
if (!targetId) {
clearReplyTarget();
return;
}
const row = rowByMessageId(targetId);
const snippet = core.normalizeSnippet(
(row && row.dataset ? row.dataset.replySnippet : "") || preview || ""
);
if (hiddenReplyTo) {
hiddenReplyTo.value = targetId;
}
if (replyBannerText) {
replyBannerText.textContent = snippet;
}
if (replyBanner) {
replyBanner.classList.remove("is-hidden");
}
clearReplySelectionClass();
if (row) {
row.classList.add("compose-reply-selected");
}
};
const parseMessageRows = function (html) {
const markup = String(html || "").trim();
if (!markup) {
return [];
}
const template = document.createElement("template");
template.innerHTML = markup;
return Array.from(template.content.querySelectorAll(".compose-row"));
};
const insertRowByTs = function (row) {
const newTs = core.toInt(row.dataset.ts);
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];
if (core.toInt(existing.dataset.ts) <= newTs) {
if (existing.nextSibling) {
thread.insertBefore(row, existing.nextSibling);
} else {
thread.appendChild(row);
}
return;
}
}
thread.insertBefore(row, rows[0]);
};
const upsertMessageRow = function (row) {
if (!row || !row.classList || !row.classList.contains("compose-row")) {
return;
}
const messageId = String((row.dataset && row.dataset.messageId) || "").trim();
if (messageId) {
const existing = rowByMessageId(messageId);
if (existing) {
existing.remove();
}
}
const empty = thread.querySelector(".compose-empty");
if (empty) {
empty.remove();
}
insertRowByTs(row);
lastTs = Math.max(lastTs, core.toInt(row.dataset.ts));
thread.dataset.lastTs = String(lastTs);
};
const appendMessageHtml = function (html, forceScroll) {
const rows = parseMessageRows(html);
const shouldStick = forceScroll || nearBottom();
rows.forEach(function (msg) {
upsertMessageRow(msg);
});
if (rows.length) {
scrollToBottom(shouldStick);
}
ensureEmptyState();
};
const applyTyping = function (payload) {
if (!typingNode) {
return;
}
const typingPayload =
payload && typeof payload === "object" ? payload : {};
if (!typingPayload.typing) {
typingNode.classList.add("is-hidden");
return;
}
const displayName = String(typingPayload.display_name || "").trim();
typingNode.textContent = (displayName || "Contact") + " is typing...";
typingNode.classList.remove("is-hidden");
};
const closeSocket = function () {
if (!state.socket) {
return;
}
try {
state.socket.close();
} catch (_err) {
// Ignore close failures.
}
state.socket = null;
state.websocketReady = false;
};
const poll = async function (forceScroll) {
if (state.polling || state.websocketReady) {
return;
}
state.polling = true;
try {
const response = await fetch(
thread.dataset.pollUrl + "?" + queryParams({ after_ts: String(lastTs) }),
{
method: "GET",
credentials: "same-origin",
headers: { Accept: "application/json" },
}
);
if (!response.ok) {
return;
}
const payload = await response.json();
appendMessageHtml(payload.messages_html || "", forceScroll);
applyTyping(payload.typing);
if (payload.last_ts !== undefined && payload.last_ts !== null) {
lastTs = Math.max(lastTs, core.toInt(payload.last_ts));
thread.dataset.lastTs = String(lastTs);
}
} catch (err) {
console.debug("compose poll error", err);
} finally {
state.polling = false;
}
};
const setupWebSocket = function () {
const wsPath = String(thread.dataset.wsUrl || "").trim();
if (!wsPath || !window.WebSocket) {
return;
}
try {
const protocol = window.location.protocol === "https:" ? "wss://" : "ws://";
const socket = new WebSocket(protocol + window.location.host + wsPath);
state.socket = socket;
socket.onopen = function () {
state.websocketReady = true;
try {
socket.send(JSON.stringify({ kind: "sync", last_ts: lastTs }));
} catch (_err) {
// Ignore sync send errors.
}
};
socket.onmessage = function (event) {
const payload = core.parseJsonSafe(event.data || "{}", {});
appendMessageHtml(payload.messages_html || "", false);
applyTyping(payload.typing);
if (payload.last_ts !== undefined && payload.last_ts !== null) {
lastTs = Math.max(lastTs, core.toInt(payload.last_ts));
thread.dataset.lastTs = String(lastTs);
}
};
socket.onclose = function () {
state.websocketReady = false;
if (state.socket === socket) {
state.socket = null;
}
};
socket.onerror = function () {
state.websocketReady = false;
};
} catch (_err) {
state.websocketReady = false;
state.socket = null;
}
};
const switchThreadContext = function (
nextService,
nextIdentifier,
nextPersonId,
nextUrl
) {
const service = String(nextService || "").trim().toLowerCase();
const identifier = core.normalizeIdentifierForService(
service,
nextIdentifier
);
const personId = String(nextPersonId || "").trim();
if (!service || !identifier) {
return;
}
if (renderMode === "page" && nextUrl) {
window.location.assign(String(nextUrl));
return;
}
if (
String(thread.dataset.service || "").toLowerCase() === service
&& String(thread.dataset.identifier || "") === identifier
&& String(thread.dataset.person || "") === personId
) {
return;
}
if (typeof beforeContextReset === "function") {
beforeContextReset();
}
thread.dataset.service = service;
thread.dataset.identifier = identifier;
if (personId) {
thread.dataset.person = personId;
} else {
delete thread.dataset.person;
}
if (hiddenService) {
hiddenService.value = service;
}
if (hiddenIdentifier) {
hiddenIdentifier.value = identifier;
}
if (hiddenPerson) {
hiddenPerson.value = personId;
}
if (metaLine) {
metaLine.textContent = core.titleCase(service) + " · " + identifier;
}
clearReplyTarget();
closeSocket();
lastTs = 0;
thread.dataset.lastTs = "0";
thread.innerHTML = "";
ensureEmptyState("Loading messages...");
applyTyping({ typing: false });
poll(true);
setupWebSocket();
};
const bindContextSelectors = function () {
if (platformSelect) {
platformSelect.addEventListener("change", function () {
const selected = platformSelect.options[platformSelect.selectedIndex];
if (!selected) {
return;
}
const targetUrl = core.buildComposeUrl(
renderMode,
selected.value || "",
selected.dataset.identifier || "",
selected.dataset.person || ""
);
switchThreadContext(
selected.value || "",
selected.dataset.identifier || "",
selected.dataset.person || "",
targetUrl
);
});
}
if (contactSelect) {
contactSelect.addEventListener("change", function () {
const selected = contactSelect.options[contactSelect.selectedIndex];
if (!selected) {
return;
}
const serviceMap = core.parseServiceMap(selected);
const currentService = String(thread.dataset.service || "").toLowerCase();
const availableServices = Object.keys(serviceMap);
let selectedService = currentService || String(selected.dataset.service || "");
let selectedIdentifier = String(serviceMap[selectedService] || "").trim();
if (!selectedIdentifier) {
selectedService = String(
selected.dataset.service || selectedService
).trim().toLowerCase();
selectedIdentifier = String(serviceMap[selectedService] || "").trim();
}
if (!selectedIdentifier && availableServices.length) {
selectedService = availableServices[0];
selectedIdentifier = String(serviceMap[selectedService] || "").trim();
}
const targetUrl = core.buildComposeUrl(
renderMode,
selectedService,
selectedIdentifier,
selected.dataset.person || ""
);
switchThreadContext(
selectedService,
selectedIdentifier,
selected.dataset.person || "",
targetUrl
);
});
}
};
const bindThreadEvents = function () {
if (replyClearBtn) {
replyClearBtn.addEventListener("click", function () {
clearReplyTarget();
textarea.focus();
});
}
thread.addEventListener("click", function (event) {
const replyLink =
event.target.closest && event.target.closest(".compose-reply-link");
if (replyLink) {
const replyRef = replyLink.closest(".compose-reply-ref");
const targetId = String(
(replyRef && replyRef.dataset && replyRef.dataset.replyTargetId) || ""
).trim();
if (!targetId) {
return;
}
const targetRow = rowByMessageId(targetId);
if (!targetRow) {
return;
}
targetRow.scrollIntoView({ behavior: "smooth", block: "center" });
flashReplyTarget(targetRow);
return;
}
const replyButton =
event.target.closest && event.target.closest(".compose-reply-btn");
if (!replyButton) {
return;
}
const row = replyButton.closest(".compose-row");
if (!row) {
return;
}
setReplyTarget(row.dataset.messageId || "", row.dataset.replySnippet || "");
textarea.focus();
});
};
const init = function () {
bindThreadEvents();
bindContextSelectors();
applyTyping(core.parseJsonSafe(panel.dataset.initialTyping || "{}", {}));
ensureEmptyState();
scrollToBottom(true);
setupWebSocket();
state.pollTimer = setInterval(function () {
if (!document.getElementById(config.panelId)) {
if (window.GIAComposePanel) {
window.GIAComposePanel.destroyPanel(config.panelId);
}
return;
}
poll(false);
}, 4000);
};
return {
clearReplyTarget: clearReplyTarget,
init: init,
poll: poll,
queryParams: queryParams,
setBeforeContextReset: function (callback) {
beforeContextReset = callback;
},
};
};
window.GIAComposePanelThread = {
createController: createController,
};
})();

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff