split people code

This commit is contained in:
defnax 2026-07-21 23:53:00 +02:00
parent 44f2907e17
commit 02dc11ec9d
5 changed files with 1069 additions and 988 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,179 @@
const m = require('mithril');
const rs = require('rswebui');
const {
State,
fetchIdDetails,
getStatusColor,
getStatusTooltip,
initializeDistantChat,
sendDistantChatMessage,
stopStatusPolling,
} = require('people/people_state');
const ChatTab = () => {
return {
view: () => {
fetchIdDetails(State.selectedId);
const details = State.selectedId ? State.gxsIdToDetailsMap[State.selectedId] : null;
if (!details) return null;
const name = details.mNickname || details.mGroupName || 'Unknown';
if (State.ownGxsIds.length === 0) {
return m('.chat-warning', [
m('i.fas.fa-exclamation-triangle'),
m('h4', 'No Identities Found'),
m('p', 'You need to create a GXS identity in the "My Identities" tab before you can start distant chats.'),
]);
}
if (State.chatDisconnected) {
return m('.chat-warning', [
m('i.fas.fa-unlink', { style: 'font-size: 2rem; color: #ef4444; margin-bottom: 1rem;' }),
m('h4', 'Conversation Ended'),
m('p', '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(),
}, 'Reconnect'),
]);
}
if (!State.chatPid) {
return m('.chat-warning', [
m('i.fas.fa-spinner.fa-spin'),
m('h4', 'Connecting...'),
m('p', 'Initiating distant chat tunnel to the peer identity...'),
]);
}
const canTalk = State.distantChatStatus && State.distantChatStatus.status === 2;
return m('.network-chat-view', [
m('.chat-identity-select-container', {
style: 'padding: 0.5rem 1rem; background-color: #ffffff; border-bottom: 1px solid #cbd5e1; display: flex; align-items: center; justify-content: space-between; font-size: 0.85rem;',
}, [
m('.chat-tunnel-status', { style: 'display: flex; align-items: center; gap: 0.5rem;' }, [
m('span', { style: 'color: #64748b; font-weight: 500;' }, 'Distant Chat Tunnel'),
m('i.fas.fa-circle', {
style: {
color: getStatusColor(State.distantChatStatus ? State.distantChatStatus.status : 0),
fontSize: '0.85rem',
transition: 'color 0.3s ease',
},
title: getStatusTooltip(State.distantChatStatus ? State.distantChatStatus.status : 0),
}),
]),
m('.chat-actions', { style: 'display: flex; align-items: center; gap: 1rem;' }, [
m('.select-own-profile', [
m('span', { style: 'margin-right: 0.5rem; color: #64748b;' }, 'Chatting as:'),
m('select', {
style: 'padding: 0.25rem 0.5rem; border-radius: 0.25rem; border: 1px solid #cbd5e1; outline: none; background: #f8fafc; font-weight: 600;',
value: State.selectedOwnGxsIdForChat,
onchange: (e) => {
State.selectedOwnGxsIdForChat = e.target.value;
initializeDistantChat();
},
}, State.ownGxsIds.map((id) => m('option', { value: id }, rs.userList.username(id)))),
]),
m('button.red.leave-btn', {
style: 'padding: 0.25rem 0.75rem; border-radius: 0.25rem; font-size: 0.85rem; display: flex; align-items: center; gap: 0.25rem; border: none; cursor: pointer; background-color: #ef4444; color: #ffffff;',
onclick: () => {
if (confirm('Are you sure you want to leave this distant chat conversation?')) {
rs.rsJsonApiRequest(
'/rsChats/closeDistantChatConnexion',
{
pid: State.chatPid,
},
(data, success) => {
if (success) {
State.chatPid = null;
State.chatMessages = [];
State.distantChatStatus = null;
State.chatDisconnected = true;
stopStatusPolling();
m.redraw();
}
}
);
}
},
}, [
m('i.fas.fa-sign-out-alt'),
'Leave Chat',
]),
]),
]),
m('.chat-messages', [
State.chatMessages.length === 0
? m('.chat-warning', [
m('i.fas.fa-comments'),
m('h4', 'No Messages'),
m('p', 'Distant chats are secure and encrypted. Start the conversation by typing a message below.'),
])
: State.chatMessages.map((msg) => {
if (msg.isSystem) {
const text = msg.msg || msg.message;
const isSecured = text.includes('secured') || text.includes('talk');
const bgColor = isSecured ? '#fffbeb' : '#f8fafc';
const borderColor = isSecured ? '#fcd34d' : '#cbd5e1';
const textColor = isSecured ? '#b45309' : '#475569';
const borderStyle = isSecured ? 'solid' : 'dashed';
return m('.chat-bubble-container.incoming', [
m('.chat-sender', 'Chat status'),
m('.chat-bubble', {
style: {
backgroundColor: bgColor,
border: `1px ${borderStyle} ${borderColor}`,
color: textColor,
},
}, text),
m('.chat-time', new Date(msg.sendTime * 1000).toLocaleTimeString()),
]);
}
const isIncoming = msg.incoming;
const senderName = isIncoming ? name : rs.userList.username(State.selectedOwnGxsIdForChat);
return m('.chat-bubble-container' + (isIncoming ? '.incoming' : '.outgoing'), [
m('.chat-sender', senderName),
m('.chat-bubble', msg.msg || msg.message),
m('.chat-time', new Date(msg.sendTime * 1000).toLocaleTimeString()),
]);
}),
]),
m('.chat-input-area', [
m('textarea.chat-textarea', {
placeholder: canTalk ? 'Type your encrypted message here...' : 'Waiting for tunnel to be secured...',
disabled: !canTalk,
value: State.chatInputMsg,
oninput: (e) => {
State.chatInputMsg = e.target.value;
},
onkeydown: (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (canTalk) sendDistantChatMessage();
}
},
}),
m(
'button.send-btn.blue',
{
disabled: !canTalk,
style: !canTalk ? 'opacity: 0.5; cursor: not-allowed;' : '',
onclick: () => {
if (canTalk) sendDistantChatMessage();
},
},
[m('i.fas.fa-paper-plane'), ' Send']
),
]),
]);
},
};
};
module.exports = ChatTab;

