dm: make a selected recipient the active conversation in the right pane

Clicking "Message" added the user to the sidebar list but the right-hand
message pane stayed on the previously-active conversation. setDirectMessageRecipient
called setFocusedDmConversation (which only tracks read-state) and never set
activeDmConversationId — the value buildDmConversationData reads to choose which
thread the right pane renders — so it kept resolving the stale id.

Set activeDmConversationId directly (mirroring handleDmConversationSelect), and
switch the mobile pane to the thread for explicit selections (not the auto
thread-default).

Regression: tests/ui/dm-set-recipient-active.test.mjs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-23 21:01:04 -04:00
1 parent ebea4434de
commit ab68ba51d0
2 files changed
+107 -4

No files matched your search

@@ -242,15 +242,24 @@ export class ProfileDirectMessageController {
this.updateMessageThreadSelection(nextRecipient);
if (nextRecipient) {
// Focus this recipient's conversation so a freshly-selected recipient
// (e.g. the "Message" button for someone with no existing thread) isn't
// overridden by a stale activeDmConversationId pointing at the top of the
// inbox.
// Make this recipient's conversation the active one so a freshly-selected
// recipient (e.g. the "Message" button for someone with no existing
// thread) isn't overridden by a stale activeDmConversationId pointing at
// the top of the inbox. setFocusedDmConversation only tracks read-state —
// activeDmConversationId is what buildDmConversationData reads to decide
// which thread the right-hand pane renders, so set it directly (mirroring
// handleDmConversationSelect).
const actor = this.helper.resolveActiveDmActor();
const conversationId = actor
? this.helper.buildDmConversationId(actor, nextRecipient)
: "";
if (conversationId) {
this.activeDmConversationId = conversationId;
// Selecting a recipient explicitly (not the auto thread-default) means
// the user wants to see that thread — switch the mobile pane to it too.
if (reason !== "thread-default") {
this.dmMobileView = "thread";
}
this.setFocusedDmConversation(conversationId);
}
void this.helper.ensureDmRecipientData(nextRecipient);
+94
View File
@@ -0,0 +1,94 @@
// Regression: clicking the "Message" button added the user to the sidebar but
// the right-hand message pane stayed on the previously-active conversation.
// setDirectMessageRecipient relied on setFocusedDmConversation (which only
// tracks read-state) and never updated activeDmConversationId — the value
// buildDmConversationData reads to decide which thread the right pane renders.
//
// Scenario (SCN-dm-select-makes-active):
// Given a controller whose activeDmConversationId points at an old thread,
// When a new recipient is selected via setDirectMessageRecipient,
// Then activeDmConversationId becomes the new recipient's conversation id
// (so the right pane switches), and the mobile pane shows the thread.
import test from "node:test";
import assert from "node:assert/strict";
import { ProfileDirectMessageController } from "../../js/ui/profileModal/ProfileDirectMessageController.js";
// The method does `instanceof HTMLElement` guards; the stubbed DOM refs are null,
// so the constructor just needs the global to exist.
if (typeof globalThis.HTMLElement === "undefined") {
globalThis.HTMLElement = class HTMLElement {};
}
const ACTOR = "a".repeat(64);
const OLD = "b".repeat(64);
const NEW = "c".repeat(64);
function makeFakeController() {
const focusCalls = [];
return {
activeDmConversationId: `dm:${[ACTOR, OLD].sort().join(":")}`,
dmMobileView: "list",
dmPrivacyToggleTouched: true,
directMessagesCache: [],
focusCalls,
mainController: {
normalizeHexPubkey: (v) => (typeof v === "string" && v.trim() ? v.trim() : ""),
state: { setDmRecipient: () => {} },
},
helper: {
resolveActiveDmActor: () => ACTOR,
buildDmConversationId: (actor, remote) =>
`dm:${[actor, remote].sort().join(":")}`,
ensureDmRecipientData: () => {},
},
renderer: {
profileMessagesList: null,
dmAppShellContainer: null,
setMessagesAnnouncement: () => {},
renderDirectMessageConversation: () => {},
renderDmAppShell: () => {},
},
updateMessageThreadSelection() {},
setFocusedDmConversation(id) {
focusCalls.push(id);
},
};
}
test("selecting a recipient makes its conversation active in the right pane", () => {
const ctx = makeFakeController();
const expectedId = `dm:${[ACTOR, NEW].sort().join(":")}`;
const result = ProfileDirectMessageController.prototype.setDirectMessageRecipient.call(
ctx,
NEW,
{ reason: "external" },
);
assert.equal(result, NEW, "returns the normalized recipient");
assert.equal(
ctx.activeDmConversationId,
expectedId,
"activeDmConversationId must move to the selected recipient (not stay on the old thread)",
);
assert.equal(ctx.dmMobileView, "thread", "mobile pane switches to the thread");
assert.deepEqual(
ctx.focusCalls,
[expectedId],
"still tracks read-state for the focused conversation",
);
});
test("the auto thread-default does not force the mobile thread view", () => {
const ctx = makeFakeController();
ProfileDirectMessageController.prototype.setDirectMessageRecipient.call(ctx, NEW, {
reason: "thread-default",
});
assert.equal(
ctx.dmMobileView,
"list",
"thread-default keeps the inbox list visible on mobile",
);
});