diff --git a/js/ui/profileModal/ProfileDirectMessageActions.js b/js/ui/profileModal/ProfileDirectMessageActions.js index 9dd5cd4d..0e19cfec 100644 --- a/js/ui/profileModal/ProfileDirectMessageActions.js +++ b/js/ui/profileModal/ProfileDirectMessageActions.js @@ -88,7 +88,7 @@ export class ProfileDirectMessageActions { } }, onPublishRelays: async (urls) => { - return this.handleDmSettingsPublish(urls); + return this.controller.handleDmSettingsPublish(urls); }, }); } @@ -170,7 +170,7 @@ export class ProfileDirectMessageActions { const previewUrl = typeof URL !== "undefined" ? URL.createObjectURL(file) : ""; this.controller.dmAttachmentQueue.push({ - id: this.generateAttachmentId(file), + id: this.mainController.generateAttachmentId(file), file, name: file.name, type: file.type || "application/octet-stream", @@ -182,7 +182,7 @@ export class ProfileDirectMessageActions { }); input.value = ""; - this.renderAttachmentQueue(); + this.mainController.renderAttachmentQueue(); } async handleSendProfileMessage() { @@ -250,7 +250,7 @@ export class ProfileDirectMessageActions { let attachmentPayloads = []; if (hasAttachments) { try { - attachmentPayloads = await this.uploadAttachmentQueue( + attachmentPayloads = await this.mainController.uploadAttachmentQueue( this.controller.helper.resolveActiveDmActor(), ); } catch (error) { @@ -279,7 +279,7 @@ export class ProfileDirectMessageActions { if (result?.ok) { input.value = ""; - this.resetAttachmentQueue({ clearInput: true }); + this.mainController.resetAttachmentQueue({ clearInput: true }); this.mainController.showSuccess("Message sent."); if (this.controller.enableNip17RelayWarning && result?.warning === "dm-relays-fallback") { this.mainController.showStatus( @@ -294,7 +294,7 @@ export class ProfileDirectMessageActions { 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.mainController.showError(this.controller.describeDirectMessageSendError(errorCode)); } catch (error) { userLogger.error("[profileModal] Unexpected DM send failure:", error); this.mainController.showError("Unable to send message. Please try again."); @@ -322,7 +322,7 @@ export class ProfileDirectMessageActions { this.setDirectMessageRecipient(remote, { reason: "thread-select" }); } - this.setFocusedDmConversation(conversationId); + this.controller.setFocusedDmConversation(conversationId); await this.controller.renderer.renderDmAppShell(this.controller.directMessagesCache, { actorPubkey: actor, @@ -364,7 +364,7 @@ export class ProfileDirectMessageActions { const recipient = this.controller.helper.resolveRemoteForConversationId(conversationId, actor); const messages = this.controller.helper.getDirectMessagesForConversation(conversationId, actor); if (recipient && messages.length) { - void this.maybePublishReadReceipt(messages, { + void this.controller.maybePublishReadReceipt(messages, { recipientPubkey: recipient, }); } @@ -425,7 +425,7 @@ export class ProfileDirectMessageActions { const recipient = this.controller.helper.resolveRemoteForConversationId(conversationId, actor); const messages = this.controller.helper.getDirectMessagesForConversation(conversationId, actor); if (recipient && messages.length) { - void this.maybePublishReadReceipt(messages, { + void this.controller.maybePublishReadReceipt(messages, { recipientPubkey: recipient, }); } @@ -561,7 +561,7 @@ export class ProfileDirectMessageActions { 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.mainController.showError(this.controller.describeDirectMessageSendError(errorCode)); this.controller.dmComposerState = "error"; } } catch (error) { @@ -582,7 +582,7 @@ export class ProfileDirectMessageActions { const actor = this.controller.helper.resolveActiveDmActor(); if (!actor) { - this.clearProfileMessages(); + this.mainController.clearProfileMessages(); return; } diff --git a/tests/dm-conversation-select.test.mjs b/tests/dm-conversation-select.test.mjs new file mode 100644 index 00000000..01bccfdc --- /dev/null +++ b/tests/dm-conversation-select.test.mjs @@ -0,0 +1,70 @@ +// Scenario (SCN-dm-open-conversation): +// Given the DM contact list is rendered, +// When the user clicks a conversation (onSelectConversation → controller +// delegate → ProfileDirectMessageActions.handleDmConversationSelect), +// Then it focuses that conversation and re-renders the thread WITHOUT throwing +// — i.e. it must call controller/main-controller methods on the right +// receiver, not on the Actions instance (the bug that made opening any +// conversation throw "… is not a function" — KNOWN_BUGS #0 follow-up). + +import test from "node:test"; +import assert from "node:assert/strict"; + +const { ProfileDirectMessageActions } = await import( + "../js/ui/profileModal/ProfileDirectMessageActions.js" +); + +test("handleDmConversationSelect focuses + renders via the correct receivers", async () => { + const calls = []; + const renderer = { + renderDmAppShell: async (...args) => { + calls.push(["renderDmAppShell", args.length]); + }, + }; + const controller = { + dmMobileView: "list", + activeDmConversationId: "", + directMessagesCache: { conversations: [] }, + helper: { + resolveActiveDmActor: () => "actor-pubkey", + resolveRemoteForConversationId: () => "remote-pubkey", + }, + renderer, + setDirectMessageRecipient: (...args) => calls.push(["setDirectMessageRecipient", args[0]]), + setFocusedDmConversation: (id) => calls.push(["setFocusedDmConversation", id]), + }; + const mainController = {}; + + const actions = new ProfileDirectMessageActions(mainController, controller); + // Actions.setDirectMessageRecipient delegates to controller; keep the real one. + + await actions.handleDmConversationSelect({ id: "conv-123" }); + + assert.equal(controller.dmMobileView, "thread", "switches to thread view"); + assert.equal(controller.activeDmConversationId, "conv-123", "sets active conversation"); + assert.ok( + calls.some(([m, v]) => m === "setFocusedDmConversation" && v === "conv-123"), + "focuses the selected conversation on the controller", + ); + assert.ok( + calls.some(([m]) => m === "renderDmAppShell"), + "re-renders the DM thread", + ); +}); + +test("handleDmConversationSelect ignores an empty conversation id", async () => { + const controller = { + dmMobileView: "list", + helper: { + resolveActiveDmActor: () => "actor", + resolveRemoteForConversationId: () => "remote", + }, + renderer: { renderDmAppShell: async () => {} }, + setFocusedDmConversation: () => { + throw new Error("should not focus on empty id"); + }, + }; + const actions = new ProfileDirectMessageActions({}, controller); + await actions.handleDmConversationSelect({ id: " " }); + assert.equal(controller.dmMobileView, "list", "no-op for blank conversation id"); +});