Files
bitvid/js/ui/dm/MessageThread.js
T
google-labs-jules[bot] d16808bcdd Refactor DM Zap button to Composer area
Moves the Zap button from the message thread header to the message composer area, adjacent to the Send button. Refactors the Zap interface creation logic into a standalone component `ZapInterface.js` to decouple it from `MessageThread.js`. Updates `Composer.js` to support an optional `onZap` handler and renders the button with an icon-only style matching the video player's accent interaction. Updates `AppShell.js` to orchestrate the Zap interface display.
2026-01-21 20:35:14 +00:00

173 lines
5.1 KiB
JavaScript

import { DayDivider } from "./DayDivider.js";
import { MessageBubble } from "./MessageBubble.js";
import { formatZapAmount } from "./zapHelpers.js";
function createElement(doc, tag, className, text) {
const element = doc.createElement(tag);
if (className) {
element.className = className;
}
if (typeof text === "string") {
element.textContent = text;
}
return element;
}
export function MessageThread({
document: doc,
contact = {},
messages = [],
state = "idle",
errorType = "",
privacyMode = "nip04",
zapReceipts = [],
conversationZapTotalSats = 0,
profileZapTotalSats = 0,
onMarkRead,
onSendZap,
onBack,
zapConfig = null,
} = {}) {
if (!doc) {
throw new Error("MessageThread requires a document reference.");
}
const root = createElement(doc, "section", "dm-message-thread");
const header = createElement(doc, "header", "dm-message-thread__header");
if (typeof onBack === "function") {
const backBtn = createElement(doc, "button", "dm-message-thread__back btn-ghost btn-icon");
backBtn.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>`;
backBtn.ariaLabel = "Back to conversations";
backBtn.addEventListener("click", (e) => {
e.preventDefault();
onBack();
});
header.appendChild(backBtn);
}
header.appendChild(
createElement(
doc,
"h3",
"dm-message-thread__title",
contact.name || "Direct Messages",
),
);
if (contact.status) {
header.appendChild(
createElement(doc, "span", "dm-message-thread__status", contact.status),
);
}
const normalizedPrivacy =
typeof privacyMode === "string" ? privacyMode.trim().toLowerCase() : "";
const isNip17 = normalizedPrivacy === "nip17" || normalizedPrivacy === "private";
const privacyLabel = `Privacy: ${isNip17 ? "NIP-17" : "NIP-04"}`;
const privacyHint = isNip17
? "NIP-17 gift-wraps your DM so relays only see the wrapper and relay hints."
: "NIP-04 sends a direct encrypted DM; relays can still see sender and recipient metadata.";
const privacyBadge = createElement(
doc,
"span",
"dm-message-thread__status",
privacyLabel,
);
privacyBadge.title = privacyHint;
header.appendChild(privacyBadge);
const zapSummary = createElement(doc, "div", "dm-message-thread__zap-summary");
zapSummary.appendChild(
createElement(doc, "span", "dm-message-thread__zap-label", "Zaps"),
);
zapSummary.appendChild(
createElement(
doc,
"span",
"dm-message-thread__zap-total",
formatZapAmount(conversationZapTotalSats),
),
);
zapSummary.appendChild(
createElement(
doc,
"span",
"dm-message-thread__zap-profile",
`Profile: ${formatZapAmount(profileZapTotalSats)}`,
),
);
header.appendChild(zapSummary);
if (typeof onMarkRead === "function") {
const actions = createElement(doc, "div", "ml-auto flex items-center gap-2");
const markReadButton = createElement(doc, "button", "btn-ghost", "Mark read");
markReadButton.type = "button";
markReadButton.dataset.size = "sm";
const conversationId =
typeof contact.id === "string" ? contact.id.trim() : "";
if (!conversationId) {
markReadButton.disabled = true;
}
markReadButton.addEventListener("click", () => {
onMarkRead(contact);
});
actions.appendChild(markReadButton);
header.appendChild(actions);
}
root.appendChild(header);
const timeline = createElement(doc, "div", "dm-message-thread__timeline");
timeline.setAttribute("role", "log");
timeline.setAttribute("aria-live", "polite");
const newMessagesBanner = createElement(
doc,
"button",
"dm-message-thread__new",
"New messages",
);
newMessagesBanner.type = "button";
newMessagesBanner.setAttribute("aria-label", "Jump to newest messages");
timeline.appendChild(newMessagesBanner);
const stateEl = createElement(doc, "div", "dm-message-thread__state");
if (state === "loading") {
stateEl.textContent = "Decrypting messages…";
timeline.appendChild(stateEl);
} else if (state === "empty") {
stateEl.textContent = "Say hello to start the conversation.";
timeline.appendChild(stateEl);
} else if (state === "error") {
const errorMessage =
errorType === "decrypt"
? "We couldn't decrypt some messages."
: "Something went wrong loading this thread.";
stateEl.textContent = errorMessage;
timeline.appendChild(stateEl);
} else {
messages.forEach((message) => {
if (message.type === "day") {
timeline.appendChild(DayDivider({ document: doc, label: message.label }));
return;
}
const bubble = MessageBubble({
document: doc,
message,
variant: message.direction === "outgoing" ? "outgoing" : "incoming",
});
timeline.appendChild(bubble);
});
}
// Scroll to bottom
if (messages.length > 0) {
setTimeout(() => {
timeline.scrollTop = timeline.scrollHeight;
}, 0);
}
root.appendChild(timeline);
return root;
}