webui v167: a stale tunnel poll ended the next conversation

Selecting a contact from a chat room (setSelectedId) changed the
selected identity but left the page-wide chat fields -- chatPid, status,
messages -- pointing at the previous contact's tunnel. The People page
then started its status poll on that pid at mount; the core no longer
knew it, two "false" answers later the poll wrote "tunnel is gone" into
the NEW contact's session and flagged the conversation as ended, while
the real tunnel was still coming up in the desktop window. On a slow link
the poll answers land late enough to overwrite the state of the
conversation opened meanwhile.

setSelectedId now resets the page chat state to the selected contact's
own session; the mount-time poll only starts on a pid that belongs to the
selected contact; and every poll answer is dropped unless it is about the
pid and the contact it was asked for. The "ended" card also says what
happened when the tunnel went away by itself, instead of "you closed it".
This commit is contained in:
jolavillette 2026-08-29 18:50:28 +02:00
parent 1111019861
commit efdc5edd39
4 changed files with 41 additions and 8 deletions

View File

@ -1,7 +1,7 @@
const m = require('mithril');
// Bumped at every change of the web UI (two places used to carry it).
const WEBUI_VERSION = 'v166';
const WEBUI_VERSION = 'v167';
const login = require('login');
const rs = require('rswebui');

View File

@ -13,6 +13,7 @@ const {
startStatusPolling,
stopStatusPolling,
initializeDistantChat,
selectChatContact,
getDistantChatSession,
drainBufferedChatMessages,
markDistantChatRead,
@ -67,12 +68,13 @@ const PeopleLayout = () => {
});
window.addEventListener('click', dismissMenu);
if (State.chatPid && !State.chatDisconnected) {
// Only poll a tunnel that is the selected contact's own; anything
// else is left over from a previous selection.
const selectedSession = State.selectedId ? getDistantChatSession(State.selectedId) : null;
if (State.chatPid && !State.chatDisconnected && selectedSession && selectedSession.pid === State.chatPid) {
// Messages received while the tab was unmounted sit in the event
// queue buffer: pick them up before the first redraw.
if (State.selectedId) {
drainBufferedChatMessages(getDistantChatSession(State.selectedId));
}
drainBufferedChatMessages(selectedSession);
startStatusPolling();
}
},
@ -190,6 +192,7 @@ PeopleLayout.setSelectedId = (id, activeTab = 'details', showCompose = false) =>
State.activeFilter = filter;
State.selectedId = id;
selectChatContact(id);
State.activeTab = activeTab;
State.pendingChatOpen = activeTab === 'chat' ? id : null;
State.mobilePane = 'detail';

View File

@ -121,7 +121,9 @@ const ChatTab = () => {
m('h4', 'Conversation Ended'),
m('p', State.chatCloseFoundNothing
? 'The tunnel was already gone: the core had no connection left to close. Click below to open a new one.'
: 'You have closed the distant chat tunnel. Click below to reconnect.'),
: State.chatEndedByPoll
? 'The tunnel went away: closed by your contact, or dropped by the core. Click below to open a new one.'
: 'You have closed the distant chat tunnel. Click below to reconnect.'),
m('button.blue', {
style: 'margin-top: 1rem; padding: 0.5rem 1.5rem; border-radius: 0.375rem; border: none; font-weight: 600; cursor: pointer;',
onclick: () => initializeDistantChat(),

View File

@ -31,6 +31,7 @@ const State = {
isHistoryLoading: false,
pendingChatOpen: null, // gxsId a chat was explicitly asked for from another page
chatCloseFoundNothing: false, // the core had no connection left to close
chatEndedByPoll: false, // the status poll saw the tunnel go, we did not close it
statusPollFailures: 0, // consecutive getDistantChatStatus answers of false
showEmojiPicker: false,
attachPath: '', // file being hashed for a retroshare:// link
@ -365,14 +366,20 @@ function getStatusTooltip(status) {
function pollDistantChatStatus() {
if (!State.chatPid) return;
const session = State.selectedId ? getDistantChatSession(State.selectedId) : null;
// Captured now: the answer lands seconds later on a slow link, and by then
// the user may be on another contact, or the page on another tunnel. An
// answer about a stale pid used to mark the new conversation as ended.
const pid = State.chatPid;
const askedFor = State.selectedId;
const session = askedFor ? getDistantChatSession(askedFor) : null;
rs.rsJsonApiRequest(
'/rsChats/getDistantChatStatus',
{
pid: State.chatPid,
pid,
},
(detail, success) => {
if (State.chatPid !== pid || State.selectedId !== askedFor) return;
// getDistantChatStatus answers false once the tunnel is gone from the
// core -- died of inaction, closed by the peer, closed by us. Ignoring
// that answer left the last known status on screen for good: a dead
@ -388,6 +395,7 @@ function pollDistantChatStatus() {
State.distantChatStatus = null;
State.chatDisconnected = true;
State.chatCloseFoundNothing = false;
State.chatEndedByPoll = true;
stopStatusPolling();
m.redraw();
}
@ -530,6 +538,7 @@ function openDistantChat(session) {
State.distantChatStatus = null;
State.chatDisconnected = false;
State.chatCloseFoundNothing = false;
State.chatEndedByPoll = false;
State.statusPollFailures = 0;
State.chatInputMsg = session.inputMsg || '';
m.redraw();
@ -695,6 +704,7 @@ function leaveDistantChat(closed) {
State.distantChatStatus = null;
State.chatDisconnected = true;
State.chatCloseFoundNothing = !closed;
State.chatEndedByPoll = false;
State.statusPollFailures = 0;
stopStatusPolling();
m.redraw();
@ -721,6 +731,23 @@ function findDistantChatSession(msgPid) {
return { session, targetGxsId };
}
// The page-wide chat fields (pid, status, messages) belong to one contact at
// a time. Selecting another one must not leave them pointing at the previous
// tunnel: the mount-time poll then asked about a pid the core may have
// dropped, and its "gone" answer ended the new conversation before it began.
function selectChatContact(gxsId) {
stopStatusPolling();
const session = gxsId ? getDistantChatSession(gxsId) : null;
State.chatPid = session ? session.pid : null;
State.chatMessages = session ? session.messages : [];
State.distantChatStatus = session ? session.status : null;
State.chatDisconnected = session ? Boolean(session.disconnected) : false;
State.chatCloseFoundNothing = false;
State.chatEndedByPoll = false;
State.statusPollFailures = 0;
State.chatInputMsg = session ? (session.inputMsg || '') : '';
}
function isDistantChatActive(gxsId) {
const session = gxsId && State.activeDistantChats[gxsId];
return Boolean(
@ -1078,6 +1105,7 @@ module.exports = {
loadChatMessages,
sendDistantChatMessage,
leaveDistantChat,
selectChatContact,
setChatDraft,
switchChatIdentity,
refreshSelectedIdDetails,