View File

@ -0,0 +1,231 @@
const m = require('mithril');
const rs = require('rswebui');
const widget = require('widgets');
const peopleUtil = require('people/people_util');
const ownIdsLayout = require('people/people_ownids');
const { EditIdentity, DeleteIdentity } = ownIdsLayout;
const {
State,
fetchIdDetails,
getSafeAvatar,
get64Num,
createUsageString,
loadGxsIdentities,
initializeDistantChat,
} = require('people/people_state');
const DetailsTab = () => {
return {
view: () => {
fetchIdDetails(State.selectedId);
const details = State.selectedId ? State.gxsIdToDetailsMap[State.selectedId] : null;
if (!details) return null;
const name = details.mNickname || details.mGroupName || 'Unknown';
const isOwn = State.ownGxsIds.includes(State.selectedId);
const entry = rs.userList.userMap[State.selectedId];
const isContact = entry && entry.isContact;
const pgpId = details.mPgpId;
return m('.network-detail-view', [
m('.detail-header', [
m('.avatar-container', {
style: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '0.5rem',
marginRight: '1rem',
},
}, [
m('.friend-avatar', m(peopleUtil.UserAvatar, {
avatar: getSafeAvatar(details),
firstLetter: (name || '?').slice(0, 1).toUpperCase(),
identityId: State.selectedId,
size: 128,
isSquare: true,
})),
m('.identity-votes', {
style: {
display: 'flex',
alignItems: 'center',
gap: '1rem',
marginTop: '0.5rem',
},
}, [
m('.vote-positive', {
style: {
display: 'flex',
alignItems: 'center',
gap: '0.25rem',
color: '#22c55e',
fontSize: '1.25rem',
fontWeight: 'bold',
},
}, [
m('i.fas.fa-thumbs-up'),
m('span', details.mReputation ? details.mReputation.mFriendsPositiveVotes : 0),
]),
m('.vote-negative', {
style: {
display: 'flex',
alignItems: 'center',
gap: '0.25rem',
color: '#ef4444',
fontSize: '1.25rem',
fontWeight: 'bold',
},
}, [
m('i.fas.fa-thumbs-down'),
m('span', details.mReputation ? details.mReputation.mFriendsNegativeVotes : 0),
]),
]),
]),
m('.detail-title', [
m('h2', name),
m('.detail-subtitle', [
m('i.fas.fa-id-card'),
m('span', isOwn ? 'My Identity' : isContact ? 'Saved Contact' : 'Discovered Identity'),
]),
]),
m('.detail-actions', [
isOwn
? [
m(
'button.btn',
{
onclick: () =>
widget.popupMessage(
m(EditIdentity, {
details,
})
),
},
[m('i.fas.fa-edit'), ' Edit']
),
m(
'button.btn.red',
{
onclick: () =>
widget.popupMessage(
m(DeleteIdentity, {
id: details.mId,
name: details.mNickname,
})
),
},
[m('i.fas.fa-trash-alt'), ' Delete']
),
]
: [
m(
'button.btn.blue',
{
onclick: () => {
State.activeTab = 'chat';
initializeDistantChat();
},
},
[m('i.fas.fa-comment-alt'), ' Start Chat']
),
m(
'button.btn',
{
onclick: () => {
State.showMailCompose = true;
},
},
[m('i.fas.fa-envelope'), ' Send Mail']
),
m(
'button.btn' + (isContact ? '.red' : '.blue'),
{
onclick: () => {
rs.rsJsonApiRequest(
'/rsIdentity/setAsRegularContact',
{ id: State.selectedId, isContact: !isContact },
() => {
rs.userList.loadUsers();
loadGxsIdentities();
}
);
},
},
isContact
? [m('i.fas.fa-user-minus'), ' Remove Contact']
: [m('i.fas.fa-user-plus'), ' Add Contact']
),
],
]),
]),
m('.detail-section', [
m('h3', 'Identity Info'),
m('.info-grid', [
m('.info-label', 'GXS ID'),
m('.info-value', details.mId),
m('.info-label', 'Type'),
m('.info-value', details.mFlags === 14 ? 'Signed ID' : 'Anonymous ID'),
m('.info-label', 'Owner Node GPG'),
m('.info-value', pgpId && pgpId !== '0000000000000000' ? pgpId : 'None'),
m('.info-label', 'Created On'),
m(
'.info-value',
typeof details.mPublishTS === 'object'
? new Date(details.mPublishTS.xint64 * 1000).toLocaleString()
: 'Unknown'
),
m('.info-label', 'Last Used'),
m(
'.info-value',
typeof details.mLastUsageTS === 'object'
? new Date(details.mLastUsageTS.xint64 * 1000).toLocaleDateString()
: 'Unknown'
),
m('.info-label', 'Friend votes'),
m('.info-value', details.mReputation && (details.mReputation.mFriendsPositiveVotes > 0 || details.mReputation.mFriendsNegativeVotes > 0)
? `${details.mReputation.mFriendsPositiveVotes} positive, ${details.mReputation.mFriendsNegativeVotes} negative`
: 'No votes from friends'),
m('.info-label', 'Overall'),
m('.info-value', (() => {
const pos = details.mReputation ? details.mReputation.mFriendsPositiveVotes : 0;
const neg = details.mReputation ? details.mReputation.mFriendsNegativeVotes : 0;
if (pos > neg) return 'Positive';
if (pos < neg) return 'Negative';
return 'Neutral';
})()),
]),
]),
m('.detail-section', [
m('h3', 'Usage Statistics'),
m('.usage-list', [
(!details.mUseCases || details.mUseCases.length === 0)
? m('p.usage-placeholder', { style: 'font-style: italic; color: #64748b; padding: 0.5rem 0;' }, '[No record in current session]')
: (() => {
const sorted = [...details.mUseCases].sort((a, b) => get64Num(b.value) - get64Num(a.value));
return sorted.map((item) => {
const usage = item.key;
const ts = get64Num(item.value);
const dateStr = ts > 0 ? new Date(ts * 1000).toLocaleString() : 'Unknown';
return m('.usage-item', {
style: {
padding: '0.5rem 0',
borderBottom: '1px solid #f1f5f9',
fontSize: '0.9rem',
display: 'flex',
gap: '1rem',
alignItems: 'flex-start',
},
}, [
m('strong.usage-time', { style: 'color: #64748b; flex-shrink: 0; min-width: 150px;' }, dateStr),
m('span.usage-desc', createUsageString(usage)),
]);
});
})(),
]),
]),
]);
},
};
};
module.exports = DetailsTab;

