webui: the send and history callbacks the capture commits missed

The wave's capture fixes covered the network and distant send paths and
the rooms' history -- three siblings kept reading the world at answer
time. The room send echo read this.currentLobby and appended to the
room on screen: after a room switch, the sent message landed in the
wrong room, under the wrong identity, and scrolled it. The distant
initiate's SUCCESS answer clobbered State.chatPid with no guard,
defeating every downstream isCurrentChat check and merging the old
contact's tunnel into the newly opened conversation (its refusal also
answers a null pid, which the poll then chased into "conversation
gone"). And loadChatMessages wrote its preview line -- or deleted one
-- under whatever contact was selected when the answer landed, then
scrolled it; loadOlderChatHistory fired its scroll-fixup done() into
the new conversation. All four now capture and compare.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jolavillette 2026-09-10 11:06:27 +02:00
parent 9a07e14489
commit d411a6c1f1
2 changed files with 46 additions and 12 deletions

View File

@ -1200,6 +1200,11 @@ const ChatLobbyModel = {
},
sendMessage(msg, onsuccess) {
const cid = this.chatId();
// Captured now: the answer can land after a room switch, and the echo,
// its sender identity and the target list must be the room the message
// was typed in -- addMessages() writes into the room on screen.
const askedLobbyId = this.lastLobbyId;
const senderGxsId = this.currentLobby ? this.currentLobby.gxs_id : '';
rs.rsJsonApiRequest(
'/rsChats/sendChat',
@ -1209,11 +1214,18 @@ const ChatLobbyModel = {
},
(data, success) => {
if (success) {
if (this.lastLobbyId !== askedLobbyId) {
// Another room is open: its message array is not this echo's
// home. The message itself was sent; the sender's own line will
// come back through the history on the next visit.
if (onsuccess) onsuccess();
return;
}
const echoMsg = {
chat_id: cid,
msg,
sendTime: Math.floor(Date.now() / 1000),
lobby_peer_gxs_id: this.currentLobby.gxs_id,
lobby_peer_gxs_id: senderGxsId,
};
this.addMessages([echoMsg], true);
if (onsuccess) onsuccess();

View File

@ -528,6 +528,9 @@ function findLiveTunnelIdentity(peerGxsId, done) {
}
function openDistantChat(session) {
// Captured now: the initiate answer can land seconds later, after the
// user moved to another contact.
const askedFor = State.selectedId;
session.pid = null;
session.status = null;
resetSessionMessages(session, [
@ -558,9 +561,19 @@ function openDistantChat(session) {
notify: true,
},
(res) => {
if (res && res.pid) {
const hexPid = rs.idToHex(res.pid);
// A refused initiate (unknown own identity, for one) answers with a
// null id: taking "000...0" for a tunnel makes the status poll chase
// it and declare the conversation gone.
const hexPid = res && res.pid ? rs.idToHex(res.pid) : '';
if (hexPid && !/^0+$/.test(hexPid)) {
// The session keeps its pid whatever is on screen by now; the
// page-wide state and the loads/polls belong to the conversation
// still being looked at. Without this, a late answer clobbered
// State.chatPid and every downstream guard that compares against
// it, merging the old contact's tunnel into the new one's view.
session.pid = hexPid;
if (State.selectedId !== askedFor) return;
State.chatPid = hexPid;
State.distantChatStatus = null;
drainBufferedChatMessages(session);
@ -587,8 +600,10 @@ function loadChatMessages() {
if (!State.chatPid) return;
// Captured now: the answer may come back after the user selected another
// peer, and it must then land in the session it was asked for.
const session = State.selectedId ? getDistantChatSession(State.selectedId) : null;
// peer, and it must then land in the session -- and the Chats preview
// line -- it was asked for.
const askedFor = State.selectedId;
const session = askedFor ? getDistantChatSession(askedFor) : null;
// The current tunnel first, then whatever else the core holds with this
// contact (other identities, direct chat), so the pane shows the whole
// conversation and not only the file of the tunnel just opened.
@ -611,23 +626,27 @@ function loadChatMessages() {
// the live event handler share.
addSessionMessages(session, data.msgs);
if (session.pid === State.chatPid) State.chatMessages = session.messages;
} else {
} else if (State.selectedId === askedFor) {
State.chatMessages = data.msgs;
}
// The preview line and the scroll belong to the contact this was
// asked for: a late answer after a switch must not write the old
// conversation's last message under the new contact's key -- nor
// delete the new contact's entry when the old query was empty.
const realUserMsgs = data.msgs.filter(
(m) => !m.isSystem && !isSystemMsg(m.message || m.msg)
);
if (realUserMsgs.length > 0 && State.selectedId) {
if (realUserMsgs.length > 0 && askedFor) {
const last = realUserMsgs[realUserMsgs.length - 1];
State.chatHistoryMap[State.selectedId] = {
State.chatHistoryMap[askedFor] = {
lastMsg: last.message || last.msg || '',
lastTime: last.sendTime || last.recvTime || Math.floor(Date.now() / 1000),
};
} else if (State.selectedId) {
delete State.chatHistoryMap[State.selectedId];
} else if (askedFor) {
delete State.chatHistoryMap[askedFor];
}
m.redraw();
scrollChatToBottom();
if (State.selectedId === askedFor) scrollChatToBottom();
}
}
);
@ -1085,7 +1104,10 @@ function loadOlderChatHistory(done) {
if (!anyFull) session.historyExhausted = true;
if (session.pid === State.chatPid) State.chatMessages = session.messages;
m.redraw();
if (done) done();
// done() restores a scroll position measured in the pane of the
// conversation that asked; fired after a switch it would perturb the
// new one (same rule as the rooms' loadOlderHistory).
if (done && State.selectedId === gxsId) done();
});
});
return true;