Fixed to not lose distant chat when you navigate to other pages

Added counter to show active distant chats
This commit is contained in:
defnax 2026-07-26 12:03:23 +02:00
parent ec21174f22
commit 70f5fd9975
6 changed files with 273 additions and 18 deletions

View File

@ -9,9 +9,11 @@ const {
loadOwnGxsIds,
preloadAllChatHistory,
syncFilter,
startStatusPolling,
stopStatusPolling,
initializeDistantChat,
} = require('people/people_state');
const PeopleSidebar = require('people/people_sidebar');
const DetailsTab = require('people/people_details_tab');
const ChatTab = require('people/people_chat_tab');
@ -36,9 +38,41 @@ const PeopleLayout = () => {
// Register for chatEvents to receive live incoming messages
rs.events[15].notify = (chatMessage) => {
const msgCid = chatMessage.chat_id;
if (msgCid && msgCid.type === 2 && State.chatPid) {
if (msgCid && msgCid.type === 2) {
const msgPid = rs.idToHex(msgCid.distant_chat_id);
if (msgPid === State.chatPid) {
// Find active session matching this distant chat PID
let session = null;
let targetGxsId = null;
Object.keys(State.activeDistantChats || {}).forEach((id) => {
if (State.activeDistantChats[id] && State.activeDistantChats[id].pid === msgPid) {
session = State.activeDistantChats[id];
targetGxsId = id;
}
});
if (session) {
const isNearDuplicate = session.messages.some(
(m) => (m.msg || m.message) === chatMessage.msg && Math.abs(m.sendTime - chatMessage.sendTime) < 5
);
if (!isNearDuplicate) {
session.messages.push(chatMessage);
session.messages.sort((a, b) => a.sendTime - b.sendTime);
if (targetGxsId) {
State.chatHistoryMap[targetGxsId] = {
lastMsg: chatMessage.msg || chatMessage.message || '',
lastTime: chatMessage.sendTime || Math.floor(Date.now() / 1000),
};
}
m.redraw();
if (State.selectedId === targetGxsId) {
setTimeout(() => {
const element = document.querySelector('.chat-messages');
if (element) element.scrollTop = element.scrollHeight;
}, 100);
}
}
} else if (State.chatPid && msgPid === State.chatPid) {
const isNearDuplicate = State.chatMessages.some(
(m) => (m.msg || m.message) === chatMessage.msg && Math.abs(m.sendTime - chatMessage.sendTime) < 5
);
@ -54,6 +88,10 @@ const PeopleLayout = () => {
}
}
};
if (State.chatPid && !State.chatDisconnected) {
startStatusPolling();
}
},
onremove: () => {
if (rs.events[15]) {
@ -62,6 +100,7 @@ const PeopleLayout = () => {
stopStatusPolling();
window.removeEventListener('click', dismissMenu);
},
onupdate: (vnode) => {
syncFilter(vnode.attrs.tab);
},

View File

@ -141,7 +141,7 @@ const ChatTab = () => {
value: ownId,
onchange: (e) => {
State.selectedOwnGxsIdForChat = e.target.value;
initializeDistantChat();
initializeDistantChat(true);
},
}, State.ownGxsIds.map((id) => m('option', { value: id }, rs.userList.username(id)))),
]);
@ -170,6 +170,9 @@ const ChatTab = () => {
},
(data, success) => {
if (success) {
if (State.selectedId && State.activeDistantChats[State.selectedId]) {
delete State.activeDistantChats[State.selectedId];
}
State.chatPid = null;
State.chatMessages = [];
State.distantChatStatus = null;
@ -182,6 +185,7 @@ const ChatTab = () => {
}
},
}, [
m('i.fas.fa-sign-out-alt'),
'Leave Chat',
]),

View File

@ -35,6 +35,17 @@ const PeopleSidebar = () => {
// 1. Determine list based on mainTab ('people' vs 'chats')
let displayItems = [];
// 0. Compute active chats count (conversations with real message history)
const allUserGroupIds = new Set((rs.userList.users || []).map((u) => u.mGroupId));
Object.keys(State.chatHistoryMap || {}).forEach((id) => allUserGroupIds.add(id));
let activeChatsCount = 0;
allUserGroupIds.forEach((gxsId) => {
const hist = State.chatHistoryMap && State.chatHistoryMap[gxsId];
if (hist && hist.lastMsg && !isSystemMsg(hist.lastMsg)) {
activeChatsCount++;
}
});
if (State.mainTab === 'people') {
let baseList = [];
if (State.activeFilter === 'own') {
@ -57,10 +68,7 @@ const PeopleSidebar = () => {
});
} else {
// Chats Tab: ONLY contacts and identities that have real chat history (ignoring system tunnel status logs)
const userGroupIds = new Set((rs.userList.users || []).map((u) => u.mGroupId));
Object.keys(State.chatHistoryMap || {}).forEach((id) => userGroupIds.add(id));
displayItems = Array.from(userGroupIds)
displayItems = Array.from(allUserGroupIds)
.map((gxsId) => {
const entry = rs.userList.userMap[gxsId];
const name = entry && entry.name ? entry.name : (rs.userList.username(gxsId) || 'Unknown');
@ -127,10 +135,15 @@ const PeopleSidebar = () => {
m.redraw();
},
},
[m('i.fas.fa-comments'), ' Chats']
[
m('i.fas.fa-comments'),
' Chats',
activeChatsCount > 0 && m('span.segment-badge', activeChatsCount),
]
),
]),
// 3. Sub-Filter Row (People Tab)
State.mainTab === 'people' &&
m('.sub-filter-row', [

View File

@ -21,6 +21,7 @@ const State = {
distantChatStatus: null,
statusPollInterval: null,
chatDisconnected: false,
activeDistantChats: {}, // gxsId -> { pid, status, messages, inputMsg, disconnected }
activeMenu: null,
showHistoryModal: false,
historySearchQuery: '',
@ -28,6 +29,21 @@ const State = {
isHistoryLoading: false,
};
function getDistantChatSession(gxsId) {
if (!gxsId) return null;
if (!State.activeDistantChats[gxsId]) {
State.activeDistantChats[gxsId] = {
pid: null,
status: null,
messages: [],
inputMsg: '',
disconnected: false,
};
}
return State.activeDistantChats[gxsId];
}
function fetchIdDetails(gxsId) {
if (!gxsId) return;
if (State.gxsIdToDetailsMap[gxsId] === undefined) {
@ -175,11 +191,6 @@ function syncFilter(tab) {
if (State.activeFilter !== newFilter) {
State.activeFilter = newFilter;
State.selectedId = null;
State.chatPid = null;
State.chatMessages = [];
State.chatInputMsg = '';
State.activeTab = 'details';
}
}
@ -203,6 +214,8 @@ function getStatusTooltip(status) {
function pollDistantChatStatus() {
if (!State.chatPid) return;
const session = State.selectedId ? getDistantChatSession(State.selectedId) : null;
rs.rsJsonApiRequest(
'/rsChats/getDistantChatStatus',
{
@ -211,6 +224,7 @@ function pollDistantChatStatus() {
(detail, success) => {
if (success && detail.retval) {
State.distantChatStatus = detail.info;
if (session) session.status = detail.info;
if (detail.info.status === 2) {
const text = 'Tunnel is secured. You can talk!';
@ -258,7 +272,6 @@ function stopStatusPolling() {
clearInterval(State.statusPollInterval);
State.statusPollInterval = null;
}
State.distantChatStatus = null;
}
function isSystemMsg(msgText) {
@ -273,11 +286,28 @@ function isSystemMsg(msgText) {
);
}
function initializeDistantChat() {
function initializeDistantChat(force = false) {
if (!State.selectedId || !State.selectedOwnGxsIdForChat) return;
State.chatPid = null;
State.chatMessages = [
const session = getDistantChatSession(State.selectedId);
// If chat session is already established/initiating for this peer and not forced/disconnected:
if (!force && session.pid && !session.disconnected) {
State.chatPid = session.pid;
State.chatMessages = session.messages;
State.distantChatStatus = session.status;
State.chatDisconnected = session.disconnected;
loadChatMessages();
pollDistantChatStatus();
startStatusPolling();
return;
}
// Otherwise, start a new tunnel for this peer
session.pid = null;
session.status = null;
session.messages = [
{
incoming: true,
isSystem: true,
@ -285,6 +315,11 @@ function initializeDistantChat() {
sendTime: Math.floor(Date.now() / 1000),
}
];
session.disconnected = false;
State.chatPid = null;
State.chatMessages = session.messages;
State.distantChatStatus = null;
State.chatDisconnected = false;
m.redraw();
@ -297,7 +332,9 @@ function initializeDistantChat() {
},
(res) => {
if (res && res.pid) {
State.chatPid = rs.idToHex(res.pid);
const hexPid = rs.idToHex(res.pid);
session.pid = hexPid;
State.chatPid = hexPid;
State.distantChatStatus = null;
loadChatMessages();
pollDistantChatStatus();
@ -307,6 +344,7 @@ function initializeDistantChat() {
);
}
function loadChatMessages() {
if (!State.chatPid) return;
@ -564,6 +602,7 @@ function loadAllHistoryForSelectedPeer(callback) {
module.exports = {
State,
getDistantChatSession,
isSystemMsg,
preloadAllChatHistory,
loadAllHistoryForSelectedPeer,
@ -586,3 +625,4 @@ module.exports = {
loadChatMessages,
sendDistantChatMessage,
};

View File

@ -58,3 +58,140 @@ img.avatar {
color: green;
cursor: pointer;
}
.people-sidebar-header {
display: flex;
flex-direction: column;
padding: 0.75rem 1rem 0.5rem 1rem;
gap: 0.75rem;
border-bottom: 1px solid #e2e8f0;
background-color: #ffffff;
.searchbar-wrapper {
position: relative;
display: flex;
align-items: center;
i.fa-search {
position: absolute;
left: 0.85rem;
color: #94a3b8;
font-size: 0.9rem;
}
input.searchbar-input {
width: 100%;
padding: 0.5rem 0.75rem 0.5rem 2.25rem;
border: 1px solid #e2e8f0;
border-radius: 0.5rem;
font-size: 0.9rem;
background-color: #f8fafc;
color: #1e293b;
outline: none;
transition: all 0.2s ease;
&:focus {
border-color: #3b82f6;
background-color: #ffffff;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
}
}
.segmented-control {
display: flex;
background-color: #f1f5f9;
padding: 3px;
border-radius: 0.5rem;
gap: 4px;
button.segment-tab {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
font-size: 0.9rem;
font-weight: 600;
color: #64748b;
background: transparent;
border: none;
border-radius: 0.375rem;
cursor: pointer;
box-shadow: none;
transition: all 0.2s ease;
&:hover {
color: #1e293b;
}
&.active {
background-color: #ffffff;
color: #0f172a;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
.segment-badge {
background-color: #019dff;
color: #ffffff;
}
}
.segment-badge {
display: inline-flex;
align-items: center;
justify-content: center;
background-color: #cbd5e1;
color: #334155;
font-size: 0.75rem;
font-weight: 700;
min-width: 1.25rem;
height: 1.25rem;
padding: 0 0.35rem;
border-radius: 9999px;
line-height: 1;
transition: all 0.2s ease;
}
}
}
.sub-filter-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
min-height: 32px;
select.filter-select {
padding: 0.35rem 0.6rem;
border: 1px solid #cbd5e1;
border-radius: 0.375rem;
font-size: 0.85rem;
font-weight: 600;
color: #475569;
background-color: #ffffff;
cursor: pointer;
outline: none;
}
.btn-add-id {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 0.375rem;
background-color: #3b82f6;
color: #ffffff;
border: none;
cursor: pointer;
font-size: 0.9rem;
transition: background-color 0.2s;
&:hover {
background-color: #2563eb;
}
}
}
}

View File

@ -689,6 +689,28 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
}
.people-sidebar-header .segmented-control button.segment-tab .segment-badge {
display: inline-flex;
align-items: center;
justify-content: center;
background-color: #cbd5e1;
color: #334155;
font-size: 0.75rem;
font-weight: 700;
min-width: 1.25rem;
height: 1.25rem;
padding: 0 0.35rem;
border-radius: 9999px;
line-height: 1;
transition: all 0.2s ease;
}
.people-sidebar-header .segmented-control button.segment-tab.active .segment-badge {
background-color: #019dff;
color: #ffffff;
}
/* Sub-header Filter Row */
.people-sidebar-header .sub-filter-row {
display: flex;