View File

@ -0,0 +1,270 @@
const m = require('mithril');
const rs = require('rswebui');
const widget = require('widgets');
const peopleUtil = require('people/people_util');
const ownIdsLayout = require('people/people_ownids');
const { CreateIdentity } = ownIdsLayout;
const {
State,
fetchIdDetails,
loadGxsIdentities,
getSafeAvatar,
stopStatusPolling,
initializeDistantChat,
} = require('people/people_state');
const PeopleSidebar = () => {
return {
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) || [];
} else {
baseList = peopleUtil.sortUsers(rs.userList.users) || [];
}
// 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...]', {
value: State.searchString,
oninput: (e) => {
State.searchString = e.target.value;
},
}),
]),
// Scrollable list
m('.friends-scroll', [
filteredList.length === 0
? m('.network-pane-placeholder', { style: 'padding: 2rem 0;' }, 'No identities found')
: filteredList.map((item) => {
let gxsId, displayName;
if (State.activeFilter === 'own') {
gxsId = item;
displayName = rs.userList.username(gxsId) || 'Unknown';
} else {
gxsId = item.mGroupId;
displayName = item.mGroupName || 'Unknown';
}
fetchIdDetails(gxsId);
const itemDetails = State.gxsIdToDetailsMap[gxsId];
const itemAvatar = getSafeAvatar(itemDetails);
const itemFirstLetter = (displayName || '?').slice(0, 1).toUpperCase();
const isSelected = State.selectedId === gxsId;
const itemEntry = rs.userList.userMap[gxsId];
const itemIsContact = itemEntry && itemEntry.isContact;
const itemIsOwn = State.ownGxsIds.includes(gxsId);
return m(
'.friend-list-item',
{
class: isSelected ? 'selected' : '',
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 };
}
}
const idChanged = State.selectedId !== gxsId;
State.selectedId = gxsId;
if (idChanged) {
State.chatPid = null;
State.chatMessages = [];
stopStatusPolling();
if (State.activeTab === 'chat') {
initializeDistantChat();
}
}
m.redraw();
},
oncontextmenu: (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;
State.activeMenu = { gxsId, displayName, isContact: itemIsContact, top };
}
m.redraw();
},
},
[
m('.friend-avatar', m(peopleUtil.UserAvatar, {
avatar: itemAvatar,
firstLetter: itemFirstLetter,
identityId: gxsId,
})),
m('.friend-meta', [
m('.friend-name', displayName),
m(
'.friend-status',
itemIsOwn
? 'My Identity'
: itemIsContact
? 'Contact'
: 'Identity'
),
]),
]
);
}),
]),
// Context Menu
State.activeMenu && (() => {
const menu = State.activeMenu;
const isOwn = State.ownGxsIds.includes(menu.gxsId);
return m('.people-context-menu', {
style: {
top: `${menu.top}px`,
},
onclick: (e) => {
e.stopPropagation();
},
}, [
!isOwn && m('.menu-item', {
onclick: () => {
State.activeMenu = null;
State.selectedId = menu.gxsId;
State.activeTab = 'chat';
State.chatPid = null;
State.chatMessages = [];
initializeDistantChat();
m.redraw();
},
}, [
m('i.fas.fa-comments', { style: 'color: #3b82f6; margin-right: 0.5rem;' }),
'Start chat',
]),
!isOwn && m('.menu-item', {
onclick: () => {
State.activeMenu = null;
State.selectedId = menu.gxsId;
State.activeTab = 'details';
State.showMailCompose = true;
m.redraw();
},
}, [
m('i.fas.fa-envelope', { style: 'color: #10b981; margin-right: 0.5rem;' }),
'Send mail',
]),
!isOwn && m('.menu-item', {
onclick: () => {
State.activeMenu = null;
rs.rsJsonApiRequest(
'/rsIdentity/setAsRegularContact',
{ id: menu.gxsId, isContact: !menu.isContact },
(data, success) => {
if (success) {
loadGxsIdentities();
}
}
);
},
}, [
m('i.fas' + (menu.isContact ? '.fa-user-minus' : '.fa-user-plus'), {
style: {
color: menu.isContact ? '#ef4444' : '#3b82f6',
marginRight: '0.5rem',
},
}),
menu.isContact ? 'Remove from Contacts' : 'Add to Contacts',
]),
]);
})(),
]),
]);
},
};
};
module.exports = PeopleSidebar;

