diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index dedb9c3..558ca14 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -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'), diff --git a/webui-src/app/chat/chat_state.js b/webui-src/app/chat/chat_state.js index 3d04b1d..996a156 100644 --- a/webui-src/app/chat/chat_state.js +++ b/webui-src/app/chat/chat_state.js @@ -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 = { diff --git a/webui-src/app/people/people_chat_tab.js b/webui-src/app/people/people_chat_tab.js index 6c9b830..edff5cf 100644 --- a/webui-src/app/people/people_chat_tab.js +++ b/webui-src/app/people/people_chat_tab.js @@ -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), ]); }, }; diff --git a/webui-src/app/people/people_history.js b/webui-src/app/people/people_history.js new file mode 100644 index 0000000..ce43094 --- /dev/null +++ b/webui-src/app/people/people_history.js @@ -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; diff --git a/webui-src/app/people/people_state.js b/webui-src/app/people/people_state.js index 39a1ef4..3867d1e 100644 --- a/webui-src/app/people/people_state.js +++ b/webui-src/app/people/people_state.js @@ -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,