Fixed Network active chats

- Fixed. Chat previews
- Implemented unread chat counts
This commit is contained in:
defnax 2026-08-15 12:27:39 +02:00
parent c7011ff280
commit 209adb6564
7 changed files with 138 additions and 50 deletions

View File

@ -0,0 +1,33 @@
function chatPreviewText(rawText) {
if (!rawText) return '';
const source = String(rawText);
if (!/[<&]/.test(source)) return source.trim();
const hasImage = /<img\b/i.test(source) || /&lt;img\b/i.test(source);
const decodeEntities = (text) => {
const decoder = document.createElement('textarea');
decoder.innerHTML = text;
return decoder.value;
};
const stripMarkup = (text) => text
.replace(/<(style|script|head)\b[^>]*>[\s\S]*?<\/\1>/gi, ' ')
.replace(/<br\s*\/?>/gi, ' ')
.replace(/<\/p\s*>|<\/div\s*>/gi, ' ')
.replace(/<[^>]+>/g, ' ');
// Some peers send literal HTML while others send the same payload with its
// tags entity-encoded. Decode and strip a second time for the latter form.
let text = decodeEntities(stripMarkup(source));
if (/<[^>]+>/.test(text)) text = decodeEntities(stripMarkup(text));
text = text
.replace(/\u00a0/g, ' ')
.replace(/\s+/g, ' ')
.trim();
if (text) return text;
if (hasImage) return 'Photo';
return 'Message';
}
module.exports = chatPreviewText;

View File

