Increase platform abstraction cohesion
This commit is contained in:
@@ -381,6 +381,47 @@
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div
|
||||
id="{{ panel_id }}-availability-summary"
|
||||
class="tags are-small mt-1{% if not availability_summary %} is-hidden{% endif %}"
|
||||
data-summary='{{ availability_summary_json|default:"{}"|escapejs }}'
|
||||
aria-label="Contact availability summary">
|
||||
{% if availability_summary %}
|
||||
<span class="tag is-light {% if availability_summary.state == 'available' %}is-success{% elif availability_summary.state == 'fading' %}is-warning{% elif availability_summary.state == 'unavailable' %}is-danger{% endif %}">
|
||||
{{ availability_summary.state_label }}
|
||||
</span>
|
||||
<span class="tag is-light">{{ availability_summary.service|upper|default:"-" }}</span>
|
||||
{% if availability_summary.ts_label %}
|
||||
<span class="tag is-light">Updated {{ availability_summary.ts_label }}</span>
|
||||
{% endif %}
|
||||
{% if availability_summary.is_cross_service %}
|
||||
<span class="tag is-light">Cross-service fallback</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="compose-history-nav" role="tablist" aria-label="Conversation views">
|
||||
<button
|
||||
type="button"
|
||||
class="compose-history-tab is-active"
|
||||
data-target="thread"
|
||||
aria-selected="true">
|
||||
Thread
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="compose-history-tab"
|
||||
data-target="deleted"
|
||||
aria-selected="false">
|
||||
Deleted
|
||||
<span id="{{ panel_id }}-deleted-count" class="compose-history-count">0</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="{{ panel_id }}-deleted" class="compose-deleted-pane is-hidden">
|
||||
<p id="{{ panel_id }}-deleted-empty" class="compose-empty">No deleted messages noted yet.</p>
|
||||
<div id="{{ panel_id }}-deleted-list" class="compose-deleted-list"></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="{{ panel_id }}-thread"
|
||||
@@ -397,12 +438,14 @@
|
||||
data-quick-insights-url="{{ compose_quick_insights_url }}"
|
||||
data-history-sync-url="{{ compose_history_sync_url }}"
|
||||
data-react-url="{% url 'compose_react' %}"
|
||||
data-capability-reactions="{% if capability_reactions %}1{% else %}0{% endif %}"
|
||||
data-capability-reactions-reason="{{ capability_reactions_reason|default:''|escape }}"
|
||||
data-reaction-actor-prefix="web:{{ request.user.id }}:"
|
||||
data-toggle-command-url="{{ compose_toggle_command_url }}"
|
||||
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 }}" data-message-id="{{ msg.id }}" data-author="{{ msg.author|default:''|escape }}" data-sender-uuid="{{ msg.sender_uuid|default:''|escape }}" data-display-ts="{{ msg.display_ts|escape }}" data-source-service="{{ msg.source_service|default:''|escape }}" data-source-label="{{ msg.source_label|default:''|escape }}" data-source-message-id="{{ msg.source_message_id|default:''|escape }}" data-direction="{% if msg.outgoing %}outgoing{% else %}incoming{% endif %}"{% if msg.reply_to_id %} data-reply-to-id="{{ msg.reply_to_id }}"{% endif %} data-reply-snippet="{{ msg.display_text|default:msg.text|default:''|truncatechars:120|escape }}">
|
||||
<div class="compose-row {% if msg.outgoing %}is-out{% else %}is-in{% endif %}{% if msg.is_deleted %} is-deleted{% endif %}" data-ts="{{ msg.ts }}" data-message-id="{{ msg.id }}" data-author="{{ msg.author|default:''|escape }}" data-sender-uuid="{{ msg.sender_uuid|default:''|escape }}" data-display-ts="{{ msg.display_ts|escape }}" data-source-service="{{ msg.source_service|default:''|escape }}" data-source-label="{{ msg.source_label|default:''|escape }}" data-source-message-id="{{ msg.source_message_id|default:''|escape }}" data-direction="{% if msg.outgoing %}outgoing{% else %}incoming{% endif %}" data-is-deleted="{% if msg.is_deleted %}1{% else %}0{% endif %}" data-deleted-ts="{{ msg.deleted_ts|default:0 }}" data-deleted-display="{{ msg.deleted_display|default:''|escape }}" data-deleted-actor="{{ msg.deleted_actor|default:''|escape }}" data-deleted-source="{{ msg.deleted_source_service|default:''|escape }}" data-edit-count="{{ msg.edit_count|default:0 }}" data-edit-history="{{ msg.edit_history_json|default:'[]'|escapejs }}" data-raw-text="{{ msg.text|default:''|truncatechars:220|escape }}"{% if msg.reply_to_id %} data-reply-to-id="{{ msg.reply_to_id }}"{% endif %} data-reply-snippet="{{ msg.display_text|default:msg.text|default:''|truncatechars:120|escape }}">
|
||||
{% if msg.gap_fragments %}
|
||||
{% with gap=msg.gap_fragments.0 %}
|
||||
<p
|
||||
@@ -460,7 +503,26 @@
|
||||
{% else %}
|
||||
<p class="compose-body compose-image-fallback is-hidden">(no text)</p>
|
||||
{% endif %}
|
||||
{% if service == "signal" or service == "whatsapp" %}
|
||||
{% if msg.edit_count %}
|
||||
<details class="compose-edit-history">
|
||||
<summary>Edited {{ msg.edit_count }} time{% if msg.edit_count != 1 %}s{% endif %}</summary>
|
||||
<ul>
|
||||
{% for edit in msg.edit_history %}
|
||||
<li>
|
||||
{% if edit.edited_display %}{{ edit.edited_display }}{% else %}Unknown time{% endif %}
|
||||
{% if edit.actor %} · {{ edit.actor }}{% endif %}
|
||||
{% if edit.source_service %} · {{ edit.source_service|upper }}{% endif %}
|
||||
<div class="compose-edit-diff">
|
||||
<span class="compose-edit-old">{{ edit.previous_text|default:"(empty)" }}</span>
|
||||
<span class="compose-edit-arrow">→</span>
|
||||
<span class="compose-edit-new">{{ edit.new_text|default:"(empty)" }}</span>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
{% if capability_reactions %}
|
||||
<div class="compose-reaction-actions" data-message-id="{{ msg.id }}">
|
||||
<button type="button" class="compose-react-btn" data-emoji="👍" title="React with thumbs up">👍</button>
|
||||
<button type="button" class="compose-react-btn" data-emoji="❤️" title="React with heart">❤️</button>
|
||||
@@ -495,6 +557,12 @@
|
||||
{% endif %}
|
||||
<p class="compose-msg-meta">
|
||||
{{ msg.display_ts }}{% if msg.author %} · {{ msg.author }}{% endif %}
|
||||
{% if msg.is_edited %}
|
||||
<span class="compose-msg-flag is-edited" title="Message edited{% if msg.last_edit_display %} at {{ msg.last_edit_display }}{% endif %}">edited</span>
|
||||
{% endif %}
|
||||
{% if msg.is_deleted %}
|
||||
<span class="compose-msg-flag is-deleted" title="Deleted{% if msg.deleted_display %} at {{ msg.deleted_display }}{% endif %}{% if msg.deleted_actor %} by {{ msg.deleted_actor }}{% endif %}">deleted</span>
|
||||
{% endif %}
|
||||
{% if msg.read_ts %}
|
||||
<span
|
||||
class="compose-ticks js-receipt-trigger"
|
||||
@@ -561,8 +629,11 @@
|
||||
<input type="hidden" name="failsafe_confirm" value="0">
|
||||
<div class="compose-send-safety">
|
||||
<label class="checkbox is-size-7">
|
||||
<input type="checkbox" class="manual-confirm"> Confirm Send
|
||||
<input type="checkbox" class="manual-confirm"{% if not capability_send %} disabled{% endif %}> Confirm Send
|
||||
</label>
|
||||
{% if not capability_send %}
|
||||
<p class="help is-size-7 has-text-grey">Send disabled: {{ capability_send_reason }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="{{ panel_id }}-reply-banner" class="compose-reply-banner is-hidden">
|
||||
<span class="compose-reply-banner-label">Replying to:</span>
|
||||
@@ -576,7 +647,7 @@
|
||||
name="text"
|
||||
rows="1"
|
||||
placeholder="Type a message. Enter to send, Shift+Enter for newline."></textarea>
|
||||
<button class="button is-link is-light compose-send-btn" type="submit" disabled>
|
||||
<button class="button is-link is-light compose-send-btn" type="submit" disabled{% if not capability_send %} title="{{ capability_send_reason }}"{% endif %}>
|
||||
<span class="icon is-small"><i class="{{ manual_icon_class }}"></i></span>
|
||||
<span>Send</span>
|
||||
</button>
|
||||
@@ -605,6 +676,134 @@
|
||||
padding: 0.65rem;
|
||||
background: linear-gradient(180deg, rgba(248, 250, 252, 0.7), rgba(255, 255, 255, 0.98));
|
||||
}
|
||||
#{{ panel_id }} .compose-history-nav {
|
||||
margin-top: 0.45rem;
|
||||
display: inline-flex;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-history-tab {
|
||||
border: 1px solid rgba(38, 68, 111, 0.24);
|
||||
background: #f3f7fc;
|
||||
color: #2b4364;
|
||||
border-radius: 999px;
|
||||
padding: 0.2rem 0.58rem;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
cursor: pointer;
|
||||
}
|
||||
#{{ panel_id }} .compose-history-tab.is-active {
|
||||
background: #2b4f7a;
|
||||
color: #fff;
|
||||
border-color: #2b4f7a;
|
||||
}
|
||||
#{{ panel_id }} .compose-history-count {
|
||||
margin-left: 0.22rem;
|
||||
display: inline-block;
|
||||
min-width: 1.05rem;
|
||||
text-align: center;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
font-size: 0.62rem;
|
||||
padding: 0.03rem 0.24rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-deleted-pane {
|
||||
margin-top: 0.55rem;
|
||||
margin-bottom: 0.55rem;
|
||||
min-height: 8rem;
|
||||
max-height: 46vh;
|
||||
overflow-y: auto;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
border-radius: 8px;
|
||||
padding: 0.6rem;
|
||||
background: linear-gradient(180deg, rgba(253, 248, 247, 0.85), rgba(255, 255, 255, 0.98));
|
||||
}
|
||||
#{{ panel_id }} .compose-deleted-pane.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
#{{ panel_id }} .compose-deleted-item {
|
||||
border: 1px solid rgba(181, 96, 80, 0.2);
|
||||
border-radius: 8px;
|
||||
padding: 0.4rem 0.5rem;
|
||||
margin-bottom: 0.4rem;
|
||||
background: rgba(255, 248, 247, 0.98);
|
||||
}
|
||||
#{{ panel_id }} .compose-deleted-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
#{{ panel_id }} .compose-deleted-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.28rem;
|
||||
font-size: 0.65rem;
|
||||
color: #7b4c42;
|
||||
margin-bottom: 0.18rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-deleted-preview {
|
||||
margin: 0;
|
||||
font-size: 0.71rem;
|
||||
color: #4b3b38;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
#{{ panel_id }} .compose-deleted-jump {
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-msg-flag {
|
||||
display: inline-block;
|
||||
margin-left: 0.3rem;
|
||||
border-radius: 999px;
|
||||
padding: 0.03rem 0.34rem;
|
||||
font-size: 0.58rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
#{{ panel_id }} .compose-msg-flag.is-edited {
|
||||
color: #7d5010;
|
||||
background: rgba(255, 241, 214, 0.95);
|
||||
border: 1px solid rgba(169, 115, 31, 0.28);
|
||||
}
|
||||
#{{ panel_id }} .compose-msg-flag.is-deleted {
|
||||
color: #842f2f;
|
||||
background: rgba(255, 228, 228, 0.95);
|
||||
border: 1px solid rgba(173, 52, 52, 0.28);
|
||||
}
|
||||
#{{ panel_id }} .compose-edit-history {
|
||||
margin-top: 0.28rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(124, 102, 63, 0.25);
|
||||
background: rgba(255, 252, 245, 0.96);
|
||||
padding: 0.24rem 0.4rem;
|
||||
font-size: 0.64rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-edit-history summary {
|
||||
cursor: pointer;
|
||||
color: #7a5a22;
|
||||
font-weight: 600;
|
||||
}
|
||||
#{{ panel_id }} .compose-edit-history ul {
|
||||
margin: 0.25rem 0 0;
|
||||
padding-left: 1.05rem;
|
||||
}
|
||||
#{{ panel_id }} .compose-edit-diff {
|
||||
margin-top: 0.08rem;
|
||||
display: flex;
|
||||
gap: 0.22rem;
|
||||
align-items: baseline;
|
||||
}
|
||||
#{{ panel_id }} .compose-edit-old {
|
||||
color: #6e6a66;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
#{{ panel_id }} .compose-edit-new {
|
||||
color: #2f4f78;
|
||||
font-weight: 600;
|
||||
}
|
||||
#{{ panel_id }} .compose-row.is-deleted .compose-bubble {
|
||||
border-style: dashed;
|
||||
opacity: 0.96;
|
||||
}
|
||||
#{{ panel_id }} .compose-availability-lane {
|
||||
margin-top: 0.42rem;
|
||||
display: flex;
|
||||
@@ -1932,6 +2131,12 @@
|
||||
const exportClear = document.getElementById(panelId + "-export-clear");
|
||||
const exportBuffer = document.getElementById(panelId + "-export-buffer");
|
||||
const availabilityLane = document.getElementById(panelId + "-availability");
|
||||
const availabilitySummaryNode = document.getElementById(panelId + "-availability-summary");
|
||||
const deletedPane = document.getElementById(panelId + "-deleted");
|
||||
const deletedList = document.getElementById(panelId + "-deleted-list");
|
||||
const deletedEmpty = document.getElementById(panelId + "-deleted-empty");
|
||||
const deletedCountNode = document.getElementById(panelId + "-deleted-count");
|
||||
const historyTabs = Array.from(panel.querySelectorAll(".compose-history-tab"));
|
||||
const csrfToken = "{{ csrf_token }}";
|
||||
if (lightbox && lightbox.parentElement !== document.body) {
|
||||
document.body.appendChild(lightbox);
|
||||
@@ -1976,6 +2181,7 @@
|
||||
rangeStartId: "",
|
||||
rangeEndId: "",
|
||||
rangeMode: "inside",
|
||||
historyView: "thread",
|
||||
};
|
||||
window.giaComposePanels[panelId] = panelState;
|
||||
const triggerButtons = Array.from(panel.querySelectorAll(".js-ai-trigger"));
|
||||
@@ -2014,6 +2220,13 @@
|
||||
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 minuteBucketFromTs = function (tsValue) {
|
||||
const ts = toInt(tsValue);
|
||||
@@ -2848,9 +3061,9 @@
|
||||
|
||||
const QUICK_REACTION_EMOJIS = ["👍", "❤️", "😂", "😮", "😢", "😡"];
|
||||
const supportsReactions = function () {
|
||||
const service = String(thread.dataset.service || "").trim().toLowerCase();
|
||||
const reactUrl = String(thread.dataset.reactUrl || "").trim();
|
||||
return !!reactUrl && (service === "signal" || service === "whatsapp");
|
||||
const capability = String(thread.dataset.capabilityReactions || "").trim() === "1";
|
||||
return !!reactUrl && capability;
|
||||
};
|
||||
const reactionActorKeyForService = function (service) {
|
||||
const prefix = String(thread.dataset.reactionActorPrefix || "web::");
|
||||
@@ -2977,6 +3190,46 @@
|
||||
bar.appendChild(menu);
|
||||
return bar;
|
||||
};
|
||||
const renderEditHistoryDetails = function (bubble, msg) {
|
||||
if (!bubble) {
|
||||
return;
|
||||
}
|
||||
const rows = Array.isArray(msg && msg.edit_history) ? msg.edit_history : [];
|
||||
if (!rows.length) {
|
||||
return;
|
||||
}
|
||||
const details = document.createElement("details");
|
||||
details.className = "compose-edit-history";
|
||||
const summary = document.createElement("summary");
|
||||
summary.textContent = "Edited " + rows.length + (rows.length === 1 ? " time" : " times");
|
||||
details.appendChild(summary);
|
||||
const list = document.createElement("ul");
|
||||
rows.forEach(function (entry) {
|
||||
const li = document.createElement("li");
|
||||
const editedDisplay = String((entry && entry.edited_display) || "").trim() || "Unknown time";
|
||||
const actor = String((entry && entry.actor) || "").trim();
|
||||
const source = String((entry && entry.source_service) || "").trim();
|
||||
li.textContent = editedDisplay + (actor ? (" · " + actor) : "") + (source ? (" · " + source.toUpperCase()) : "");
|
||||
const diff = document.createElement("div");
|
||||
diff.className = "compose-edit-diff";
|
||||
const oldNode = document.createElement("span");
|
||||
oldNode.className = "compose-edit-old";
|
||||
oldNode.textContent = String((entry && entry.previous_text) || "(empty)");
|
||||
const arrow = document.createElement("span");
|
||||
arrow.className = "compose-edit-arrow";
|
||||
arrow.textContent = "->";
|
||||
const newNode = document.createElement("span");
|
||||
newNode.className = "compose-edit-new";
|
||||
newNode.textContent = String((entry && entry.new_text) || "(empty)");
|
||||
diff.appendChild(oldNode);
|
||||
diff.appendChild(arrow);
|
||||
diff.appendChild(newNode);
|
||||
li.appendChild(diff);
|
||||
list.appendChild(li);
|
||||
});
|
||||
details.appendChild(list);
|
||||
bubble.appendChild(details);
|
||||
};
|
||||
|
||||
const appendBubble = function (msg) {
|
||||
const messageId = String(msg && msg.id ? msg.id : "").trim();
|
||||
@@ -2991,6 +3244,9 @@
|
||||
const row = document.createElement("div");
|
||||
const outgoing = !!msg.outgoing;
|
||||
row.className = "compose-row " + (outgoing ? "is-out" : "is-in");
|
||||
if (msg.is_deleted) {
|
||||
row.classList.add("is-deleted");
|
||||
}
|
||||
row.dataset.ts = String(msg.ts || 0);
|
||||
row.dataset.minute = minuteBucketFromTs(msg.ts || 0);
|
||||
row.dataset.replySnippet = normalizeSnippet(
|
||||
@@ -3003,6 +3259,20 @@
|
||||
row.dataset.sourceLabel = String(msg.source_label || "");
|
||||
row.dataset.sourceMessageId = String(msg.source_message_id || "");
|
||||
row.dataset.direction = outgoing ? "outgoing" : "incoming";
|
||||
row.dataset.isDeleted = msg.is_deleted ? "1" : "0";
|
||||
row.dataset.deletedTs = String(msg.deleted_ts || 0);
|
||||
row.dataset.deletedDisplay = String(msg.deleted_display || "");
|
||||
row.dataset.deletedActor = String(msg.deleted_actor || "");
|
||||
row.dataset.deletedSource = String(msg.deleted_source_service || "");
|
||||
row.dataset.editCount = String(msg.edit_count || 0);
|
||||
try {
|
||||
row.dataset.editHistory = JSON.stringify(
|
||||
Array.isArray(msg.edit_history) ? msg.edit_history : []
|
||||
);
|
||||
} catch (_err) {
|
||||
row.dataset.editHistory = "[]";
|
||||
}
|
||||
row.dataset.rawText = String(msg.text || "");
|
||||
if (msg.reply_to_id) {
|
||||
row.dataset.replyToId = String(msg.reply_to_id || "");
|
||||
}
|
||||
@@ -3078,6 +3348,7 @@
|
||||
fallback.textContent = "(no text)";
|
||||
bubble.appendChild(fallback);
|
||||
}
|
||||
renderEditHistoryDetails(bubble, msg);
|
||||
const reactionBar = buildReactionActions(messageId);
|
||||
if (reactionBar) {
|
||||
bubble.appendChild(reactionBar);
|
||||
@@ -3094,6 +3365,22 @@
|
||||
metaText += " · " + String(msg.author);
|
||||
}
|
||||
meta.textContent = metaText;
|
||||
if (msg.is_edited) {
|
||||
const editedFlag = document.createElement("span");
|
||||
editedFlag.className = "compose-msg-flag is-edited";
|
||||
editedFlag.title = "Message edited" + (msg.last_edit_display ? (" at " + String(msg.last_edit_display)) : "");
|
||||
editedFlag.textContent = "edited";
|
||||
meta.appendChild(editedFlag);
|
||||
}
|
||||
if (msg.is_deleted) {
|
||||
const deletedFlag = document.createElement("span");
|
||||
deletedFlag.className = "compose-msg-flag is-deleted";
|
||||
deletedFlag.title = "Deleted"
|
||||
+ (msg.deleted_display ? (" at " + String(msg.deleted_display)) : "")
|
||||
+ (msg.deleted_actor ? (" by " + String(msg.deleted_actor)) : "");
|
||||
deletedFlag.textContent = "deleted";
|
||||
meta.appendChild(deletedFlag);
|
||||
}
|
||||
// Render delivery/read ticks and a small time label when available.
|
||||
if (msg.read_ts) {
|
||||
const tickWrap = document.createElement("span");
|
||||
@@ -3164,6 +3451,7 @@
|
||||
wireImageFallbacks(row);
|
||||
bindReplyReferences(row);
|
||||
updateGlanceFromMessage(msg);
|
||||
renderDeletedList();
|
||||
};
|
||||
|
||||
// Receipt popover (similar to contact info popover)
|
||||
@@ -3444,6 +3732,69 @@
|
||||
availabilityLane.classList.remove("is-hidden");
|
||||
};
|
||||
|
||||
const renderAvailabilitySummary = function (summary, slices) {
|
||||
if (!availabilitySummaryNode) {
|
||||
return;
|
||||
}
|
||||
const rows = Array.isArray(slices) ? slices : [];
|
||||
let source = (
|
||||
summary && typeof summary === "object"
|
||||
) ? Object.assign({}, summary) : {};
|
||||
if (!source.state && rows.length > 0) {
|
||||
const newest = rows.reduce(function (best, item) {
|
||||
if (!best) return item;
|
||||
return Number(item.end_ts || 0) > Number(best.end_ts || 0) ? item : best;
|
||||
}, null);
|
||||
if (newest) {
|
||||
source.state = String(newest.state || "unknown").toLowerCase();
|
||||
source.state_label = source.state.charAt(0).toUpperCase() + source.state.slice(1);
|
||||
source.service = String(newest.service || "").toLowerCase();
|
||||
source.confidence = Number(newest.confidence_end || 0);
|
||||
source.source_kind = String(
|
||||
((newest.payload && newest.payload.inferred_from) || (newest.payload && newest.payload.extended_by) || "")
|
||||
).trim();
|
||||
source.ts = Number(newest.end_ts || 0);
|
||||
source.ts_label = source.ts > 0 ? new Date(source.ts).toLocaleTimeString() : "";
|
||||
}
|
||||
}
|
||||
if (!source.state) {
|
||||
availabilitySummaryNode.classList.add("is-hidden");
|
||||
availabilitySummaryNode.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const state = String(source.state || "unknown").toLowerCase();
|
||||
const service = String(source.service || "").toUpperCase();
|
||||
const stateTag = document.createElement("span");
|
||||
stateTag.className = "tag is-light";
|
||||
if (state === "available") stateTag.classList.add("is-success");
|
||||
if (state === "fading") stateTag.classList.add("is-warning");
|
||||
if (state === "unavailable") stateTag.classList.add("is-danger");
|
||||
stateTag.textContent = String(source.state_label || (state.charAt(0).toUpperCase() + state.slice(1)));
|
||||
|
||||
const serviceTag = document.createElement("span");
|
||||
serviceTag.className = "tag is-light";
|
||||
serviceTag.textContent = service || "-";
|
||||
|
||||
availabilitySummaryNode.innerHTML = "";
|
||||
availabilitySummaryNode.appendChild(stateTag);
|
||||
availabilitySummaryNode.appendChild(serviceTag);
|
||||
|
||||
if (source.ts_label) {
|
||||
const tsTag = document.createElement("span");
|
||||
tsTag.className = "tag is-light";
|
||||
tsTag.textContent = "Updated " + String(source.ts_label);
|
||||
availabilitySummaryNode.appendChild(tsTag);
|
||||
}
|
||||
if (source.is_cross_service) {
|
||||
const fallbackTag = document.createElement("span");
|
||||
fallbackTag.className = "tag is-light";
|
||||
fallbackTag.textContent = "Cross-service fallback";
|
||||
availabilitySummaryNode.appendChild(fallbackTag);
|
||||
}
|
||||
availabilitySummaryNode.classList.remove("is-hidden");
|
||||
};
|
||||
|
||||
const applyTyping = function (typingPayload) {
|
||||
if (!typingNode || !typingPayload || typeof typingPayload !== "object") {
|
||||
return;
|
||||
@@ -3481,6 +3832,9 @@
|
||||
if (payload.availability_slices) {
|
||||
renderAvailabilitySlices(payload.availability_slices);
|
||||
}
|
||||
if (payload.availability_summary || payload.availability_slices) {
|
||||
renderAvailabilitySummary(payload.availability_summary, payload.availability_slices);
|
||||
}
|
||||
if (payload.last_ts !== undefined && payload.last_ts !== null) {
|
||||
lastTs = Math.max(lastTs, toInt(payload.last_ts));
|
||||
thread.dataset.lastTs = String(lastTs);
|
||||
@@ -3521,6 +3875,9 @@
|
||||
if (payload.availability_slices) {
|
||||
renderAvailabilitySlices(payload.availability_slices);
|
||||
}
|
||||
if (payload.availability_summary || payload.availability_slices) {
|
||||
renderAvailabilitySummary(payload.availability_summary, payload.availability_slices);
|
||||
}
|
||||
if (payload.last_ts !== undefined && payload.last_ts !== null) {
|
||||
lastTs = Math.max(lastTs, toInt(payload.last_ts));
|
||||
thread.dataset.lastTs = String(lastTs);
|
||||
@@ -3544,6 +3901,7 @@
|
||||
const armInput = form.querySelector("input[name='failsafe_arm']");
|
||||
const confirmInput = form.querySelector("input[name='failsafe_confirm']");
|
||||
const sendButton = form.querySelector(".compose-send-btn");
|
||||
const sendCapabilityEnabled = {{ capability_send|yesno:"true,false" }};
|
||||
const updateManualSafety = function () {
|
||||
const confirm = !!(manualConfirm && manualConfirm.checked);
|
||||
if (armInput) {
|
||||
@@ -3553,7 +3911,7 @@
|
||||
confirmInput.value = confirm ? "1" : "0";
|
||||
}
|
||||
if (sendButton) {
|
||||
sendButton.disabled = !confirm;
|
||||
sendButton.disabled = (!confirm) || (!sendCapabilityEnabled);
|
||||
}
|
||||
};
|
||||
if (manualConfirm) {
|
||||
@@ -3561,14 +3919,21 @@
|
||||
}
|
||||
updateManualSafety();
|
||||
try {
|
||||
const initialTyping = JSON.parse("{{ typing_state_json|escapejs }}");
|
||||
applyTyping(initialTyping);
|
||||
try {
|
||||
const initialSlices = JSON.parse(String((availabilityLane && availabilityLane.dataset.slices) || "[]"));
|
||||
renderAvailabilitySlices(initialSlices);
|
||||
} catch (err) {
|
||||
renderAvailabilitySlices([]);
|
||||
}
|
||||
const initialTyping = JSON.parse("{{ typing_state_json|escapejs }}");
|
||||
applyTyping(initialTyping);
|
||||
try {
|
||||
const initialSlices = JSON.parse(
|
||||
String((availabilityLane && availabilityLane.dataset.slices) || "[]")
|
||||
);
|
||||
const initialSummary = JSON.parse(
|
||||
String((availabilitySummaryNode && availabilitySummaryNode.dataset.summary) || "{}")
|
||||
);
|
||||
renderAvailabilitySlices(initialSlices);
|
||||
renderAvailabilitySummary(initialSummary, initialSlices);
|
||||
} catch (err) {
|
||||
renderAvailabilitySlices([]);
|
||||
renderAvailabilitySummary({}, []);
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore invalid initial typing state payload.
|
||||
}
|
||||
@@ -3692,6 +4057,120 @@
|
||||
const allMessageRows = function () {
|
||||
return Array.from(thread.querySelectorAll(".compose-row[data-message-id]"));
|
||||
};
|
||||
const readRowEditHistory = function (row) {
|
||||
if (!row || !row.dataset) {
|
||||
return [];
|
||||
}
|
||||
const parsed = parseJsonSafe(row.dataset.editHistory || "[]", []);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
};
|
||||
const renderDeletedList = function () {
|
||||
if (!deletedList || !deletedEmpty) {
|
||||
return;
|
||||
}
|
||||
const deletedRows = allMessageRows()
|
||||
.filter(function (row) {
|
||||
return String(row.dataset.isDeleted || "0") === "1";
|
||||
})
|
||||
.sort(function (a, b) {
|
||||
return toInt(b.dataset.deletedTs || 0) - toInt(a.dataset.deletedTs || 0);
|
||||
});
|
||||
if (deletedCountNode) {
|
||||
deletedCountNode.textContent = String(deletedRows.length);
|
||||
}
|
||||
deletedList.innerHTML = "";
|
||||
if (!deletedRows.length) {
|
||||
deletedEmpty.classList.remove("is-hidden");
|
||||
return;
|
||||
}
|
||||
deletedEmpty.classList.add("is-hidden");
|
||||
deletedRows.forEach(function (row) {
|
||||
const messageId = String(row.dataset.messageId || "").trim();
|
||||
const deletedTs = String(row.dataset.deletedDisplay || "").trim() || String(row.dataset.displayTs || "").trim() || "-";
|
||||
const deletedActor = String(row.dataset.deletedActor || "").trim() || "unknown";
|
||||
const deletedSource = String(row.dataset.deletedSource || "").trim() || "unknown";
|
||||
const preview = normalizeSnippet(row.dataset.rawText || row.dataset.replySnippet || "(message deleted)");
|
||||
const edits = readRowEditHistory(row);
|
||||
const card = document.createElement("article");
|
||||
card.className = "compose-deleted-item";
|
||||
const meta = document.createElement("p");
|
||||
meta.className = "compose-deleted-meta";
|
||||
meta.textContent = "Deleted " + deletedTs + " by " + deletedActor + " via " + deletedSource.toUpperCase();
|
||||
card.appendChild(meta);
|
||||
const text = document.createElement("p");
|
||||
text.className = "compose-deleted-preview";
|
||||
text.textContent = preview;
|
||||
card.appendChild(text);
|
||||
if (edits.length) {
|
||||
const details = document.createElement("details");
|
||||
details.className = "compose-edit-history";
|
||||
const summary = document.createElement("summary");
|
||||
summary.textContent = "Edit history (" + edits.length + ")";
|
||||
details.appendChild(summary);
|
||||
const list = document.createElement("ul");
|
||||
edits.forEach(function (entry) {
|
||||
const li = document.createElement("li");
|
||||
const editedDisplay = String((entry && entry.edited_display) || "").trim() || "Unknown time";
|
||||
const actor = String((entry && entry.actor) || "").trim();
|
||||
const source = String((entry && entry.source_service) || "").trim();
|
||||
const oldText = String((entry && entry.previous_text) || "(empty)");
|
||||
const newText = String((entry && entry.new_text) || "(empty)");
|
||||
li.textContent = editedDisplay + (actor ? (" · " + actor) : "") + (source ? (" · " + source.toUpperCase()) : "");
|
||||
const diff = document.createElement("div");
|
||||
diff.className = "compose-edit-diff";
|
||||
const oldNode = document.createElement("span");
|
||||
oldNode.className = "compose-edit-old";
|
||||
oldNode.textContent = oldText;
|
||||
const arrow = document.createElement("span");
|
||||
arrow.className = "compose-edit-arrow";
|
||||
arrow.textContent = "->";
|
||||
const newNode = document.createElement("span");
|
||||
newNode.className = "compose-edit-new";
|
||||
newNode.textContent = newText;
|
||||
diff.appendChild(oldNode);
|
||||
diff.appendChild(arrow);
|
||||
diff.appendChild(newNode);
|
||||
li.appendChild(diff);
|
||||
list.appendChild(li);
|
||||
});
|
||||
details.appendChild(list);
|
||||
card.appendChild(details);
|
||||
}
|
||||
const jump = document.createElement("button");
|
||||
jump.type = "button";
|
||||
jump.className = "button is-light is-small compose-deleted-jump";
|
||||
jump.dataset.targetId = messageId;
|
||||
jump.textContent = "Jump to message";
|
||||
card.appendChild(jump);
|
||||
deletedList.appendChild(card);
|
||||
});
|
||||
};
|
||||
const switchHistoryView = function (viewName) {
|
||||
const target = String(viewName || "thread").trim().toLowerCase() === "deleted"
|
||||
? "deleted"
|
||||
: "thread";
|
||||
panelState.historyView = target;
|
||||
historyTabs.forEach(function (tab) {
|
||||
const active = String(tab.dataset.target || "") === target;
|
||||
tab.classList.toggle("is-active", active);
|
||||
tab.setAttribute("aria-selected", active ? "true" : "false");
|
||||
});
|
||||
if (target === "deleted") {
|
||||
if (thread) {
|
||||
thread.classList.add("is-hidden");
|
||||
}
|
||||
if (deletedPane) {
|
||||
deletedPane.classList.remove("is-hidden");
|
||||
}
|
||||
} else {
|
||||
if (thread) {
|
||||
thread.classList.remove("is-hidden");
|
||||
}
|
||||
if (deletedPane) {
|
||||
deletedPane.classList.add("is-hidden");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const selectedRangeRows = function () {
|
||||
const rows = allMessageRows();
|
||||
@@ -4172,7 +4651,33 @@
|
||||
});
|
||||
});
|
||||
};
|
||||
historyTabs.forEach(function (tab) {
|
||||
tab.addEventListener("click", function () {
|
||||
switchHistoryView(String(tab.dataset.target || "thread"));
|
||||
});
|
||||
});
|
||||
if (deletedList) {
|
||||
deletedList.addEventListener("click", function (ev) {
|
||||
const jumpBtn = ev.target.closest && ev.target.closest(".compose-deleted-jump");
|
||||
if (!jumpBtn) {
|
||||
return;
|
||||
}
|
||||
const targetId = String(jumpBtn.dataset.targetId || "").trim();
|
||||
if (!targetId) {
|
||||
return;
|
||||
}
|
||||
switchHistoryView("thread");
|
||||
const row = rowByMessageId(targetId);
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
row.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
flashReplyTarget(row);
|
||||
});
|
||||
}
|
||||
bindReplyReferences(panel);
|
||||
renderDeletedList();
|
||||
switchHistoryView(panelState.historyView);
|
||||
initExportUi();
|
||||
if (replyClearBtn) {
|
||||
replyClearBtn.addEventListener("click", function () {
|
||||
@@ -4264,6 +4769,16 @@
|
||||
panelState.websocketReady = false;
|
||||
hideAllCards();
|
||||
thread.innerHTML = '<p class="compose-empty">Loading messages...</p>';
|
||||
if (deletedList) {
|
||||
deletedList.innerHTML = "";
|
||||
}
|
||||
if (deletedEmpty) {
|
||||
deletedEmpty.classList.remove("is-hidden");
|
||||
}
|
||||
if (deletedCountNode) {
|
||||
deletedCountNode.textContent = "0";
|
||||
}
|
||||
switchHistoryView("thread");
|
||||
lastTs = 0;
|
||||
thread.dataset.lastTs = "0";
|
||||
panelState.seenMessageIds = new Set();
|
||||
|
||||
Reference in New Issue
Block a user