Added Chat History browser

This commit is contained in:
defnax 2026-07-25 17:06:45 +02:00
parent 33e7e867d7
commit 7d7adb8b83
5 changed files with 325 additions and 29 deletions

View File

@ -4,6 +4,7 @@ const peopleUtil = require('people/people_util');
const people = require('people/people');
const chatState = require('chat/chat_state');
const chatEmoji = require('chat/chat_emoji');
const HistoryBrowserModal = require('people/people_history');
const {
get64Num,
@ -235,32 +236,45 @@ const ChatRoomHeader = () => {
]),
m('.chat-header-actions', [
isDistant
? m(
'button.red',
{
title: 'Leave Distant Chat',
onclick: () => {
if (confirm('Are you sure you want to leave this distant chat conversation?')) {
rs.rsJsonApiRequest(
'/rsChats/closeDistantChatConnexion',
{
pid: lobbyHexId,
},
(data, success) => {
if (success) {
ChatLobbyModel.stopStatusPolling();
ChatHubState.selectedRoom = null;
ChatHubState.selectedRoomId = null;
ChatHubState.selectedRoomType = null;
m.route.set('/chat');
}
}
);
? [
m(
'button.blue',
{
title: 'View distant chat history',
style: 'margin-right: 0.75rem;',
onclick: () => {
ChatHubState.showHistoryModal = true;
}
},
},
[m('i.fas.fa-sign-out-alt'), ' Leave Chat']
)
[m('i.fas.fa-history'), ' History']
),
m(
'button.red',
{
title: 'Leave Distant Chat',
onclick: () => {
if (confirm('Are you sure you want to leave this distant chat conversation?')) {
rs.rsJsonApiRequest(
'/rsChats/closeDistantChatConnexion',
{
pid: lobbyHexId,
},
(data, success) => {
if (success) {
ChatLobbyModel.stopStatusPolling();
ChatHubState.selectedRoom = null;
ChatHubState.selectedRoomId = null;
ChatHubState.selectedRoomType = null;
m.route.set('/chat');
}
}
);
}
},
},
[m('i.fas.fa-sign-out-alt'), ' Leave Chat']
)
]
: [
m(
'button',
@ -274,6 +288,17 @@ const ChatRoomHeader = () => {
},
[m('i.fas.fa-user-plus'), ' Invite']
),
m(
'button.blue',
{
title: 'View chat room history',
style: 'margin-right: 0.75rem;',
onclick: () => {
ChatHubState.showHistoryModal = true;
}
},
[m('i.fas.fa-history'), ' History']
),
m(
'button.red',
{
@ -557,6 +582,7 @@ const ChatConversationView = () => {
])
])
]),
m(HistoryBrowserModal, { isRoom: true }),
]),
m('.chat-hub-rightbar', [
m('.rightbar-title', 'Participants'),

View File

@ -532,6 +532,38 @@ const ChatLobbyModel = {
}
);
},
loadAllHistoryForRoom(lobbyId, callback) {
ChatHubState.isHistoryLoading = true;
ChatHubState.fullHistoryMessages = [];
m.redraw();
const chatType = this.currentLobby && this.currentLobby.chatType;
const isDistant = chatType === 2;
const chatPeerId = {
broadcast_status_peer_id: '00000000000000000000000000000000',
type: isDistant ? 2 : 3,
peer_id: '00000000000000000000000000000000',
distant_chat_id: isDistant ? (lobbyId || '') : '00000000000000000000000000000000',
lobby_id: { xstr64: isDistant ? '0' : (lobbyId || '0') },
};
rs.rsJsonApiRequest(
'/rsHistory/getMessages',
{
chatPeerId: chatPeerId,
loadCount: 0,
},
(data, success) => {
let msgs = (success && data && data.msgs) ? data.msgs : [];
msgs.sort((a, b) => (a.sendTime || a.recvTime) - (b.sendTime || b.recvTime));
ChatHubState.fullHistoryMessages = msgs;
ChatHubState.isHistoryLoading = false;
m.redraw();
if (callback) callback();
}
);
},
setupAction: (lobbyId, nick) => { },
setIdentity(lobbyId, nick) {
rs.rsJsonApiRequest(
@ -772,6 +804,10 @@ const ChatHubState = {
showInviteModal: false,
friendsList: [],
selectedFriendsToInvite: new Set(),
showHistoryModal: false,
historySearchQuery: '',
fullHistoryMessages: [],
isHistoryLoading: false,
};
module.exports = {

View File

@ -8,10 +8,12 @@ const {
initializeDistantChat,
sendDistantChatMessage,
stopStatusPolling,
loadAllHistoryForSelectedPeer,
} = require('people/people_state');
const { renderChatMessage } = require('chat/chat_state');
const chatEmoji = require('chat/chat_emoji');
const peopleUtil = require('people/people_util');
const HistoryBrowserModal = require('people/people_history');
// Mirroring C++ Distant Chat packet size limit (200KB)
function formatChatImage(file, callback) {
@ -98,6 +100,14 @@ const ChatTab = () => {
const canTalk = State.distantChatStatus && State.distantChatStatus.status === 2;
// Filter history by search query for history modal
const query = (State.historySearchQuery || '').toLowerCase();
const filteredHistory = (State.fullHistoryMessages || []).filter((msg) => {
if (!query) return true;
const text = (msg.msg || msg.message || '').toLowerCase();
return text.includes(query);
});
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;',
@ -113,7 +123,7 @@ const ChatTab = () => {
title: getStatusTooltip(State.distantChatStatus ? State.distantChatStatus.status : 0),
}),
]),
m('.chat-actions', { style: 'display: flex; align-items: center; gap: 1rem;' }, [
m('.chat-actions', { style: 'display: flex; align-items: center; gap: 0.75rem;' }, [
m('.select-own-profile', { style: 'display: flex; align-items: center; gap: 0.5rem;' }, [
m('span', { style: 'color: #64748b;' }, 'Chatting as:'),
(() => {
@ -137,6 +147,18 @@ const ChatTab = () => {
]);
})(),
]),
m('button.blue.history-btn', {
style: 'padding: 0.25rem 0.75rem; border-radius: 0.25rem; font-size: 0.85rem; display: flex; align-items: center; gap: 0.35rem; border: none; cursor: pointer; background-color: #3b82f6; color: #ffffff; font-weight: 600;',
title: 'View all past chat history with this contact',
onclick: () => {
State.showHistoryModal = true;
State.historySearchQuery = '';
loadAllHistoryForSelectedPeer();
},
}, [
m('i.fas.fa-history', { style: 'color: #ffffff;' }),
'History',
]),
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: () => {
@ -304,6 +326,9 @@ const ChatTab = () => {
},
}, [m('i.fas.fa-paper-plane'), ' Send']),
]),
// Chat History Browser Modal
m(HistoryBrowserModal),
]);
},
};