View File

@ -0,0 +1,376 @@
const m = require('mithril');
const rs = require('rswebui');
const Data = require('network/network_data');
const peopleUtil = require('people/people_util');
const State = {
searchString: '',
selectedId: null, // GXS ID of the selected identity
activeFilter: 'contacts', // 'all' | 'contacts' | 'own'
gxsIdToDetailsMap: {},
ownGxsIds: [],
gpgToGxsIdMap: {},
showMailCompose: false,
activeTab: 'details',
selectedOwnGxsIdForChat: '',
chatPid: null,
chatMessages: [],
chatInputMsg: '',
distantChatStatus: null,
statusPollInterval: null,
chatDisconnected: false,
activeMenu: null,
};
function fetchIdDetails(gxsId) {
if (!gxsId) return;
if (State.gxsIdToDetailsMap[gxsId] === undefined) {
State.gxsIdToDetailsMap[gxsId] = null; // Mark as loading
rs.rsJsonApiRequest('/rsIdentity/getIdDetails', { id: gxsId }, (detData) => {
if (detData && detData.details) {
State.gxsIdToDetailsMap[gxsId] = detData.details;
const pgpId = detData.details.mPgpId;
if (pgpId && pgpId !== '0000000000000000') {
State.gpgToGxsIdMap[pgpId.toLowerCase()] = gxsId;
}
m.redraw();
}
});
}
}
function loadGxsIdentities() {
rs.rsJsonApiRequest('/rsIdentity/getIdentitiesSummaries', {}, (data) => {
if (data && data.ids) {
m.redraw();
}
});
}
function loadOwnGxsIds() {
return new Promise((resolve) => {
peopleUtil.ownIds((ids) => {
State.ownGxsIds = ids || [];
if (State.ownGxsIds.length > 0 && !State.selectedOwnGxsIdForChat) {
State.selectedOwnGxsIdForChat = State.ownGxsIds[0];
}
m.redraw();
resolve();
});
});
}
function get64Num(val) {
if (!val) return 0;
if (typeof val === 'object') {
return val.xint64 || parseInt(val.xstr64) || 0;
}
return Number(val) || 0;
}
function getServiceName(serviceId) {
switch (serviceId) {
case 1: return 'Channels';
case 2: return 'Forums';
case 3: return 'Boards';
case 4: return 'Chat';
case 5: return 'GxsCircles';
case 6: return 'GxsMail';
case 7: return 'GxsCircles';
case 8: return 'Wire';
default: return 'Unknown (' + serviceId + ')';
}
}
function createUsageString(u) {
if (!u) return '[Unknown]';
const serviceName = getServiceName(u.mServiceId);
const usageCode = u.mUsageCode;
switch (usageCode) {
case 0:
return '[Unknown]';
case 1:
return `Admin signature in service ${serviceName}`;
case 2:
return `Admin signature verification in service ${serviceName}`;
case 3:
return `Creation of author signature in service ${serviceName}`;
case 4:
case 7:
return `Group author for group ${u.mGrpId || 'Unknown'} in service ${serviceName}`;
case 5:
return `Message signature creation in group ${u.mGrpId || 'Unknown'} of service ${serviceName}`;
case 6:
case 8:
return `Vote/comment in ${serviceName} service (Group: ${u.mGrpId || 'Unknown'}, Msg: ${u.mMsgId || 'Unknown'})`;
case 9:
return `Message in chat room (Id: ${get64Num(u.mAdditionalId)})`;
case 10:
return 'Distant message signature validation.';
case 11:
return 'Distant message signature creation.';
case 12:
return 'Signature validation in distant tunnel system.';
case 13:
return 'Signature in distant tunnel system.';
case 14:
return 'Received from GXS sync.';
case 15:
return 'Received from GXS discovery.';
case 16:
return 'Explicit request to friend.';
case 17:
return 'Generic signature validation.';
case 18:
return 'Generic signature creation.';
case 19:
return 'Generic encryption.';
case 20:
return 'Generic decryption.';
case 21:
return 'Circle membership check.';
default:
return `Usage code ${usageCode} in service ${serviceName}`;
}
}
function getSafeAvatar(details) {
return details && details.mAvatar ? details.mAvatar : undefined;
}
function getOnlineSslId(gpgId) {
if (!gpgId) return null;
const friend = Data.gpgDetails[gpgId.toLowerCase()];
if (friend && friend.locations) {
const onlineLoc = friend.locations.find((loc) => loc.isOnline);
return onlineLoc ? onlineLoc.id : null;
}
return null;
}
function isIdentityOnline(gxsId) {
fetchIdDetails(gxsId);
const details = State.gxsIdToDetailsMap[gxsId];
if (details && details.mPgpId && details.mPgpId !== '0000000000000000') {
const friend = Data.gpgDetails[details.mPgpId.toLowerCase()];
return friend ? friend.isOnline : false;
}
return false;
}
function syncFilter(tab) {
let newFilter = 'all';
if (tab === 'OwnIdentity') {
newFilter = 'own';
} else if (tab === 'MyContacts') {
newFilter = 'contacts';
}
if (State.activeFilter !== newFilter) {
State.activeFilter = newFilter;
State.selectedId = null;
State.chatPid = null;
State.chatMessages = [];
State.chatInputMsg = '';
State.activeTab = 'details';
}
}
function getStatusColor(status) {
switch (status) {
case 1: return '#eab308'; // Yellow
case 2: return '#22c55e'; // Green
case 3: return '#ef4444'; // Red
default: return '#94a3b8'; // Grey
}
}
function getStatusTooltip(status) {
switch (status) {
case 1: return 'Tunnel is pending. Please wait...';
case 2: return 'End-to-end encrypted conversation established. You can talk!';
case 3: return 'Your partner closed the conversation.';
default: return 'Remote status unknown.';
}
}
function pollDistantChatStatus() {
if (!State.chatPid) return;
rs.rsJsonApiRequest(
'/rsChats/getDistantChatStatus',
{
pid: State.chatPid,
},
(detail, success) => {
if (success && detail.retval) {
const oldStatus = State.distantChatStatus ? State.distantChatStatus.status : null;
State.distantChatStatus = detail.info;
if (oldStatus !== null && oldStatus !== detail.info.status) {
if (detail.info.status === 2) {
const text = 'Tunnel is secured. You can talk!';
const exists = State.chatMessages.some((m) => m.isSystem && m.msg === text);
if (!exists) {
State.chatMessages.push({
incoming: true,
isSystem: true,
msg: text,
sendTime: Math.floor(Date.now() / 1000),
});
State.chatMessages.sort((a, b) => a.sendTime - b.sendTime);
}
} else if (detail.info.status === 3) {
const text = 'Your partner closed the conversation.';
const exists = State.chatMessages.some((m) => m.isSystem && m.msg === text);
if (!exists) {
State.chatMessages.push({
incoming: true,
isSystem: true,
msg: text,
sendTime: Math.floor(Date.now() / 1000),
});
State.chatMessages.sort((a, b) => a.sendTime - b.sendTime);
}
}
}
m.redraw();
}
}
);
}
function startStatusPolling() {
stopStatusPolling();
pollDistantChatStatus();
State.statusPollInterval = setInterval(pollDistantChatStatus, 3000);
}
function stopStatusPolling() {
if (State.statusPollInterval) {
clearInterval(State.statusPollInterval);
State.statusPollInterval = null;
}
State.distantChatStatus = null;
}
function initializeDistantChat() {
if (!State.selectedId || !State.selectedOwnGxsIdForChat) return;
State.chatPid = null;
State.chatMessages = [];
State.chatDisconnected = false;
m.redraw();
rs.rsJsonApiRequest(
'/rsChats/initiateDistantChatConnexion',
{
to_pid: State.selectedId,
from_pid: State.selectedOwnGxsIdForChat,
notify: true,
},
(res) => {
if (res && res.pid) {
State.chatPid = rs.idToHex(res.pid);
State.distantChatStatus = null;
loadChatMessages();
pollDistantChatStatus();
startStatusPolling();
}
}
);
}
function loadChatMessages() {
if (!State.chatPid) return;
const chatPeerId = {
broadcast_status_peer_id: '00000000000000000000000000000000',
type: 2, // DISTANT
peer_id: '00000000000000000000000000000000',
distant_chat_id: State.chatPid,
lobby_id: { xstr64: '0' },
};
rs.rsJsonApiRequest(
'/rsHistory/getMessages',
{
chatPeerId: chatPeerId,
loadCount: 50,
},
(data, success) => {
if (success && data.msgs) {
State.chatMessages = data.msgs;
m.redraw();
setTimeout(() => {
const element = document.querySelector('.chat-messages');
if (element) element.scrollTop = element.scrollHeight;
}, 100);
}
}
);
}
function sendDistantChatMessage() {
if (!State.chatInputMsg.trim() || !State.chatPid) return;
const cid = {
broadcast_status_peer_id: '00000000000000000000000000000000',
type: 2, // DISTANT
peer_id: '00000000000000000000000000000000',
distant_chat_id: State.chatPid,
lobby_id: { xstr64: '0' },
};
const text = State.chatInputMsg;
State.chatInputMsg = '';
const echoMsg = {
chat_id: cid,
msg: text,
sendTime: Math.floor(Date.now() / 1000),
incoming: false,
lobby_peer_gxs_id: State.selectedOwnGxsIdForChat,
};
State.chatMessages.push(echoMsg);
m.redraw();
setTimeout(() => {
const element = document.querySelector('.chat-messages');
if (element) element.scrollTop = element.scrollHeight;
}, 100);
rs.rsJsonApiRequest(
'/rsChats/sendChat',
{
id: cid,
msg: text,
},
(data, success) => {
if (!success) {
console.error('[RS] Failed to send distant chat message');
}
}
);
}
module.exports = {
State,
fetchIdDetails,
loadGxsIdentities,
loadOwnGxsIds,
get64Num,
getServiceName,
createUsageString,
getSafeAvatar,
getOnlineSslId,
isIdentityOnline,
syncFilter,
getStatusColor,
getStatusTooltip,
pollDistantChatStatus,
startStatusPolling,
stopStatusPolling,
initializeDistantChat,
loadChatMessages,
sendDistantChatMessage,
};