@ -10,6 +10,8 @@ const {
startDirectChat,
getOnlineSslId,
preloadNetworkChatHistory,
loadDirectChatMessages,
markDirectChatRead,
} = require('network/network_state');
const { OwnProfileCard, FriendsList } = require('network/network_friends_list');
const DetailsTab = require('network/network_details_tab');
@ -19,6 +21,8 @@ const NetworkGraph = require('network/network_graph');
const NetworkLayout = () => {
return {
oninit: () => {
// Keep the active-chat list current even when no conversation is open.
loadDirectChatMessages();
Data.refreshGpgDetails().then(() => {
preloadNetworkChatHistory();
m.redraw();
@ -71,6 +75,7 @@ const NetworkLayout = () => {
onclick: () => {
State.activeTab = 'chat';
State.mobilePane = 'detail';
markDirectChatRead(State.selectedFriendGpgId);
const sslId = getOnlineSslId(State.selectedFriendGpgId);
if (sslId && !State.currentChatPeerId) {
startDirectChat(sslId);

View File

@ -1,12 +1,14 @@
const m = require('mithril');
const Data = require('network/network_data');
const peopleUtil = require('people/people_util');
const chatPreviewText = require('chat/chat_preview');
const {
State,
startDirectChat,
getOnlineSslId,
setOwnCustomStateString,
setOwnStatus,
markDirectChatRead,
} = require('network/network_state');
function formatRelativeTime(ts) {
@ -132,12 +134,9 @@ const FriendsList = () => {
const allGpgEntries = Object.entries(Data.gpgDetails || {});
// Compute active chats count
let activeChatsCount = 0;
let unreadChatsCount = 0;
allGpgEntries.forEach(([gpgId]) => {
const hist = State.chatHistoryMap && State.chatHistoryMap[gpgId];
if (hist && hist.lastMsg) {
activeChatsCount++;
}
unreadChatsCount += State.unreadChatCount[gpgId] || 0;
});
let displayFriends;
@ -199,7 +198,7 @@ const FriendsList = () => {
[
m('i.fas.fa-comments'),
' Chats',
activeChatsCount > 0 && m('span.segment-badge', activeChatsCount),
unreadChatsCount > 0 && m('span.segment-badge', unreadChatsCount),
]
),
m(
@ -240,6 +239,7 @@ const FriendsList = () => {
State.selectedFriendGpgId = gpgId;
State.activeTab = 'chat';
State.mobilePane = 'detail';
markDirectChatRead(gpgId);
const sslId = getOnlineSslId(gpgId);
if (sslId) startDirectChat(sslId);
},
@ -262,10 +262,12 @@ const FriendsList = () => {
},
friend.name
),
m('.chat-last-msg', hist ? hist.lastMsg : ''),
m('.chat-last-msg', hist ? chatPreviewText(hist.lastMsg) : ''),
]),
m('.chat-meta', [
hist && hist.lastTime && m('.chat-time', formatRelativeTime(hist.lastTime)),
(State.unreadChatCount[gpgId] || 0) > 0 &&
m('.chat-unread-badge', State.unreadChatCount[gpgId]),
]),
]
);

View File

@ -26,6 +26,7 @@ const State = {
gxsIdToDetailsMap: {},
gxsIdentities: [],
chatHistoryMap: {}, // gpgId -> { lastMsg, lastTime }
unreadChatCount: {}, // gpgId -> unread messages received during this session
currentChatPeerId: null,
chatMessages: [],
chatInputMsg: '',
@ -186,6 +187,13 @@ function loadGxsIdentities() {
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();
}
@ -213,36 +221,33 @@ function preloadNetworkChatHistory() {
gpgIds.forEach((gpgId) => {
if (!gpgId || gpgId === '0000000000000000') return;
const privatePeerId = {
broadcast_status_peer_id: '00000000000000000000000000000000',
type: 1, // PRIVATE
peer_id: gpgId,
distant_chat_id: '00000000000000000000000000000000',
lobby_id: { xstr64: '0' },
};
const friend = Data.gpgDetails[gpgId];
const sslIds = Array.from(new Set(
((friend && friend.locations) || []).map((location) => location.id).filter(Boolean)
));
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];
State.chatHistoryMap[gpgId] = {
lastMsg: last.message || last.msg || '',
lastTime: last.sendTime || last.recvTime || Math.floor(Date.now() / 1000),
};
m.redraw();
}
}
}
);
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();
});
});
}
@ -251,24 +256,47 @@ function loadDirectChatMessages() {
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 === State.currentChatPeerId
) {
State.chatMessages.push(chatMessage);
if (State.selectedFriendGpgId) {
State.chatHistoryMap[State.selectedFriendGpgId] = {
lastMsg: chatMessage.msg || chatMessage.message || '',
lastTime: chatMessage.sendTime || chatMessage.recvTime || Math.floor(Date.now() / 1000),
};
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 =
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;
}
m.redraw();
}
if (normalizedPeerId === String(State.currentChatPeerId || '').toLowerCase()) {
State.chatMessages = mergeDirectChatMessages(State.chatMessages.concat(chatMessage));
scrollChatToBottom();
}
m.redraw();
};
}
function markDirectChatRead(gpgId) {
if (!gpgId || !State.unreadChatCount[gpgId]) return;
State.unreadChatCount[gpgId] = 0;
}
function directChatId(peerId) {
return {
broadcast_status_peer_id: '00000000000000000000000000000000',
@ -412,6 +440,7 @@ module.exports = {
getOnlineSslId,
preloadNetworkChatHistory,
loadDirectChatMessages,
markDirectChatRead,
loadRecentDirectChatHistory,
loadAllDirectChatHistory,
sendDirectChatMessage,

View File

@ -2,6 +2,7 @@ const m = require('mithril');
const rs = require('rswebui');
const widget = require('widgets');
const peopleUtil = require('people/people_util');
const chatPreviewText = require('chat/chat_preview');
const ownIdsLayout = require('people/people_ownids');
const { CreateIdentity } = ownIdsLayout;
const {
@ -208,7 +209,9 @@ const PeopleSidebar = () => {
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');
const lastMsgText = hist && hist.lastMsg
? chatPreviewText(hist.lastMsg)
: (itemIsOwn ? 'My Identity' : itemIsContact ? 'Saved Contact' : 'Distant Chat');
if (State.mainTab === 'chats') {
return m(

View File

@ -335,6 +335,22 @@
}
}
.chat-unread-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.25rem;
height: 1.25rem;
margin-top: .2rem;
padding: 0 .35rem;
border-radius: 999px;
background: #0284c7;
color: #fff;
font-size: .7rem;
font-weight: 700;
line-height: 1;
}
/* Shared phone master/detail header. It is intentionally absent on desktop. */
.mobile-pane-header,
.mobile-graph-shortcut {

File diff suppressed because one or more lines are too long