mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-01 18:32:27 +00:00
update
This commit is contained in:
1 parent
589925f1fe
commit
ef29bb6eb3
97 files changed
+46855
No files matched your search
@@ -0,0 +1,949 @@
|
||||
import { devLogger, userLogger } from "../../utils/logger.js";
|
||||
import { extractAttachmentsFromMessage, formatAttachmentSize, describeAttachment } from "../../attachments/attachmentUtils.js";
|
||||
import { downloadAttachment, clearAttachmentCache, getAttachmentCacheStats } from "../../services/attachmentService.js";
|
||||
import { getLinkPreviewSettings, setLinkPreviewAutoFetch } from "../../utils/linkPreviewSettings.js";
|
||||
import { sanitizeRelayList } from "../../nostr/nip46Client.js";
|
||||
import { SHORT_TIMEOUT_MS } from "../../constants.js";
|
||||
|
||||
const TYPING_INDICATOR_TTL_SECONDS = 15;
|
||||
const TYPING_INDICATOR_COOLDOWN_MS = 4000;
|
||||
|
||||
export class ProfileDirectMessageActions {
|
||||
constructor(mainController, controller) {
|
||||
this.mainController = mainController;
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
getDmPrivacySettingsSnapshot(...args) {
|
||||
return this.controller.getDmPrivacySettingsSnapshot(...args);
|
||||
}
|
||||
|
||||
persistDmPrivacySettings(...args) {
|
||||
return this.controller.persistDmPrivacySettings(...args);
|
||||
}
|
||||
|
||||
syncDmPrivacySettingsUi(...args) {
|
||||
return this.controller.syncDmPrivacySettingsUi(...args);
|
||||
}
|
||||
|
||||
focusMessageComposer(...args) {
|
||||
return this.controller.focusMessageComposer(...args);
|
||||
}
|
||||
|
||||
setDirectMessageRecipient(...args) {
|
||||
return this.controller.setDirectMessageRecipient(...args);
|
||||
}
|
||||
|
||||
handleReadReceiptsToggle(enabled) {
|
||||
this.persistDmPrivacySettings({
|
||||
readReceiptsEnabled: Boolean(enabled),
|
||||
});
|
||||
this.syncDmPrivacySettingsUi();
|
||||
this.mainController.showStatus(
|
||||
enabled
|
||||
? "Read receipts enabled for direct messages."
|
||||
: "Read receipts disabled.",
|
||||
);
|
||||
}
|
||||
|
||||
handleTypingIndicatorsToggle(enabled) {
|
||||
this.persistDmPrivacySettings({
|
||||
typingIndicatorsEnabled: Boolean(enabled),
|
||||
});
|
||||
this.syncDmPrivacySettingsUi();
|
||||
this.mainController.showStatus(
|
||||
enabled
|
||||
? "Typing indicators enabled for direct messages."
|
||||
: "Typing indicators disabled.",
|
||||
);
|
||||
}
|
||||
|
||||
handleLinkPreviewToggle(enabled) {
|
||||
setLinkPreviewAutoFetch(Boolean(enabled));
|
||||
}
|
||||
|
||||
openDmSettingsModal() {
|
||||
const owner = this.controller.helper.resolveActiveDmRelayOwner();
|
||||
if (!owner) {
|
||||
this.mainController.showError("Please sign in to manage DM settings.");
|
||||
return;
|
||||
}
|
||||
|
||||
const privacySettings = this.getDmPrivacySettingsSnapshot();
|
||||
const relayHints = this.controller.helper.getActiveDmRelayPreferences();
|
||||
|
||||
this.controller.dmSettingsModalController.show({
|
||||
privacySettings,
|
||||
relayHints,
|
||||
onPrivacyChange: (key, value) => {
|
||||
this.persistDmPrivacySettings({ [key]: value });
|
||||
if (key === "readReceiptsEnabled") {
|
||||
this.mainController.showStatus(
|
||||
value ? "Read receipts enabled." : "Read receipts disabled.",
|
||||
);
|
||||
} else if (key === "typingIndicatorsEnabled") {
|
||||
this.mainController.showStatus(
|
||||
value ? "Typing indicators enabled." : "Typing indicators disabled.",
|
||||
);
|
||||
}
|
||||
},
|
||||
onPublishRelays: async (urls) => {
|
||||
return this.handleDmSettingsPublish(urls);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async handleSendDmRequest() {
|
||||
const recipient = this.controller.helper.resolveActiveDmRecipient();
|
||||
if (!recipient) {
|
||||
this.mainController.showError("Please select a message recipient.");
|
||||
return;
|
||||
}
|
||||
|
||||
const context = await this.controller.helper.ensureDmRecipientData(recipient);
|
||||
this.focusMessageComposer();
|
||||
|
||||
const callback = this.mainController.callbacks.onSendDm;
|
||||
if (callback && callback !== noop) {
|
||||
callback({
|
||||
actorPubkey: this.controller.helper.resolveActiveDmActor(),
|
||||
recipient: context,
|
||||
controller: this,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async handleOpenDmRelaysRequest() {
|
||||
const recipient = this.controller.helper.resolveActiveDmRecipient();
|
||||
if (!recipient) {
|
||||
this.mainController.showError("Please select a message recipient.");
|
||||
return;
|
||||
}
|
||||
|
||||
const context = await this.controller.helper.ensureDmRecipientData(recipient);
|
||||
|
||||
const callback = this.mainController.callbacks.onOpenRelays;
|
||||
if (callback && callback !== noop) {
|
||||
callback({ controller: this, recipient: context });
|
||||
}
|
||||
}
|
||||
|
||||
handlePrivacyToggle(enabled) {
|
||||
const recipientContext = this.controller.helper.buildDmRecipientContext(
|
||||
this.controller.helper.resolveActiveDmRecipient(),
|
||||
);
|
||||
const relayHints = Array.isArray(recipientContext?.relayHints)
|
||||
? recipientContext.relayHints
|
||||
: [];
|
||||
|
||||
if (this.controller.enableNip17RelayWarning && enabled && !relayHints.length) {
|
||||
this.mainController.showStatus(
|
||||
"Privacy warning: this recipient has not shared NIP-17 relays, so we'll use your default relays.",
|
||||
{ autoHideMs: SHORT_TIMEOUT_MS },
|
||||
);
|
||||
}
|
||||
|
||||
this.dmPrivacyToggleTouched = true;
|
||||
this.controller.renderer.updateMessagePrivacyModeDisplay();
|
||||
const callback = this.mainController.callbacks.onTogglePrivacy;
|
||||
if (callback && callback !== noop) {
|
||||
callback({
|
||||
controller: this,
|
||||
enabled: Boolean(enabled),
|
||||
recipient: recipientContext,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleAttachmentSelection() {
|
||||
const input = this.controller.renderer.profileMessageAttachmentInput;
|
||||
if (!(input instanceof HTMLInputElement) || !input.files) {
|
||||
return;
|
||||
}
|
||||
|
||||
const files = Array.from(input.files);
|
||||
if (!files.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
files.forEach((file) => {
|
||||
const previewUrl =
|
||||
typeof URL !== "undefined" ? URL.createObjectURL(file) : "";
|
||||
this.controller.dmAttachmentQueue.push({
|
||||
id: this.generateAttachmentId(file),
|
||||
file,
|
||||
name: file.name,
|
||||
type: file.type || "application/octet-stream",
|
||||
size: file.size,
|
||||
previewUrl,
|
||||
status: "pending",
|
||||
progress: 0,
|
||||
});
|
||||
});
|
||||
|
||||
input.value = "";
|
||||
this.renderAttachmentQueue();
|
||||
}
|
||||
|
||||
async handleSendProfileMessage() {
|
||||
const input = this.controller.renderer.profileMessageInput;
|
||||
if (!(input instanceof HTMLTextAreaElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const message = typeof input.value === "string" ? input.value.trim() : "";
|
||||
const hasAttachments = this.controller.dmAttachmentQueue.length > 0;
|
||||
if (!message && !hasAttachments) {
|
||||
this.mainController.showError("Please enter a message or attach a file.");
|
||||
return;
|
||||
}
|
||||
|
||||
const targetHex = this.controller.helper.resolveActiveDmRecipient();
|
||||
const target =
|
||||
typeof targetHex === "string" && typeof this.mainController.safeEncodeNpub === "function"
|
||||
? this.mainController.safeEncodeNpub(targetHex)
|
||||
: "";
|
||||
if (!target) {
|
||||
this.mainController.showError("Please select a message recipient.");
|
||||
return;
|
||||
}
|
||||
|
||||
const recipientContext = this.controller.helper.buildDmRecipientContext(targetHex);
|
||||
const recipientRelayHints = Array.isArray(recipientContext?.relayHints)
|
||||
? recipientContext.relayHints
|
||||
: [];
|
||||
const useNip17 =
|
||||
this.controller.renderer.profileMessagesPrivacyToggle instanceof HTMLInputElement
|
||||
? this.controller.renderer.profileMessagesPrivacyToggle.checked
|
||||
: false;
|
||||
const senderRelayHints = this.controller.helper.getActiveDmRelayPreferences();
|
||||
|
||||
if (hasAttachments && !useNip17) {
|
||||
this.mainController.showError(
|
||||
"Attachments require NIP-17 delivery. Enable the privacy toggle to send files.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.controller.enableNip17RelayWarning && useNip17 && !recipientRelayHints.length) {
|
||||
this.mainController.showStatus(
|
||||
"Privacy warning: this recipient has not shared NIP-17 relays, so we'll use your default relays.",
|
||||
{ autoHideMs: SHORT_TIMEOUT_MS },
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!this.mainController.services.nostrClient ||
|
||||
typeof this.mainController.services.nostrClient.sendDirectMessage !== "function"
|
||||
) {
|
||||
this.mainController.showError("Direct message service unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
const sendButton = this.controller.renderer.profileMessageSendButton;
|
||||
if (sendButton instanceof HTMLElement && "disabled" in sendButton) {
|
||||
sendButton.disabled = true;
|
||||
sendButton.setAttribute("aria-disabled", "true");
|
||||
}
|
||||
|
||||
try {
|
||||
let attachmentPayloads = [];
|
||||
if (hasAttachments) {
|
||||
try {
|
||||
attachmentPayloads = await this.uploadAttachmentQueue(
|
||||
this.controller.helper.resolveActiveDmActor(),
|
||||
);
|
||||
} catch (error) {
|
||||
const messageText =
|
||||
error && typeof error.message === "string"
|
||||
? error.message
|
||||
: "Attachment upload failed.";
|
||||
this.mainController.showError(messageText);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.mainController.services.nostrClient.sendDirectMessage(
|
||||
target,
|
||||
message,
|
||||
null,
|
||||
useNip17
|
||||
? {
|
||||
useNip17: true,
|
||||
recipientRelayHints,
|
||||
senderRelayHints,
|
||||
attachments: attachmentPayloads,
|
||||
}
|
||||
: {},
|
||||
);
|
||||
|
||||
if (result?.ok) {
|
||||
input.value = "";
|
||||
this.resetAttachmentQueue({ clearInput: true });
|
||||
this.mainController.showSuccess("Message sent.");
|
||||
if (this.controller.enableNip17RelayWarning && result?.warning === "dm-relays-fallback") {
|
||||
this.mainController.showStatus(
|
||||
"Privacy warning: this message used default relays because no NIP-17 relay list was found.",
|
||||
{ autoHideMs: SHORT_TIMEOUT_MS },
|
||||
);
|
||||
}
|
||||
void this.populateProfileMessages({ force: true, reason: "send-message" });
|
||||
return;
|
||||
}
|
||||
|
||||
const errorCode =
|
||||
typeof result?.error === "string" ? result.error : "unknown";
|
||||
userLogger.warn("[profileModal] Failed to send direct message:", errorCode);
|
||||
this.mainController.showError(this.describeDirectMessageSendError(errorCode));
|
||||
} catch (error) {
|
||||
userLogger.error("[profileModal] Unexpected DM send failure:", error);
|
||||
this.mainController.showError("Unable to send message. Please try again.");
|
||||
} finally {
|
||||
this.controller.renderer.updateMessageComposerState();
|
||||
}
|
||||
}
|
||||
|
||||
async handleDmConversationSelect(conversation) {
|
||||
const conversationId =
|
||||
conversation && typeof conversation.id === "string"
|
||||
? conversation.id.trim()
|
||||
: "";
|
||||
if (!conversationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.controller.dmMobileView = "thread";
|
||||
|
||||
const actor = this.controller.helper.resolveActiveDmActor();
|
||||
const remote = this.controller.helper.resolveRemoteForConversationId(conversationId, actor);
|
||||
|
||||
this.controller.activeDmConversationId = conversationId;
|
||||
if (remote) {
|
||||
this.setDirectMessageRecipient(remote, { reason: "thread-select" });
|
||||
}
|
||||
|
||||
this.setFocusedDmConversation(conversationId);
|
||||
|
||||
await this.controller.renderer.renderDmAppShell(this.controller.directMessagesCache, {
|
||||
actorPubkey: actor,
|
||||
});
|
||||
}
|
||||
|
||||
async handleDmConversationMarkRead(conversation) {
|
||||
const conversationId =
|
||||
conversation && typeof conversation.id === "string"
|
||||
? conversation.id.trim()
|
||||
: this.controller.activeDmConversationId;
|
||||
if (!conversationId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actor = this.controller.helper.resolveActiveDmActor();
|
||||
if (
|
||||
!actor ||
|
||||
!this.mainController.nostrService ||
|
||||
typeof this.mainController.nostrService.acknowledgeRenderedDirectMessages !== "function"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const renderedUntil = this.controller.helper.getLatestDirectMessageTimestampForConversation(
|
||||
conversationId,
|
||||
actor,
|
||||
);
|
||||
|
||||
try {
|
||||
await this.mainController.nostrService.acknowledgeRenderedDirectMessages(
|
||||
conversationId,
|
||||
renderedUntil,
|
||||
);
|
||||
} catch (error) {
|
||||
devLogger.warn("[profileModal] Failed to mark conversation read:", error);
|
||||
}
|
||||
|
||||
const recipient = this.controller.helper.resolveRemoteForConversationId(conversationId, actor);
|
||||
const messages = this.controller.helper.getDirectMessagesForConversation(conversationId, actor);
|
||||
if (recipient && messages.length) {
|
||||
void this.maybePublishReadReceipt(messages, {
|
||||
recipientPubkey: recipient,
|
||||
});
|
||||
}
|
||||
|
||||
await this.controller.renderer.renderDmAppShell(this.controller.directMessagesCache, {
|
||||
actorPubkey: actor,
|
||||
});
|
||||
}
|
||||
|
||||
async handleDmMarkAllConversationsRead() {
|
||||
if (
|
||||
!this.mainController.nostrService ||
|
||||
typeof this.mainController.nostrService.acknowledgeRenderedDirectMessages !== "function"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actor = this.controller.helper.resolveActiveDmActor();
|
||||
if (!actor) {
|
||||
return;
|
||||
}
|
||||
|
||||
const summaries =
|
||||
typeof this.mainController.nostrService.listDirectMessageConversationSummaries === "function"
|
||||
? await this.mainController.nostrService.listDirectMessageConversationSummaries()
|
||||
: [];
|
||||
const list = Array.isArray(summaries) ? summaries : [];
|
||||
|
||||
for (const summary of list) {
|
||||
const conversationId =
|
||||
typeof summary?.conversation_id === "string"
|
||||
? summary.conversation_id.trim()
|
||||
: typeof summary?.conversationId === "string"
|
||||
? summary.conversationId.trim()
|
||||
: "";
|
||||
if (!conversationId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const renderedUntil =
|
||||
Number(summary?.last_message_at) ||
|
||||
Number(summary?.downloaded_until) ||
|
||||
Number(summary?.opened_until) ||
|
||||
this.controller.helper.getLatestDirectMessageTimestampForConversation(conversationId, actor);
|
||||
|
||||
try {
|
||||
await this.mainController.nostrService.acknowledgeRenderedDirectMessages(
|
||||
conversationId,
|
||||
renderedUntil,
|
||||
);
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[profileModal] Failed to mark conversation read:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
const recipient = this.controller.helper.resolveRemoteForConversationId(conversationId, actor);
|
||||
const messages = this.controller.helper.getDirectMessagesForConversation(conversationId, actor);
|
||||
if (recipient && messages.length) {
|
||||
void this.maybePublishReadReceipt(messages, {
|
||||
recipientPubkey: recipient,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await this.controller.renderer.renderDmAppShell(this.controller.directMessagesCache, {
|
||||
actorPubkey: actor,
|
||||
});
|
||||
}
|
||||
|
||||
async handleDmAppShellSendMessage(messageText, payload = {}) {
|
||||
const normalizedPayload =
|
||||
payload && typeof payload === "object" ? payload : {};
|
||||
const message =
|
||||
typeof messageText === "string" ? messageText.trim() : "";
|
||||
const attachments = Array.isArray(normalizedPayload.attachments)
|
||||
? normalizedPayload.attachments
|
||||
: [];
|
||||
|
||||
if (!message && !attachments.length) {
|
||||
this.mainController.showError("Please enter a message or attach a file.");
|
||||
this.controller.dmComposerState = "error";
|
||||
await this.controller.renderer.renderDmAppShell(this.controller.directMessagesCache, {
|
||||
actorPubkey: this.controller.helper.resolveActiveDmActor(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const actor = this.controller.helper.resolveActiveDmActor();
|
||||
const activeConversationId =
|
||||
this.controller.activeDmConversationId ||
|
||||
(actor && this.controller.helper.resolveActiveDmRecipient()
|
||||
? this.controller.helper.buildDmConversationId(actor, this.controller.helper.resolveActiveDmRecipient())
|
||||
: "");
|
||||
const targetHex =
|
||||
this.controller.helper.resolveRemoteForConversationId(activeConversationId, actor) ||
|
||||
this.controller.helper.resolveActiveDmRecipient();
|
||||
|
||||
const target =
|
||||
typeof targetHex === "string" && typeof this.mainController.safeEncodeNpub === "function"
|
||||
? this.mainController.safeEncodeNpub(targetHex)
|
||||
: "";
|
||||
if (!target) {
|
||||
this.mainController.showError("Please select a message recipient.");
|
||||
this.controller.dmComposerState = "error";
|
||||
await this.controller.renderer.renderDmAppShell(this.controller.directMessagesCache, {
|
||||
actorPubkey: actor,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!this.mainController.services.nostrClient ||
|
||||
typeof this.mainController.services.nostrClient.sendDirectMessage !== "function"
|
||||
) {
|
||||
this.mainController.showError("Direct message service unavailable.");
|
||||
this.controller.dmComposerState = "error";
|
||||
await this.controller.renderer.renderDmAppShell(this.controller.directMessagesCache, {
|
||||
actorPubkey: actor,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const privacyMode =
|
||||
typeof normalizedPayload.privacyMode === "string"
|
||||
? normalizedPayload.privacyMode.trim().toLowerCase()
|
||||
: "nip04";
|
||||
const useNip17 = privacyMode === "nip17" || privacyMode === "private";
|
||||
|
||||
if (attachments.length && !useNip17) {
|
||||
this.mainController.showError(
|
||||
"Attachments require NIP-17 delivery. Enable the privacy toggle to send files.",
|
||||
);
|
||||
this.controller.dmComposerState = "error";
|
||||
await this.controller.renderer.renderDmAppShell(this.controller.directMessagesCache, {
|
||||
actorPubkey: actor,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const recipientContext = this.controller.helper.buildDmRecipientContext(targetHex);
|
||||
const recipientRelayHints = Array.isArray(recipientContext?.relayHints)
|
||||
? recipientContext.relayHints
|
||||
: [];
|
||||
const senderRelayHints = this.controller.helper.getActiveDmRelayPreferences();
|
||||
|
||||
if (useNip17 && !recipientRelayHints.length) {
|
||||
this.mainController.showStatus(
|
||||
"Privacy warning: this recipient has not shared NIP-17 relays, so we'll use your default relays.",
|
||||
{ autoHideMs: SHORT_TIMEOUT_MS },
|
||||
);
|
||||
}
|
||||
|
||||
this.controller.dmComposerState = "sending";
|
||||
await this.controller.renderer.renderDmAppShell(this.controller.directMessagesCache, {
|
||||
actorPubkey: actor,
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await this.mainController.services.nostrClient.sendDirectMessage(
|
||||
target,
|
||||
message,
|
||||
null,
|
||||
useNip17
|
||||
? {
|
||||
useNip17: true,
|
||||
recipientRelayHints,
|
||||
senderRelayHints,
|
||||
attachments,
|
||||
}
|
||||
: {},
|
||||
);
|
||||
|
||||
if (result?.ok) {
|
||||
this.mainController.showSuccess("Message sent.");
|
||||
if (result?.warning === "dm-relays-fallback") {
|
||||
this.mainController.showStatus(
|
||||
"Privacy warning: this message used default relays because no NIP-17 relay list was found.",
|
||||
{ autoHideMs: SHORT_TIMEOUT_MS },
|
||||
);
|
||||
}
|
||||
if (
|
||||
this.mainController.nostrService &&
|
||||
typeof this.mainController.nostrService.loadDirectMessages === "function"
|
||||
) {
|
||||
await this.mainController.nostrService.loadDirectMessages({
|
||||
actorPubkey: actor,
|
||||
initialLoad: false,
|
||||
});
|
||||
}
|
||||
this.controller.dmComposerState = "idle";
|
||||
} else {
|
||||
const errorCode =
|
||||
typeof result?.error === "string" ? result.error : "unknown";
|
||||
userLogger.warn("[profileModal] Failed to send direct message:", errorCode);
|
||||
this.mainController.showError(this.describeDirectMessageSendError(errorCode));
|
||||
this.controller.dmComposerState = "error";
|
||||
}
|
||||
} catch (error) {
|
||||
userLogger.error("[profileModal] Unexpected DM send failure:", error);
|
||||
this.mainController.showError("Unable to send message. Please try again.");
|
||||
this.controller.dmComposerState = "error";
|
||||
} finally {
|
||||
await this.controller.renderer.renderDmAppShell(this.controller.directMessagesCache, {
|
||||
actorPubkey: actor,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async populateProfileMessages(options = {}) {
|
||||
const settings =
|
||||
options && typeof options === "object" ? options : { force: false };
|
||||
const { force = false } = settings;
|
||||
|
||||
const actor = this.controller.helper.resolveActiveDmActor();
|
||||
if (!actor) {
|
||||
this.clearProfileMessages();
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!this.mainController.nostrService ||
|
||||
typeof this.mainController.nostrService.loadDirectMessages !== "function"
|
||||
) {
|
||||
this.controller.renderer.setMessagesLoadingState("error", {
|
||||
message: "Direct message service unavailable.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!force &&
|
||||
!this.controller.messagesInitialLoadPending &&
|
||||
Array.isArray(this.controller.directMessagesCache) &&
|
||||
this.controller.directMessagesCache.length
|
||||
) {
|
||||
await this.controller.renderer.renderProfileMessages(this.controller.directMessagesCache, {
|
||||
actorPubkey: actor,
|
||||
});
|
||||
this.controller.renderer.setMessagesLoadingState("ready");
|
||||
return;
|
||||
}
|
||||
|
||||
const requestId = Symbol("messagesLoad");
|
||||
this.controller.activeMessagesRequest = requestId;
|
||||
this.controller.messagesInitialLoadPending = false;
|
||||
this.controller.renderer.setMessagesLoadingState("loading");
|
||||
|
||||
if (
|
||||
this.controller.directMessagesLastActor &&
|
||||
this.controller.directMessagesLastActor !== actor
|
||||
) {
|
||||
this.controller.resetDirectMessageSubscription();
|
||||
if (
|
||||
typeof this.mainController.nostrService.clearDirectMessages === "function"
|
||||
) {
|
||||
try {
|
||||
this.mainController.nostrService.clearDirectMessages({ emit: true });
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[profileModal] Failed to clear direct messages cache before reload:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let snapshot = await this.mainController.nostrService.loadDirectMessages({
|
||||
actorPubkey: actor,
|
||||
initialLoad: true,
|
||||
});
|
||||
if (!Array.isArray(snapshot)) {
|
||||
snapshot = [];
|
||||
}
|
||||
|
||||
if (this.controller.activeMessagesRequest !== requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.controller.directMessagesCache = snapshot;
|
||||
this.controller.directMessagesLastActor = actor;
|
||||
|
||||
await this.controller.renderer.renderProfileMessages(snapshot, { actorPubkey: actor });
|
||||
|
||||
if (!snapshot.length) {
|
||||
this.controller.renderer.setMessagesLoadingState("empty");
|
||||
} else {
|
||||
this.controller.renderer.setMessagesLoadingState("ready", {
|
||||
message:
|
||||
snapshot.length === 1
|
||||
? "1 direct message thread loaded."
|
||||
: `${snapshot.length} direct message threads loaded.`,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.controller.activeMessagesRequest === requestId) {
|
||||
userLogger.error(
|
||||
"[profileModal] Failed to load direct messages:",
|
||||
error,
|
||||
);
|
||||
this.controller.renderer.setMessagesLoadingState("error", {
|
||||
message: "Failed to load direct messages. Try again later.",
|
||||
});
|
||||
this.controller.messagesInitialLoadPending = true;
|
||||
}
|
||||
return;
|
||||
} finally {
|
||||
if (this.controller.activeMessagesRequest === requestId) {
|
||||
this.controller.activeMessagesRequest = null;
|
||||
this.controller.renderer.updateMessagesReloadState();
|
||||
}
|
||||
}
|
||||
|
||||
void this.controller.ensureDirectMessageSubscription(actor);
|
||||
}
|
||||
|
||||
resumeProfileMessages() {
|
||||
this.controller.messagesViewActive = true;
|
||||
this.controller.renderer.mountDmAppShell();
|
||||
this.controller.renderer.updateMessagesReloadState();
|
||||
}
|
||||
|
||||
pauseProfileMessages() {
|
||||
this.controller.messagesViewActive = false;
|
||||
this.controller.renderer.unmountDmAppShell();
|
||||
this.controller.renderer.updateMessagesReloadState();
|
||||
}
|
||||
|
||||
handleDirectMessagesUpdated(detail = {}) {
|
||||
if (
|
||||
this.controller.activeMessagesRequest &&
|
||||
detail?.reason !== "load-incremental"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messages = Array.isArray(detail?.messages)
|
||||
? detail.messages
|
||||
: [];
|
||||
this.controller.directMessagesCache = messages;
|
||||
|
||||
const actor = this.controller.helper.resolveActiveDmActor();
|
||||
if (!actor) {
|
||||
this.controller.renderer.setMessagesLoadingState("unauthenticated");
|
||||
this.controller.renderer.clearDirectMessagesUpdateQueue();
|
||||
return;
|
||||
}
|
||||
|
||||
this.controller.directMessagesLastActor = actor;
|
||||
this.controller.renderer.scheduleDirectMessagesRender({
|
||||
messages,
|
||||
actorPubkey: actor,
|
||||
reason: typeof detail?.reason === "string" ? detail.reason : "",
|
||||
});
|
||||
}
|
||||
|
||||
handleDirectMessagesCleared() {
|
||||
if (this.controller.activeMessagesRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.controller.renderer.clearDirectMessagesUpdateQueue();
|
||||
this.controller.directMessagesCache = [];
|
||||
this.setDirectMessageRecipient(null, { reason: "clear" });
|
||||
if (this.controller.renderer.profileMessagesList instanceof HTMLElement) {
|
||||
this.controller.renderer.profileMessagesList.textContent = "";
|
||||
this.controller.renderer.profileMessagesList.classList.add("hidden");
|
||||
this.controller.renderer.profileMessagesList.setAttribute("hidden", "");
|
||||
}
|
||||
|
||||
const actor = this.controller.helper.resolveActiveDmActor();
|
||||
if (!actor) {
|
||||
this.controller.renderer.setMessagesLoadingState("unauthenticated");
|
||||
} else {
|
||||
this.controller.renderer.setMessagesLoadingState("empty");
|
||||
}
|
||||
|
||||
if (this.controller.renderer.dmAppShellContainer instanceof HTMLElement) {
|
||||
void this.controller.renderer.renderDmAppShell(this.controller.directMessagesCache, {
|
||||
actorPubkey: actor,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleDirectMessagesError(detail = {}) {
|
||||
const error = detail?.error || detail?.failure || detail;
|
||||
const reason = detail?.context?.reason || "";
|
||||
const errorCode = error?.code || "";
|
||||
const errorMessage =
|
||||
typeof error === "string"
|
||||
? error
|
||||
: typeof error?.message === "string"
|
||||
? error.message
|
||||
: "";
|
||||
const requiresNip44Decryptor =
|
||||
typeof errorMessage === "string" &&
|
||||
errorMessage.includes("Gift wrap events require a NIP-44 decryptor");
|
||||
|
||||
const isBenign =
|
||||
reason === "no-decryptors" ||
|
||||
errorCode === "decryption-failed" ||
|
||||
(typeof error === "string" && error.includes("no-decryptors"));
|
||||
|
||||
if (isBenign) {
|
||||
devLogger.info("[profileModal] Direct message sync info:", error);
|
||||
} else {
|
||||
userLogger.warn(
|
||||
"[profileModal] Direct message sync issue detected:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.controller.activeMessagesRequest) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (requiresNip44Decryptor) {
|
||||
const nip44Message =
|
||||
"NIP-17 direct messages require a NIP-44-capable signer or extension. Unlock or update your extension to continue.";
|
||||
if (!this.controller.directMessagesCache.length) {
|
||||
this.controller.renderer.setMessagesLoadingState("error", {
|
||||
message: nip44Message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.controller.renderer.setMessagesAnnouncement(nip44Message);
|
||||
this.controller.renderer.updateMessagesReloadState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.controller.directMessagesCache.length) {
|
||||
this.controller.renderer.setMessagesLoadingState("error", {
|
||||
message: "Unable to sync direct messages right now.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.controller.renderer.setMessagesAnnouncement("Unable to sync direct messages right now.");
|
||||
this.controller.renderer.updateMessagesReloadState();
|
||||
}
|
||||
|
||||
handleDirectMessagesRelayWarning(detail = {}) {
|
||||
if (!this.controller.enableNip17RelayWarning) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (detail?.warning !== "dm-relays-fallback") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.controller.hasShownRelayWarning) {
|
||||
return;
|
||||
}
|
||||
this.controller.hasShownRelayWarning = true;
|
||||
|
||||
this.mainController.showStatus(
|
||||
"Privacy warning: direct messages are using your default relays because no NIP-17 relay list is available.",
|
||||
{ autoHideMs: SHORT_TIMEOUT_MS },
|
||||
);
|
||||
}
|
||||
|
||||
setActiveDmRelayPreferences(relays = []) {
|
||||
const owner = this.controller.helper.resolveActiveDmRelayOwner();
|
||||
if (!owner || typeof this.mainController.state.setDmRelayPreferences !== "function") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.mainController.state.setDmRelayPreferences(owner, relays);
|
||||
}
|
||||
|
||||
setDmRelayPreferencesStatus(message = "") {
|
||||
if (!(this.controller.renderer.profileMessagesRelayStatus instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = typeof message === "string" ? message.trim() : "";
|
||||
this.controller.renderer.profileMessagesRelayStatus.textContent = text;
|
||||
}
|
||||
|
||||
populateDmRelayPreferences() {
|
||||
if (!(this.controller.renderer.profileMessagesRelayList instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const owner = this.controller.helper.resolveActiveDmRelayOwner();
|
||||
const relays = this.controller.helper.getActiveDmRelayPreferences();
|
||||
|
||||
this.controller.renderer.profileMessagesRelayList.textContent = "";
|
||||
|
||||
if (!owner) {
|
||||
const emptyState = document.createElement("li");
|
||||
emptyState.className =
|
||||
"card border border-dashed border-border/60 p-4 text-center text-xs text-muted";
|
||||
emptyState.textContent = "Sign in to add DM relay hints.";
|
||||
this.controller.renderer.profileMessagesRelayList.appendChild(emptyState);
|
||||
this.setDmRelayPreferencesStatus("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!relays.length) {
|
||||
const emptyState = document.createElement("li");
|
||||
emptyState.className =
|
||||
"card border border-dashed border-border/60 p-4 text-center text-xs text-muted";
|
||||
emptyState.textContent = "No DM relay hints yet.";
|
||||
this.controller.renderer.profileMessagesRelayList.appendChild(emptyState);
|
||||
return;
|
||||
}
|
||||
|
||||
relays.forEach((url) => {
|
||||
const item = document.createElement("li");
|
||||
item.className = "card flex items-center justify-between gap-3 p-3";
|
||||
|
||||
const label = document.createElement("p");
|
||||
label.className = "text-xs font-medium text-text break-all";
|
||||
label.textContent = url;
|
||||
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.type = "button";
|
||||
removeBtn.className = "btn-ghost focus-ring text-xs";
|
||||
removeBtn.dataset.variant = "danger";
|
||||
removeBtn.textContent = "Remove";
|
||||
removeBtn.addEventListener("click", () => {
|
||||
void this.handleRemoveDmRelayPreference(url);
|
||||
});
|
||||
|
||||
item.appendChild(label);
|
||||
item.appendChild(removeBtn);
|
||||
this.controller.renderer.profileMessagesRelayList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
async handleAddDmRelayPreference() {
|
||||
const input = this.controller.renderer.profileMessagesRelayInput;
|
||||
if (!(input instanceof HTMLInputElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const owner = this.controller.helper.resolveActiveDmRelayOwner();
|
||||
if (!owner) {
|
||||
this.mainController.showError("Please sign in to save DM relay hints.");
|
||||
return;
|
||||
}
|
||||
|
||||
const rawValue = typeof input.value === "string" ? input.value.trim() : "";
|
||||
const sanitized = sanitizeRelayList([rawValue]);
|
||||
const relayUrl = sanitized[0];
|
||||
if (!relayUrl) {
|
||||
this.mainController.showError("Enter a valid WSS relay URL.");
|
||||
return;
|
||||
}
|
||||
|
||||
const current = this.controller.helper.getActiveDmRelayPreferences();
|
||||
const next = sanitizeRelayList([...current, relayUrl]);
|
||||
this.setActiveDmRelayPreferences(next);
|
||||
input.value = "";
|
||||
this.populateDmRelayPreferences();
|
||||
this.setDmRelayPreferencesStatus("DM relay hint added.");
|
||||
}
|
||||
|
||||
async handleRemoveDmRelayPreference(url) {
|
||||
const owner = this.controller.helper.resolveActiveDmRelayOwner();
|
||||
if (!owner) {
|
||||
this.mainController.showError("Please sign in to update DM relay hints.");
|
||||
return;
|
||||
}
|
||||
|
||||
const target = typeof url === "string" ? url.trim() : "";
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const current = this.controller.helper.getActiveDmRelayPreferences();
|
||||
const next = current.filter((entry) => entry !== target);
|
||||
this.setActiveDmRelayPreferences(next);
|
||||
this.populateDmRelayPreferences();
|
||||
this.setDmRelayPreferencesStatus("DM relay hint removed.");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user