View File

@ -0,0 +1,126 @@
const m = require('mithril');
const rs = require('rswebui');
const peopleState = require('people/people_state');
const chatState = require('chat/chat_state');
const HistoryBrowserModal = () => {
return {
oninit: (vnode) => {
const isRoom = vnode.attrs && vnode.attrs.isRoom;
if (isRoom) {
chatState.ChatHubState.historySearchQuery = '';
const lobbyId = chatState.ChatLobbyModel.currentLobby ? rs.idToHex(chatState.ChatLobbyModel.currentLobby.lobby_id) : null;
if (lobbyId) {
chatState.ChatLobbyModel.loadAllHistoryForRoom(lobbyId);
}
} else {
peopleState.State.historySearchQuery = '';
peopleState.loadAllHistoryForSelectedPeer();
}
},
view: (vnode) => {
const isRoom = vnode.attrs && vnode.attrs.isRoom;
const stateObj = isRoom ? chatState.ChatHubState : peopleState.State;
if (!stateObj.showHistoryModal) return null;
let name = 'Chat History';
if (isRoom) {
const lobby = chatState.ChatLobbyModel.currentLobby;
name = lobby ? lobby.lobby_name : 'Chat Room';
} else {
const details = peopleState.State.selectedId ? peopleState.State.gxsIdToDetailsMap[peopleState.State.selectedId] : null;
name = details ? (details.mNickname || details.mGroupName || 'Contact') : 'Contact';
}
const query = (stateObj.historySearchQuery || '').toLowerCase();
const filteredHistory = (stateObj.fullHistoryMessages || []).filter((msg) => {
if (!query) return true;
const text = (msg.msg || msg.message || '').toLowerCase();
return text.includes(query);
});
return m('.history-modal-overlay', {
style: 'position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background-color: rgba(15, 23, 42, 0.4); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; z-index: 2000;',
onclick: (e) => {
if (e.target === e.currentTarget) stateObj.showHistoryModal = false;
}
}, [
m('.history-modal', {
style: 'background: #ffffff; border-radius: 0.5rem; width: 780px; max-width: 92%; height: 85vh; max-height: 85vh; display: flex; flex-direction: column; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1); overflow: hidden;'
}, [
// Header
m('.history-modal-header', {
style: 'padding: 1rem 1.25rem; background: #f8fafc; border-bottom: 1px solid #e2e8f0; display: flex; align-items: center; justify-content: space-between;'
}, [
m('.history-title', { style: 'display: flex; align-items: center; gap: 0.5rem;' }, [
m('i.fas.fa-history', { style: 'color: #3b82f6; font-size: 1.2rem;' }),
m('h3', { style: 'margin: 0; font-size: 1.1rem; font-weight: 700; color: #1e293b;' }, `Chat History Browser — ${name}`),
]),
m('button.close-btn', {
style: 'background: transparent; border: none; font-size: 1.25rem; color: #64748b; cursor: pointer; padding: 0.25rem; border-radius: 0.25rem;',
title: 'Close history browser',
onclick: () => (stateObj.showHistoryModal = false),
}, m('i.fas.fa-times')),
]),
// Toolbar
m('.history-toolbar', {
style: 'padding: 0.75rem 1.25rem; background: #ffffff; border-bottom: 1px solid #f1f5f9; display: flex; align-items: center; justify-content: space-between; gap: 1rem;'
}, [
m('.search-input-box', { style: 'position: relative; flex: 1;' }, [
m('i.fas.fa-search', { style: 'position: absolute; left: 0.75rem; top: 50%; transform: translateY(-50%); color: #94a3b8; font-size: 0.85rem;' }),
m('input[type=text][placeholder=Search past messages or keywords...]', {
style: 'width: 100%; padding: 0.4rem 0.75rem 0.4rem 2.2rem; border-radius: 0.375rem; border: 1px solid #cbd5e1; outline: none; font-size: 0.85rem;',
value: stateObj.historySearchQuery || '',
oninput: (e) => (stateObj.historySearchQuery = e.target.value),
}),
]),
m('span.history-count', { style: 'font-size: 0.85rem; color: #64748b; font-weight: 600;' },
`${filteredHistory.length} messages`
),
]),
// Message Body
m('.history-message-list', {
style: 'flex: 1; overflow-y: auto; padding: 1rem 1.25rem; display: flex; flex-direction: column; gap: 0.75rem; background: #f8fafc;'
}, [
stateObj.isHistoryLoading
? m('.loading-spinner', { style: 'text-align: center; padding: 3rem; color: #64748b;' }, [
m('i.fas.fa-spinner.fa-spin', { style: 'font-size: 2rem; margin-bottom: 0.75rem; color: #3b82f6;' }),
m('p', { style: 'font-weight: 600;' }, 'Fetching complete chat history from Retroshare database...'),
])
: filteredHistory.length === 0
? m('.empty-history', { style: 'text-align: center; padding: 3rem; color: #64748b;' }, [
m('i.far.fa-comments', { style: 'font-size: 2.5rem; color: #cbd5e1; margin-bottom: 0.75rem;' }),
m('p', 'No past chat messages found matching your query.'),
])
: filteredHistory.map((msg) => {
const isIncoming = msg.incoming;
let senderName = msg.peerName || (isIncoming ? name : 'You');
if (!isIncoming) {
const ownId = isRoom ? (chatState.ChatLobbyModel.currentLobby ? chatState.ChatLobbyModel.currentLobby.gxs_id : '') : peopleState.State.selectedOwnGxsIdForChat;
senderName = rs.userList.username(ownId) || 'You';
}
const timeStr = new Date((msg.sendTime || msg.recvTime || 0) * 1000).toLocaleString();
return m('.history-item', {
style: 'background: #ffffff; border: 1px solid #e2e8f0; border-radius: 0.5rem; padding: 0.75rem 1rem; box-shadow: 0 1px 2px rgba(0,0,0,0.03);'
}, [
m('.history-item-header', { style: 'display: flex; align-items: center; justify-content: space-between; margin-bottom: 0.4rem;' }, [
m('span.sender', { style: `font-weight: 700; font-size: 0.85rem; color: ${isIncoming ? '#3b82f6' : '#10b981'};` }, senderName),
m('span.time', { style: 'font-size: 0.75rem; color: #94a3b8;' }, timeStr),
]),
m('.history-item-body', { style: 'font-size: 0.9rem; color: #334155; word-break: break-word;' },
chatState.renderChatMessage(msg.msg || msg.message || '')
),
]);
})
]),
])
]);
},
};
};
module.exports = HistoryBrowserModal;

