fix(dm): route Actions calls to the correct controller (open conversation, etc.)

The controller→Actions split left a batch of method calls in
ProfileDirectMessageActions pointing at `this` (the Actions instance)
instead of the object that actually defines them. Opening a conversation
threw "this.setFocusedDmConversation is not a function"; the same latent
bug also broke DM send-error messaging, read receipts, settings publish,
and the whole attachment flow (generate/render/reset/upload) — the user
just hadn't exercised them yet.

Route each to its real receiver:
- DM-controller methods → this.controller.*:
  handleDmSettingsPublish, describeDirectMessageSendError,
  maybePublishReadReceipt, setFocusedDmConversation
- main-controller methods → this.mainController.*:
  generateAttachmentId, renderAttachmentQueue, resetAttachmentQueue,
  uploadAttachmentQueue, clearProfileMessages

(Builds on the earlier handleDmConversationSelect delegation fix.) A static
sweep confirms every remaining `this.*` call in Actions resolves to an
Actions method or an explicit controller/mainController/helper receiver.

Test: tests/dm-conversation-select.test.mjs (cheat-resistant — drives the
real handleDmConversationSelect with stub receivers; would throw on the old
wrong-receiver code). DM unit + integration suites green; full build OK.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-16 15:07:14 -04:00
1 parent 7951fd0f09
commit bf30233649
2 files changed
+81 -11

No files matched your search

@@ -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;
}
+70
View File
@@ -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");
});