RSNewWebUI/webui-src/app/network/network_state.js

454 lines
14 KiB
JavaScript

const m = require('mithril');
const rs = require('rswebui');
const Data = require('network/network_data');
const peopleUtil = require('people/people_util');
const State = {
ownProfile: {
name: 'Loading...',
location: '',
ssl_id: '',
gpg_id: '',
customState: '',
statusValue: 3,
statusTimestamp: 0,
avatar: '',
},
ownGxsIds: [],
selectedOwnGxsId: '',
selectedOwnGxsDetails: null,
selectedFriendGpgId: null,
mainTab: 'network', // 'network' | 'chats'
activeTab: 'details', // 'details' | 'chat' | 'graph'
mobilePane: 'list', // Phone master/detail navigation: 'list' | 'detail'
searchString: '',
gpgToGxsIdMap: {},
gxsIdToDetailsMap: {},
gxsIdentities: [],
chatHistoryMap: {}, // gpgId -> { lastMsg, lastTime }
unreadChatCount: {}, // gpgId -> unread messages received during this session
currentChatPeerId: null,
chatMessages: [],
chatInputMsg: '',
showMailCompose: false,
showAttachModal: false,
attachPath: '',
attachBrowseHint: false,
isHashing: false,
hashingError: '',
showEmojiPicker: false,
showHistoryModal: false,
historySearchQuery: '',
fullHistoryMessages: [],
isHistoryLoading: false,
};
function loadOwnProfile() {
rs.rsJsonApiRequest('/rsStatus/getOwnStatus', {}, (statusData) => {
if (statusData && statusData.retval && statusData.statusInfo) {
State.ownProfile.statusValue = statusData.statusInfo.status;
State.ownProfile.statusTimestamp = statusData.statusInfo.time_stamp || 0;
m.redraw();
}
}).catch(() => {});
const fetchOwnCustomState = () => {
rs.rsJsonApiRequest('/rsChats/getOwnCustomStateString', {}, (statusData) => {
if (statusData) {
let customState;
if (typeof statusData.retval === 'string') {
customState = statusData.retval;
} else if (typeof statusData === 'string') {
customState = statusData;
} else if (statusData.retval && typeof statusData.retval === 'object') {
customState =
statusData.retval.status ||
statusData.retval.customState ||
statusData.retval.custom_state ||
statusData.retval.status_string ||
'';
} else {
customState =
statusData.customState ||
statusData.custom_state ||
statusData.status ||
statusData.status_string ||
statusData.ownCustomStateString ||
'';
}
State.ownProfile.customState = customState;
m.redraw();
}
}).catch(() => {
if (State.ownProfile.ssl_id) {
rs.rsJsonApiRequest(
'/rsChats/getCustomStateString',
{ peer_id: State.ownProfile.ssl_id },
(statusData) => {
if (statusData) {
const customState =
typeof statusData.retval === 'string'
? statusData.retval
: statusData.customState || statusData.custom_state || statusData.status || '';
State.ownProfile.customState = customState;
m.redraw();
}
}
).catch(() => {});
}
});
};
fetchOwnCustomState();
rs.rsJsonApiRequest('/rsConfig/getConfigNetStatus', {}, (data) => {
if (data && data.status) {
State.ownProfile.name = data.status.ownName || 'Unknown';
State.ownProfile.ssl_id = data.status.ownId || '';
if (State.ownProfile.ssl_id) {
fetchOwnCustomState();
rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { sslId: State.ownProfile.ssl_id }, (detData) => {
if (detData && detData.det) {
State.ownProfile.gpg_id = detData.det.gpg_id || '';
State.ownProfile.location = detData.det.location || '';
m.redraw();
}
});
/* Disabled getAvatar API call to avoid 404 network errors
rs.rsJsonApiRequest('/rsChats/getAvatar', { pid: State.ownProfile.ssl_id }, (avatarData) => {
if (avatarData && avatarData.retval && avatarData.avatar_base64_string) {
State.ownProfile.avatar = avatarData.avatar_base64_string;
m.redraw();
}
});
*/
}
m.redraw();
}
});
peopleUtil.ownIds((ids) => {
if (ids) {
State.ownGxsIds = ids.filter(
(id) => id && id !== '0000000000000000' && Number(id) !== 0
);
if (State.ownGxsIds.length > 0 && !State.selectedOwnGxsId) {
State.selectedOwnGxsId = State.ownGxsIds[0];
loadSelectedOwnGxsDetails();
}
m.redraw();
}
});
}
function loadSelectedOwnGxsDetails() {
if (!State.selectedOwnGxsId) return;
rs.rsJsonApiRequest(
'/rsIdentity/getIdDetails',
{ id: State.selectedOwnGxsId },
(data) => {
if (data && data.details) {
State.selectedOwnGxsDetails = data.details;
m.redraw();
}
}
);
}
function fetchIdDetails(gxsId) {
if (!gxsId) return;
if (State.gxsIdToDetailsMap[gxsId] === undefined) {
State.gxsIdToDetailsMap[gxsId] = null;
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) {
State.gxsIdentities = data.ids.map((u) => u.mGroupId);
m.redraw();
}
});
}
function startDirectChat(sslId) {
State.currentChatPeerId = sslId;
State.chatMessages = [];
const normalizedSslId = String(sslId || '').toLowerCase();
const matchingFriend = Object.entries(Data.gpgDetails || {}).find(([, friend]) =>
((friend && friend.locations) || []).some(
(location) => String(location.id || '').toLowerCase() === normalizedSslId
)
);
if (matchingFriend) markDirectChatRead(matchingFriend[0]);
loadDirectChatMessages();
loadRecentDirectChatHistory();
}
function getOnlineSslId(gpgId) {
const friend = Data.gpgDetails[gpgId];
if (!friend || !friend.locations || friend.locations.length === 0) return null;
const onlineLoc = friend.locations.find((loc) => loc.isOnline);
return onlineLoc ? onlineLoc.id : friend.locations[0].id;
}
function isSystemMsg(msg) {
if (!msg) return false;
const str = String(msg);
return (
str.includes('Distant chat requested') ||
str.includes('Distant chat established') ||
str.includes('Distant chat closed') ||
str.includes('Distant chat status')
);
}
function preloadNetworkChatHistory() {
const gpgIds = Object.keys(Data.gpgDetails || {});
gpgIds.forEach((gpgId) => {
if (!gpgId || gpgId === '0000000000000000') return;
const friend = Data.gpgDetails[gpgId];
const sslIds = Array.from(new Set(
((friend && friend.locations) || []).map((location) => location.id).filter(Boolean)
));
Promise.all(sslIds.map((sslId) => new Promise((resolve) => {
rs.rsJsonApiRequest(
'/rsHistory/getMessages',
{ chatPeerId: directChatId(sslId), loadCount: 20 },
(msgData, success) => resolve(
success && msgData && Array.isArray(msgData.msgs) ? msgData.msgs : []
)
).catch(() => resolve([]));
}))).then((messageGroups) => {
const userMsgs = messageGroups.flat().filter(
(message) => !message.isSystem && !isSystemMsg(message.message || message.msg)
).sort(
(a, b) => (a.sendTime || a.recvTime || 0) - (b.sendTime || b.recvTime || 0)
);
if (userMsgs.length === 0) return;
const last = userMsgs[userMsgs.length - 1];
State.chatHistoryMap[gpgId] = {
lastMsg: last.message || last.msg || '',
lastTime: last.sendTime || last.recvTime || Math.floor(Date.now() / 1000),
};
m.redraw();
});
});
}
function receiveDirectChatMessage(chatMessage) {
const messagePeerId = chatMessage.chat_id && chatMessage.chat_id.peer_id
? rs.idToHex(chatMessage.chat_id.peer_id)
: '';
if (!chatMessage.chat_id || chatMessage.chat_id.type !== 1 || !messagePeerId) return;
const normalizedPeerId = messagePeerId.toLowerCase();
const matchingFriend = Object.entries(Data.gpgDetails || {}).find(([, friend]) =>
((friend && friend.locations) || []).some(
(location) => String(location.id || '').toLowerCase() === normalizedPeerId
)
);
const gpgId = matchingFriend ? matchingFriend[0] : null;
// Update the Chats list for every private message, not only for the
// conversation that happens to be visible.
if (gpgId) {
State.chatHistoryMap[gpgId] = {
lastMsg: chatMessage.msg || chatMessage.message || '',
lastTime: chatMessage.sendTime || chatMessage.recvTime || Math.floor(Date.now() / 1000),
};
const isOpenConversation =
m.route.get().split('/')[1] === 'network' &&
State.activeTab === 'chat' &&
State.selectedFriendGpgId === gpgId &&
normalizedPeerId === String(State.currentChatPeerId || '').toLowerCase() &&
(window.innerWidth > 700 || State.mobilePane === 'detail');
if (chatMessage.incoming === true && !isOpenConversation) {
State.unreadChatCount[gpgId] = (State.unreadChatCount[gpgId] || 0) + 1;
}
}
if (normalizedPeerId === String(State.currentChatPeerId || '').toLowerCase()) {
State.chatMessages = mergeDirectChatMessages(State.chatMessages.concat(chatMessage));
scrollChatToBottom();
}
m.redraw();
}
function loadDirectChatMessages() {
// Kept for older callers. Incoming messages are now dispatched globally by
// main.js so counters continue to work while another page is open.
}
function markDirectChatRead(gpgId) {
if (!gpgId || !State.unreadChatCount[gpgId]) return;
State.unreadChatCount[gpgId] = 0;
}
function directChatId(peerId) {
return {
broadcast_status_peer_id: '00000000000000000000000000000000',
type: 1,
peer_id: peerId,
distant_chat_id: '00000000000000000000000000000000',
lobby_id: { xstr64: '0' },
};
}
function mergeDirectChatMessages(messages) {
const unique = new Map();
messages.forEach((message) => {
const text = message.msg || message.message || '';
const time = message.sendTime || message.recvTime || 0;
const incoming = message.incoming === true;
unique.set(`${time}_${incoming}_${text}`, message);
});
return Array.from(unique.values()).sort(
(a, b) => (a.sendTime || a.recvTime || 0) - (b.sendTime || b.recvTime || 0)
);
}
function loadRecentDirectChatHistory() {
const peerId = State.currentChatPeerId;
if (!peerId) return;
rs.rsJsonApiRequest('/rsHistory/getMessages', {
chatPeerId: directChatId(peerId),
loadCount: 20,
}, (data, success) => {
if (peerId !== State.currentChatPeerId) return;
if (success && data && Array.isArray(data.msgs)) {
State.chatMessages = mergeDirectChatMessages(data.msgs.concat(State.chatMessages));
m.redraw();
scrollChatToBottom();
}
});
}
function loadAllDirectChatHistory() {
const peerId = State.currentChatPeerId;
if (!peerId) return;
State.isHistoryLoading = true;
State.fullHistoryMessages = [];
m.redraw();
rs.rsJsonApiRequest('/rsHistory/getMessages', {
chatPeerId: directChatId(peerId),
loadCount: 0,
}, (data, success) => {
if (peerId !== State.currentChatPeerId) return;
State.fullHistoryMessages = success && data && Array.isArray(data.msgs)
? mergeDirectChatMessages(data.msgs)
: [];
State.isHistoryLoading = false;
m.redraw();
});
}
function sendDirectChatMessage() {
if (!State.chatInputMsg.trim() || !State.currentChatPeerId) return;
const msg = State.chatInputMsg;
State.chatInputMsg = '';
rs.rsJsonApiRequest(
'/rsChats/sendChat',
{
id: { type: 1, peer_id: State.currentChatPeerId },
msg,
},
(data, success) => {
if (success) {
const nowSec = Math.floor(Date.now() / 1000);
State.chatMessages.push({
chat_id: { type: 1, peer_id: State.currentChatPeerId },
msg,
sendTime: nowSec,
incoming: false,
own: true,
});
if (State.selectedFriendGpgId) {
State.chatHistoryMap[State.selectedFriendGpgId] = {
lastMsg: msg,
lastTime: nowSec,
};
}
m.redraw();
scrollChatToBottom();
} else {
console.error('[RS] Failed to send direct chat message');
}
}
);
}
function scrollChatToBottom() {
setTimeout(() => {
const el = document.getElementById('chat-messages-container');
if (el) el.scrollTop = el.scrollHeight;
}, 100);
}
function setOwnCustomStateString(statusString) {
const str = (statusString || '').trim();
rs.rsJsonApiRequest('/rsChats/setCustomStateString', { status_string: str }, () => {
State.ownProfile.customState = str;
m.redraw();
}).catch(() => {
State.ownProfile.customState = str;
m.redraw();
});
}
async function setOwnStatus(statusValue) {
const value = Number(statusValue);
if (![1, 2, 3].includes(value)) return false;
try {
const response = await rs.rsJsonApiRequest('/rsStatus/sendStatus', { status: value });
if (response && response.body && response.body.retval === false) return false;
State.ownProfile.statusValue = value;
State.ownProfile.statusTimestamp = Math.floor(Date.now() / 1000);
m.redraw();
return true;
} catch (_) {
return false;
}
}
module.exports = {
State,
loadOwnProfile,
setOwnCustomStateString,
setOwnStatus,
loadSelectedOwnGxsDetails,
fetchIdDetails,
loadGxsIdentities,
startDirectChat,
getOnlineSslId,
preloadNetworkChatHistory,
loadDirectChatMessages,
receiveDirectChatMessage,
markDirectChatRead,
loadRecentDirectChatHistory,
loadAllDirectChatHistory,
sendDirectChatMessage,
scrollChatToBottom,
};