From 557831a04d71a155699124395f55d394ad5b2f4f Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 9 Oct 2026 20:48:38 -0700 Subject: Messages: a tap on a conversation's face opens THAT person. The avatar's tap handlers were attached with addEventListener, holding the person it was first drawn for; the morph transplants only h()'s on* properties, so when a new message re-sorted the list and the rows were patched in place, a face kept its old handler — a tap on Bill opened someone else's profile, and once that page had opened, a fresh render hid the evidence. The avatar now sets onclick/onpointerdown claimed in _evs (transplanted like any h() handler), and conversation rows are keyed by person so a re-sort moves them. tools/dm-row-resort-test.js (on the old code, Bill's face opened Alice). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Vps2H27rhbG9ZqseGfMrc3 --- src/features/messages.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/features/messages.js b/src/features/messages.js index 90b080b..b3066c0 100644 --- a/src/features/messages.js +++ b/src/features/messages.js @@ -3699,8 +3699,15 @@ export function messagesFeature(ctx) { // and unlike prefetching every row on paint (which cost sixty requests // for twenty rows and starved the profile batch) this only ever asks // about the person actually being tapped. - node.addEventListener('pointerdown', () => { try { prefetchProfilePage(pk); } catch {} }, { passive: true }); - node.addEventListener('click', (e) => { e.stopPropagation(); openProfile(pk); }); + // As on* properties claimed in _evs, the way h() attaches handlers, so + // the morph transplants them: a list that re-sorts (a new message + // lifting a conversation to the top) patches this node into a + // DIFFERENT person's avatar, and an addEventListener'd handler stayed + // behind holding the old person — a tap on Bill's row opened someone + // else's profile, until the next fresh render. + node.onpointerdown = () => { try { prefetchProfilePage(pk); } catch {} }; + node.onclick = (e) => { e.stopPropagation(); openProfile(pk); }; + (node._evs || (node._evs = new Set())).add('onpointerdown').add('onclick'); } return hook('wrapAvatar', pk, node) || hatStamp(pk, node) || node; }; @@ -12407,6 +12414,9 @@ export function messagesFeature(ctx) { ? h('div', { class: 'list' }, shownDms.map((row) => row.g ? wnGroupRow(row.g) : (({ peer, last, unread }) => h('div', { class: 'item chat-thread-row' + (unread ? ' unread' : ''), + // keyed: a new message re-sorts the list, and the row moves + // with its person instead of being repainted as someone else + 'data-key': 'dm:' + peer, onClick: () => { ui.msgView = 'dm'; ui.msgPeer = peer; ui.msgStick = true; render(); }, }, avatar(peer), @@ -12838,6 +12848,7 @@ export function messagesFeature(ctx) { for (const pk of wn.accounts(g).slice(0, 12)) profileOf(pk); return h('div', { class: 'item chat-thread-row' + (unread ? ' unread' : ''), + 'data-key': 'grp:' + g.id, onClick: () => { ui.msgView = 'grp'; ui.msgPeer = g.id; ui.msgStick = true; ui.msgMembers = false; render(); }, }, h('div', { class: 'chat-avatar fallback' }, name.slice(0, 2)), -- cgit v1.2.3