Files
bitvid/js/ui/profileModal/ProfileDmAttachmentBinder.js
T
google-labs-jules[bot]andPR0M3TH3AN d22785bb87 [decompose] extract helpers from js/ui/profileModalController.js
Extracted DM Relay UI logic and DM Attachment logic into separate binder modules to reduce the size of ProfileModalController.js. Updated file size baseline.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-02-18 01:03:43 +00:00

244 lines
6.9 KiB
JavaScript

import { formatAttachmentSize } from "../../attachments/attachmentUtils.js";
import { uploadAttachment } from "../../services/attachmentService.js";
import { buildPublicUrl, buildR2Key } from "../../r2.js";
import { devLogger } from "../../utils/logger.js";
const TYPING_INDICATOR_TTL_SECONDS = 15;
const TYPING_INDICATOR_COOLDOWN_MS = 4000;
export function renderAttachmentQueue(controller) {
const list = controller.dmController.profileMessageAttachmentList;
if (!(list instanceof HTMLElement)) {
return;
}
list.textContent = "";
if (!controller.dmController.dmAttachmentQueue.length) {
return;
}
controller.dmController.dmAttachmentQueue.forEach((entry) => {
const item = document.createElement("div");
item.className = "card flex flex-col gap-2 p-3";
item.dataset.attachmentId = entry.id;
const header = document.createElement("div");
header.className = "flex items-center justify-between gap-2";
const title = document.createElement("div");
title.className = "text-sm font-semibold text-text";
title.textContent = entry.name || "Attachment";
header.appendChild(title);
const removeButton = document.createElement("button");
removeButton.type = "button";
removeButton.className = "btn-ghost focus-ring inline-flex items-center";
removeButton.dataset.size = "sm";
removeButton.textContent = "Remove";
removeButton.addEventListener("click", () => {
controller.dmController.dmAttachmentQueue =
controller.dmController.dmAttachmentQueue.filter(
(queued) => queued.id !== entry.id,
);
if (entry.previewUrl && typeof URL !== "undefined") {
URL.revokeObjectURL(entry.previewUrl);
}
renderAttachmentQueue(controller);
});
header.appendChild(removeButton);
item.appendChild(header);
const meta = document.createElement("div");
meta.className = "text-xs text-muted";
const sizeLabel = formatAttachmentSize(entry.size);
meta.textContent = sizeLabel
? `${entry.type || "file"} · ${sizeLabel}`
: entry.type || "file";
item.appendChild(meta);
if (entry.previewUrl && entry.type?.startsWith("image/")) {
const img = document.createElement("img");
img.src = entry.previewUrl;
img.alt = entry.name || "Attachment preview";
img.className = "h-24 w-24 rounded-lg object-cover";
img.loading = "lazy";
img.decoding = "async";
item.appendChild(img);
}
const progress = document.createElement("progress");
progress.className = "progress";
progress.value = entry.progress || 0;
progress.max = 1;
progress.dataset.variant = "surface";
item.appendChild(progress);
const status = document.createElement("div");
status.className = "text-xs text-muted";
status.textContent =
entry.status === "uploading"
? "Uploading…"
: entry.status === "error"
? entry.error || "Upload failed."
: "Ready to upload.";
item.appendChild(status);
list.appendChild(item);
});
}
export async function uploadAttachmentQueue(controller, actorPubkey) {
const r2Service = controller.services.r2Service;
if (!r2Service) {
throw new Error("Storage service unavailable.");
}
const encrypt =
controller.dmController.profileMessageAttachmentEncrypt instanceof
HTMLInputElement
? controller.dmController.profileMessageAttachmentEncrypt.checked
: false;
const payloads = [];
for (const entry of controller.dmController.dmAttachmentQueue) {
entry.status = "uploading";
entry.progress = 0;
renderAttachmentQueue(controller);
try {
const payload = await uploadAttachment({
r2Service,
pubkey: actorPubkey,
file: entry.file,
encrypt,
buildKey: buildR2Key,
buildUrl: buildPublicUrl,
onProgress: (fraction) => {
entry.progress = Number.isFinite(fraction)
? fraction
: entry.progress;
renderAttachmentQueue(controller);
},
});
payloads.push(payload);
entry.status = "uploaded";
entry.progress = 1;
} catch (error) {
entry.status = "error";
entry.error =
error && typeof error.message === "string"
? error.message
: "Attachment upload failed.";
renderAttachmentQueue(controller);
throw error;
}
}
return payloads;
}
export function resolveLatestDirectMessageForRecipient(
controller,
recipientPubkey,
actorPubkey = null,
) {
const normalizedRecipient =
typeof recipientPubkey === "string"
? controller.normalizeHexPubkey(recipientPubkey)
: "";
if (
!normalizedRecipient ||
!Array.isArray(controller.dmController.directMessagesCache)
) {
return null;
}
const resolvedActor = actorPubkey
? controller.normalizeHexPubkey(actorPubkey)
: controller.dmController.resolveActiveDmActor();
let latest = null;
let latestTimestamp = 0;
for (const entry of controller.dmController.directMessagesCache) {
if (
controller.dmController.resolveDirectMessageRemote(
entry,
resolvedActor,
) !== normalizedRecipient
) {
continue;
}
const timestamp = Number(entry?.timestamp) || 0;
if (!latest || timestamp > latestTimestamp) {
latest = entry;
latestTimestamp = timestamp;
}
}
return latest;
}
export async function maybePublishTypingIndicator(controller) {
const settings = controller.dmController.getDmPrivacySettingsSnapshot();
if (!settings.typingIndicatorsEnabled) {
return;
}
if (
!controller.services.nostrClient ||
typeof controller.services.nostrClient.publishDmTypingIndicator !==
"function"
) {
return;
}
const input = controller.dmController.profileMessageInput;
const messageText =
input instanceof HTMLTextAreaElement ? input.value.trim() : "";
if (!messageText) {
return;
}
const recipient = controller.dmController.resolveActiveDmRecipient();
if (!recipient) {
return;
}
const now = Date.now();
if (
now - controller.dmController.dmTypingLastSentAt <
TYPING_INDICATOR_COOLDOWN_MS
) {
return;
}
controller.dmController.dmTypingLastSentAt = now;
const relayHints =
controller.dmController.buildDmRecipientContext(recipient)?.relayHints ||
[];
const latestMessage = resolveLatestDirectMessageForRecipient(
controller,
recipient,
controller.dmController.resolveActiveDmActor(),
);
const latestEventId =
controller.dmController.resolveDirectMessageEventId(latestMessage);
try {
await controller.services.nostrClient.publishDmTypingIndicator({
recipientPubkey: recipient,
conversationEventId: latestEventId || null,
relays: relayHints,
expiresInSeconds: TYPING_INDICATOR_TTL_SECONDS,
});
} catch (error) {
devLogger.warn(
"[profileModal] Failed to publish typing indicator:",
error,
);
}
}