Files
bitvid/tests/ui/dm-conversation-focus.test.mjs
T
thePR0M3TH3ANandClaude Opus 4.8 d29287a580 dm: focus the selected recipient instead of the top conversation
Clicking "Message" on a profile (or selecting anyone you'd never DMed) set the
recipient, but buildDmConversationData discarded it: a recipient with no
existing thread wasn't in conversationMap, so it fell back to conversations[0]
(the top conversation). The inbox silently jumped to the top thread, and any
message sent then went to the wrong person ("says sent but I don't see it").

Fixes:
- setDirectMessageRecipient now focuses the selected recipient's conversation
  so a stale activeDmConversationId can't win.
- buildDmConversationData synthesizes an empty conversation for a
  selected-but-threadless recipient and treats it as active, including an
  activeRemotePubkey fallback so the composer targets the right person.
- The legacy renderProfileMessages list no longer clobbers a non-null
  recipient with threadsToRender[0].

Regression: tests/ui/dm-conversation-focus.test.mjs.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-23 20:37:30 -04:00

117 lines
3.8 KiB
JavaScript

// Regression: clicking the "Message" button on the profile of someone you've
// never DMed sets the recipient, but buildDmConversationData used to discard it
// (no existing thread => not in conversationMap => fall back to conversations[0])
// and silently focus the TOP conversation instead. Any message you then sent
// went to the wrong person ("it says sent but I don't see it").
//
// Scenario (SCN-dm-focus-new-recipient):
// Given an inbox with one existing thread (Bob),
// And the user has explicitly selected a brand-new recipient (Carol) with no
// messages yet,
// When the DM conversation data is built,
// Then the active conversation is Carol's (not Bob's), Carol appears as a
// synthesized conversation, and the active remote pubkey is Carol — so the
// composer targets Carol.
import test from "node:test";
import assert from "node:assert/strict";
import { ProfileDirectMessageHelper } from "../../js/ui/profileModal/ProfileDirectMessageHelper.js";
const ACTOR = "a".repeat(64);
const BOB = "b".repeat(64);
const CAROL = "c".repeat(64);
function buildHelper({ storedRecipient }) {
const mainController = {
normalizeHexPubkey: (value) =>
typeof value === "string" && value.trim() ? value.trim() : "",
getActivePubkey: () => ACTOR,
safeEncodeNpub: (hex) => `npub_${hex.slice(0, 6)}`,
formatShortNpub: (npub) => npub,
services: {
nostrClient: {},
getProfileCacheEntry: () => null,
},
state: {
getDmRecipient: () => storedRecipient,
getDmRelayHints: () => [],
getDmRelayPreferences: () => [],
},
nostrService: {
getDirectMessageUnseenCount: () => 0,
},
};
const controller = {
activeDmConversationId: "",
directMessagesCache: [],
};
const helper = new ProfileDirectMessageHelper(mainController, controller);
// Keep the focus-selection logic real; stub only presentation helpers that
// would otherwise require a full profile cache.
helper.resolveProfileSummaryForPubkey = (pubkey) => ({
displayName: `name_${pubkey.slice(0, 4)}`,
avatarSrc: "",
status: "",
lightningAddress: "",
});
helper.resolveDirectMessagePreviewForConversation = () => "preview";
helper.buildDmMessageTimeline = () => [];
return helper;
}
// One existing message thread with Bob.
const MESSAGES = [
{
ok: true,
actorPubkey: ACTOR,
senderPubkey: BOB,
recipientPubkey: ACTOR,
remotePubkey: BOB,
created_at: 1000,
content: "hi from bob",
scheme: "nip04",
},
];
test("focuses a freshly-selected recipient with no thread instead of the top conversation", async () => {
const helper = buildHelper({ storedRecipient: CAROL });
const data = await helper.buildDmConversationData(MESSAGES, { actorPubkey: ACTOR });
const carolId = helper.buildDmConversationId(ACTOR, CAROL);
const bobId = helper.buildDmConversationId(ACTOR, BOB);
assert.equal(
data.activeConversationId,
carolId,
"active conversation must be the selected recipient (Carol), not the top thread (Bob)",
);
assert.notEqual(data.activeConversationId, bobId);
assert.equal(
data.activeRemotePubkey,
CAROL,
"composer target (activeRemotePubkey) must be Carol",
);
assert.ok(
data.conversations.some((c) => c.pubkey === CAROL),
"a synthesized conversation for Carol must be present in the list",
);
});
test("still defaults to the top conversation when no recipient is selected", async () => {
const helper = buildHelper({ storedRecipient: null });
const data = await helper.buildDmConversationData(MESSAGES, { actorPubkey: ACTOR });
const bobId = helper.buildDmConversationId(ACTOR, BOB);
assert.equal(
data.activeConversationId,
bobId,
"with no explicit recipient, default to the most recent thread (Bob)",
);
assert.equal(data.activeRemotePubkey, BOB);
});