From ab68ba51d03d27a12c34fbbb3025f50d3555e85a Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Tue, 23 Jun 2026 21:01:04 -0400 Subject: [PATCH] dm: make a selected recipient the active conversation in the right pane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../ProfileDirectMessageController.js | 17 +++- tests/ui/dm-set-recipient-active.test.mjs | 94 +++++++++++++++++++ 2 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 tests/ui/dm-set-recipient-active.test.mjs diff --git a/js/ui/profileModal/ProfileDirectMessageController.js b/js/ui/profileModal/ProfileDirectMessageController.js index 3ee263c2..592a0028 100644 --- a/js/ui/profileModal/ProfileDirectMessageController.js +++ b/js/ui/profileModal/ProfileDirectMessageController.js @@ -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); diff --git a/tests/ui/dm-set-recipient-active.test.mjs b/tests/ui/dm-set-recipient-active.test.mjs new file mode 100644 index 00000000..ec57e550 --- /dev/null +++ b/tests/ui/dm-set-recipient-active.test.mjs @@ -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", + ); +});