View File

@ -22,6 +22,10 @@ const State = {
statusPollInterval: null,
chatDisconnected: false,
activeMenu: null,
showHistoryModal: false,
historySearchQuery: '',
fullHistoryMessages: [],
isHistoryLoading: false,
};
function fetchIdDetails(gxsId) {
@ -308,7 +312,7 @@ function loadChatMessages() {
const chatPeerId = {
broadcast_status_peer_id: '00000000000000000000000000000000',
type: 2, // DISTANT
type: 2, // TYPE_PRIVATE_DISTANT
peer_id: '00000000000000000000000000000000',
distant_chat_id: State.chatPid,
lobby_id: { xstr64: '0' },
@ -350,7 +354,7 @@ function sendDistantChatMessage() {
const cid = {
broadcast_status_peer_id: '00000000000000000000000000000000',
type: 2, // DISTANT
type: 2, // TYPE_PRIVATE_DISTANT
peer_id: '00000000000000000000000000000000',
distant_chat_id: State.chatPid,
lobby_id: { xstr64: '0' },
@ -405,10 +409,10 @@ function preloadAllChatHistory() {
const gxsId = typeof u === 'object' ? u.mGroupId : u;
if (!gxsId) return;
// Check Distant Chat History (type: 2)
// Check Distant Chat History (type: 2 - TYPE_PRIVATE_DISTANT)
const distantPeerId = {
broadcast_status_peer_id: '00000000000000000000000000000000',
type: 2, // DISTANT
type: 2, // TYPE_PRIVATE_DISTANT
peer_id: '00000000000000000000000000000000',
distant_chat_id: gxsId,
lobby_id: { xstr64: '0' },
@ -480,10 +484,89 @@ function preloadAllChatHistory() {
});
}
function loadAllHistoryForSelectedPeer(callback) {
if (!State.selectedId) return;
State.isHistoryLoading = true;
State.fullHistoryMessages = [];
m.redraw();
const queries = [];
// Query 1: Distant Chat History by active chatPid (type: 2 - TYPE_PRIVATE_DISTANT)
if (State.chatPid) {
queries.push({
broadcast_status_peer_id: '00000000000000000000000000000000',
type: 2, // TYPE_PRIVATE_DISTANT
peer_id: '00000000000000000000000000000000',
distant_chat_id: State.chatPid,
lobby_id: { xstr64: '0' },
});
}
// Query 2: Distant Chat History by selectedId if different (type: 2 - TYPE_PRIVATE_DISTANT)
if (State.selectedId && State.selectedId !== State.chatPid) {
queries.push({
broadcast_status_peer_id: '00000000000000000000000000000000',
type: 2, // TYPE_PRIVATE_DISTANT
peer_id: '00000000000000000000000000000000',
distant_chat_id: State.selectedId,
lobby_id: { xstr64: '0' },
});
}
// Query 3: Private Chat History by PGP ID if available (type: 1 - TYPE_PRIVATE)
const details = State.gxsIdToDetailsMap[State.selectedId];
const pgpId = details ? details.mPgpId : null;
if (pgpId && pgpId !== '0000000000000000') {
queries.push({
broadcast_status_peer_id: '00000000000000000000000000000000',
type: 1, // TYPE_PRIVATE
peer_id: pgpId,
distant_chat_id: '00000000000000000000000000000000',
lobby_id: { xstr64: '0' },
});
}
let accumulatedMsgs = [];
let completed = 0;
queries.forEach((chatPeerId) => {
rs.rsJsonApiRequest(
'/rsHistory/getMessages',
{
chatPeerId: chatPeerId,
loadCount: 0, // 0 = load all messages in C++
},
(msgData, success) => {
if (success && msgData && msgData.msgs) {
accumulatedMsgs = accumulatedMsgs.concat(msgData.msgs);
}
completed++;
if (completed === queries.length) {
const map = new Map();
accumulatedMsgs.forEach((mItem) => {
const text = mItem.msg || mItem.message || '';
const key = `${mItem.sendTime || mItem.recvTime}_${text}`;
if (!map.has(key)) map.set(key, mItem);
});
let uniqueMsgs = Array.from(map.values());
uniqueMsgs.sort((a, b) => (a.sendTime || a.recvTime) - (b.sendTime || b.recvTime));
State.fullHistoryMessages = uniqueMsgs;
State.isHistoryLoading = false;
m.redraw();
if (callback) callback();
}
}
);
});
}
module.exports = {
State,
isSystemMsg,
preloadAllChatHistory,
loadAllHistoryForSelectedPeer,
fetchIdDetails,
loadGxsIdentities,
loadOwnGxsIds,