Fixed context menu right mouse click

Improved People list to get more useable to switch
Added Chats lists
This commit is contained in:
defnax 2026-07-24 23:10:08 +02:00
parent 1ecb2dce7c
commit fcd0953784
4 changed files with 336 additions and 119 deletions

View File

@ -536,25 +536,8 @@ const ChatConversationView = () => {
e.preventDefault();
e.stopPropagation();
ChatHubState.hoveredUser = null;
const rect = e.currentTarget.getBoundingClientRect();
const rightbar = document.querySelector('.chat-hub-rightbar');
if (rightbar) {
const parentRect = rightbar.getBoundingClientRect();
const itemBottom = rect.bottom - parentRect.top;
const estimatedMenuHeight = 310;
let top = itemBottom;
if (itemBottom + estimatedMenuHeight > parentRect.height) {
top = rect.top - parentRect.top - estimatedMenuHeight;
if (top < 10) top = 10;
}
if (ChatHubState.activeMenu && ChatHubState.activeMenu.gxsId === gxsId) {
ChatHubState.activeMenu = null;
} else {
ChatHubState.activeMenu = { gxsId, name, top };
}
m.redraw();
}
ChatHubState.activeMenu = null;
m.redraw();
},
oncontextmenu: (e) => {
e.preventDefault();

View File

@ -7,6 +7,7 @@ const {
fetchIdDetails,
loadGxsIdentities,
loadOwnGxsIds,
preloadAllChatHistory,
syncFilter,
stopStatusPolling,
initializeDistantChat,
@ -28,7 +29,8 @@ const PeopleLayout = () => {
syncFilter(vnode.attrs.tab);
Data.refreshGpgDetails().then(() => m.redraw());
loadGxsIdentities();
loadOwnGxsIds();
loadOwnGxsIds().then(() => preloadAllChatHistory());
preloadAllChatHistory();
window.addEventListener('click', dismissMenu);
// Register for chatEvents to receive live incoming messages

View File

@ -6,99 +6,99 @@ const ownIdsLayout = require('people/people_ownids');
const { CreateIdentity } = ownIdsLayout;
const {
State,
isSystemMsg,
preloadAllChatHistory,
fetchIdDetails,
loadGxsIdentities,
getSafeAvatar,
get64Num,
stopStatusPolling,
initializeDistantChat,
} = require('people/people_state');
function formatRelativeTime(ts) {
if (!ts) return '';
const now = Math.floor(Date.now() / 1000);
const diff = now - ts;
if (diff < 30) return 'Just Now';
if (diff < 3600) return `${Math.floor(diff / 60)} min${Math.floor(diff / 60) > 1 ? 's' : ''}`;
if (diff < 86400) return `${Math.floor(diff / 3600)} hr${Math.floor(diff / 3600) > 1 ? 's' : ''}`;
return `${Math.floor(diff / 86400)} d`;
}
const PeopleSidebar = () => {
return {
oninit: () => {
preloadAllChatHistory();
},
view: () => {
// 1. Get base list based on filter
let baseList = [];
if (State.activeFilter === 'own') {
baseList = peopleUtil.sortIds(State.ownGxsIds) || [];
} else if (State.activeFilter === 'contacts') {
baseList = peopleUtil.contactlist(rs.userList.users) || [];
// 1. Determine list based on mainTab ('people' vs 'chats')
let displayItems = [];
if (State.mainTab === 'people') {
let baseList = [];
if (State.activeFilter === 'own') {
baseList = peopleUtil.sortIds(State.ownGxsIds) || [];
} else if (State.activeFilter === 'contacts') {
baseList = peopleUtil.contactlist(rs.userList.users) || [];
} else {
baseList = peopleUtil.sortUsers(rs.userList.users) || [];
}
displayItems = baseList.filter((item) => {
let name = State.activeFilter === 'own' ? (rs.userList.username(item) || 'Unknown') : (item.mGroupName || 'Unknown');
return name.toLowerCase().includes(State.searchString.toLowerCase());
});
displayItems.sort((a, b) => {
let nameA = State.activeFilter === 'own' ? (rs.userList.username(a) || '') : (a.mGroupName || '');
let nameB = State.activeFilter === 'own' ? (rs.userList.username(b) || '') : (b.mGroupName || '');
return nameA.localeCompare(nameB);
});
} else {
baseList = peopleUtil.sortUsers(rs.userList.users) || [];
// 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)
.map((gxsId) => {
const entry = rs.userList.userMap[gxsId];
const name = entry && entry.name ? entry.name : (rs.userList.username(gxsId) || 'Unknown');
return { mGroupId: gxsId, mGroupName: name };
})
.filter((item) => {
const gxsId = item.mGroupId;
fetchIdDetails(gxsId);
const hist = State.chatHistoryMap && State.chatHistoryMap[gxsId];
const hasRealHistory = Boolean(hist && hist.lastMsg && !isSystemMsg(hist.lastMsg));
if (!hasRealHistory) return false;
const name = item.mGroupName || 'Unknown';
return name.toLowerCase().includes(State.searchString.toLowerCase());
});
// Sort by chat timestamp descending
displayItems.sort((a, b) => {
const histA = State.chatHistoryMap[a.mGroupId];
const histB = State.chatHistoryMap[b.mGroupId];
const detailsA = State.gxsIdToDetailsMap[a.mGroupId];
const detailsB = State.gxsIdToDetailsMap[b.mGroupId];
const timeA = histA ? histA.lastTime : (detailsA ? get64Num(detailsA.mLastUsageTS) : 0);
const timeB = histB ? histB.lastTime : (detailsB ? get64Num(detailsB.mLastUsageTS) : 0);
return timeB - timeA;
});
}
// 2. Apply search filter
const filteredList = baseList.filter((item) => {
let name = '';
if (State.activeFilter === 'own') {
name = rs.userList.username(item) || 'Unknown';
} else {
name = item.mGroupName || 'Unknown';
}
return name.toLowerCase().includes(State.searchString.toLowerCase());
});
// Sort alphabetically by name
filteredList.sort((a, b) => {
let nameA = '';
let nameB = '';
if (State.activeFilter === 'own') {
nameA = rs.userList.username(a) || '';
nameB = rs.userList.username(b) || '';
} else {
nameA = a.mGroupName || '';
nameB = b.mGroupName || '';
}
return nameA.localeCompare(nameB);
});
return m('.people-left-pane', [
// Filter Tabs Group
m('.people-filter-group', [
m(
'button.filter-btn' + (State.activeFilter === 'contacts' ? '.active' : ''),
{
onclick: () => {
m.route.set('/people/MyContacts');
},
},
'Contacts'
),
m(
'button.filter-btn' + (State.activeFilter === 'own' ? '.active' : ''),
{
onclick: () => {
m.route.set('/people/OwnIdentity');
},
},
'My Identities'
),
m(
'button.filter-btn' + (State.activeFilter === 'all' ? '.active' : ''),
{
onclick: () => {
m.route.set('/people/All');
},
},
'All'
),
]),
// Create Identity container (only shown for "My Identities")
State.activeFilter === 'own' &&
m('.create-id-container', [
m(
'button.create-id-btn.blue',
{
onclick: () => widget.popupMessage(m(CreateIdentity)),
},
[m('i.fas.fa-plus-circle'), ' Create New Identity']
),
]),
// Search bar
m('.friends-list-container', [
m('.searchbar-container', [
m('input.searchbar[type=text][placeholder=Search Identities...]', {
// Sidebar Header Container
m('.people-sidebar-header', [
// 1. Top Search Bar
m('.searchbar-wrapper', [
m('i.fas.fa-search'),
m('input.searchbar-input[type=text][placeholder=Search...]', {
value: State.searchString,
oninput: (e) => {
State.searchString = e.target.value;
@ -106,13 +106,75 @@ const PeopleSidebar = () => {
}),
]),
// Scrollable list
// 2. Dual Segmented Tab Control: [People] | [Chats]
m('.segmented-control', [
m(
'button.segment-tab' + (State.mainTab === 'people' ? '.active' : ''),
{
onclick: () => {
State.mainTab = 'people';
m.redraw();
},
},
[m('i.fas.fa-users'), ' People']
),
m(
'button.segment-tab' + (State.mainTab === 'chats' ? '.active' : ''),
{
onclick: () => {
State.mainTab = 'chats';
preloadAllChatHistory();
m.redraw();
},
},
[m('i.fas.fa-comments'), ' Chats']
),
]),
// 3. Sub-Filter Row (People Tab)
State.mainTab === 'people' &&
m('.sub-filter-row', [
m(
'select.filter-select',
{
value: State.activeFilter,
onchange: (e) => {
State.activeFilter = e.target.value;
m.route.set(
'/people/' +
(State.activeFilter === 'contacts'
? 'MyContacts'
: State.activeFilter === 'own'
? 'OwnIdentity'
: 'All')
);
},
},
[
m('option[value=contacts]', 'Contacts'),
m('option[value=own]', 'My Identities'),
m('option[value=all]', 'All Users'),
]
),
State.activeFilter === 'own' &&
m(
'button.btn-add-id[title=Create New Identity]',
{
onclick: () => widget.popupMessage(m(CreateIdentity)),
},
m('i.fas.fa-plus')
),
]),
]),
// Scrollable List Container
m('.friends-list-container', [
m('.friends-scroll', [
filteredList.length === 0
? m('.network-pane-placeholder', { style: 'padding: 2rem 0;' }, 'No identities found')
: filteredList.map((item) => {
displayItems.length === 0
? m('.network-pane-placeholder', { style: 'padding: 2rem 0;' }, State.mainTab === 'chats' ? 'No active chats' : 'No identities found')
: displayItems.map((item) => {
let gxsId, displayName;
if (State.activeFilter === 'own') {
if (State.mainTab === 'people' && State.activeFilter === 'own') {
gxsId = item;
displayName = rs.userList.username(gxsId) || 'Unknown';
} else {
@ -130,6 +192,65 @@ const PeopleSidebar = () => {
const itemIsContact = itemEntry && itemEntry.isContact;
const itemIsOwn = State.ownGxsIds.includes(gxsId);
const hist = State.chatHistoryMap[gxsId];
const lastTS = hist ? hist.lastTime : (itemDetails ? get64Num(itemDetails.mLastUsageTS) : 0);
const relativeTimeStr = formatRelativeTime(lastTS);
const lastMsgText = hist && hist.lastMsg ? hist.lastMsg : (itemIsOwn ? 'My Identity' : itemIsContact ? 'Saved Contact' : 'Distant Chat');
if (State.mainTab === 'chats') {
return m(
'.chat-item',
{
class: isSelected ? 'selected' : '',
onclick: (e) => {
e.preventDefault();
e.stopPropagation();
State.activeMenu = null;
State.selectedId = gxsId;
State.activeTab = 'chat';
initializeDistantChat();
m.redraw();
},
oncontextmenu: (e) => {
e.preventDefault();
e.stopPropagation();
State.selectedId = gxsId;
const container = document.querySelector('.friends-list-container');
if (container) {
const parentRect = container.getBoundingClientRect();
const top = e.clientY - parentRect.top;
const left = Math.min(Math.max(e.clientX - parentRect.left, 10), 160);
State.activeMenu = { gxsId, displayName, isContact: itemIsContact, top, left };
}
m.redraw();
},
},
[
m('.chat-avatar-wrapper', [
m(peopleUtil.UserAvatar, {
avatar: itemAvatar,
firstLetter: itemFirstLetter,
identityId: gxsId,
size: 40,
}),
m('.status-dot', {
style: {
backgroundColor: itemIsContact || itemIsOwn ? '#22c55e' : '#cbd5e1',
},
}),
]),
m('.chat-info', [
m('.chat-name', displayName),
m('.chat-last-msg', lastMsgText),
]),
m('.chat-meta', [
relativeTimeStr && m('.chat-time', relativeTimeStr),
]),
]
);
}
// People tab list item
return m(
'.friend-list-item',
{
@ -137,18 +258,7 @@ const PeopleSidebar = () => {
onclick: (e) => {
e.preventDefault();
e.stopPropagation();
const rect = e.currentTarget.getBoundingClientRect();
const container = document.querySelector('.friends-list-container');
if (container) {
const parentRect = container.getBoundingClientRect();
const top = rect.bottom - parentRect.top;
if (State.activeMenu && State.activeMenu.gxsId === gxsId) {
State.activeMenu = null;
} else {
State.activeMenu = { gxsId, displayName, isContact: itemIsContact, top };
}
}
State.activeMenu = null;
const idChanged = State.selectedId !== gxsId;
State.selectedId = gxsId;
@ -165,13 +275,14 @@ const PeopleSidebar = () => {
oncontextmenu: (e) => {
e.preventDefault();
e.stopPropagation();
State.selectedId = gxsId;
const rect = e.currentTarget.getBoundingClientRect();
const container = document.querySelector('.friends-list-container');
if (container) {
const parentRect = container.getBoundingClientRect();
const top = rect.bottom - parentRect.top;
State.activeMenu = { gxsId, displayName, isContact: itemIsContact, top };
const top = e.clientY - parentRect.top;
const left = Math.min(Math.max(e.clientX - parentRect.left, 10), 160);
State.activeMenu = { gxsId, displayName, isContact: itemIsContact, top, left };
}
m.redraw();
},
@ -206,6 +317,9 @@ const PeopleSidebar = () => {
return m('.people-context-menu', {
style: {
top: `${menu.top}px`,
left: menu.left !== undefined ? `${menu.left}px` : '10px',
position: 'absolute',
zIndex: 1000,
},
onclick: (e) => {
e.stopPropagation();

View File

@ -6,10 +6,12 @@ const peopleUtil = require('people/people_util');
const State = {
searchString: '',
selectedId: null, // GXS ID of the selected identity
mainTab: 'people', // 'people' | 'chats'
activeFilter: 'contacts', // 'all' | 'contacts' | 'own'
gxsIdToDetailsMap: {},
ownGxsIds: [],
gpgToGxsIdMap: {},
chatHistoryMap: {}, // gxsId -> { lastMsg, lastTime }
showMailCompose: false,
activeTab: 'details',
selectedOwnGxsIdForChat: '',
@ -254,6 +256,18 @@ function stopStatusPolling() {
State.distantChatStatus = null;
}
function isSystemMsg(msgText) {
if (!msgText || typeof msgText !== 'string') return true;
const lower = msgText.toLowerCase();
return (
lower.includes('starting distant chat') ||
lower.includes('please wait for secure tunnel') ||
lower.includes('tunnel is secured') ||
lower.includes('chat initiated') ||
lower.includes('closed the conversation')
);
}
function initializeDistantChat() {
if (!State.selectedId || !State.selectedOwnGxsIdForChat) return;
@ -301,6 +315,18 @@ function loadChatMessages() {
(data, success) => {
if (success && data.msgs) {
State.chatMessages = data.msgs;
const realUserMsgs = data.msgs.filter(
(m) => !m.isSystem && !isSystemMsg(m.message || m.msg)
);
if (realUserMsgs.length > 0 && State.selectedId) {
const last = realUserMsgs[realUserMsgs.length - 1];
State.chatHistoryMap[State.selectedId] = {
lastMsg: last.message || last.msg || '',
lastTime: last.sendTime || last.recvTime || Math.floor(Date.now() / 1000),
};
} else if (State.selectedId) {
delete State.chatHistoryMap[State.selectedId];
}
m.redraw();
setTimeout(() => {
const element = document.querySelector('.chat-messages');
@ -333,6 +359,12 @@ function sendDistantChatMessage() {
lobby_peer_gxs_id: State.selectedOwnGxsIdForChat,
};
State.chatMessages.push(echoMsg);
if (State.selectedId) {
State.chatHistoryMap[State.selectedId] = {
lastMsg: text,
lastTime: Math.floor(Date.now() / 1000),
};
}
m.redraw();
setTimeout(() => {
const element = document.querySelector('.chat-messages');
@ -353,8 +385,94 @@ function sendDistantChatMessage() {
);
}
function preloadAllChatHistory() {
rs.rsJsonApiRequest('/rsIdentity/getIdentitiesSummaries', {}, (data) => {
const ids = (data && data.ids) ? data.ids : (rs.userList.users || []);
if (!ids || ids.length === 0) return;
ids.forEach((u) => {
const gxsId = typeof u === 'object' ? u.mGroupId : u;
if (!gxsId) return;
// Check Distant Chat History (type: 2)
const distantPeerId = {
broadcast_status_peer_id: '00000000000000000000000000000000',
type: 2, // DISTANT
peer_id: '00000000000000000000000000000000',
distant_chat_id: gxsId,
lobby_id: { xstr64: '0' },
};
rs.rsJsonApiRequest(
'/rsHistory/getMessages',
{
chatPeerId: distantPeerId,
loadCount: 20,
},
(msgData, success) => {
if (success && msgData && msgData.msgs) {
const userMsgs = msgData.msgs.filter(
(m) => !m.isSystem && !isSystemMsg(m.message || m.msg)
);
if (userMsgs.length > 0) {
const last = userMsgs[userMsgs.length - 1];
State.chatHistoryMap[gxsId] = {
lastMsg: last.message || last.msg || '',
lastTime: last.sendTime || last.recvTime || Math.floor(Date.now() / 1000),
};
m.redraw();
}
}
}
);
// Also check Private Chat History (type: 1) if PGP ID is known
const details = State.gxsIdToDetailsMap[gxsId];
const pgpId = details ? details.mPgpId : (typeof u === 'object' ? u.mPgpId : null);
if (pgpId && pgpId !== '0000000000000000') {
const privatePeerId = {
broadcast_status_peer_id: '00000000000000000000000000000000',
type: 1, // PRIVATE
peer_id: pgpId,
distant_chat_id: '00000000000000000000000000000000',
lobby_id: { xstr64: '0' },
};
rs.rsJsonApiRequest(
'/rsHistory/getMessages',
{
chatPeerId: privatePeerId,
loadCount: 20,
},
(msgData, success) => {
if (success && msgData && msgData.msgs) {
const userMsgs = msgData.msgs.filter(
(m) => !m.isSystem && !isSystemMsg(m.message || m.msg)
);
if (userMsgs.length > 0) {
const last = userMsgs[userMsgs.length - 1];
const existing = State.chatHistoryMap[gxsId];
const lastTime = last.sendTime || last.recvTime || Math.floor(Date.now() / 1000);
if (!existing || lastTime > existing.lastTime) {
State.chatHistoryMap[gxsId] = {
lastMsg: last.message || last.msg || '',
lastTime: lastTime,
};
m.redraw();
}
}
}
}
);
}
});
});
}
module.exports = {
State,
isSystemMsg,
preloadAllChatHistory,
fetchIdDetails,
loadGxsIdentities,
loadOwnGxsIds,