From c1eb3a767cdae30fc98299f31ede97851cd52b45 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sun, 5 Jul 2026 18:49:22 +0200 Subject: [PATCH 01/40] Improved Network page --- webui-src/app/mail/mail_compose.js | 85 ++- webui-src/app/network/network.js | 711 +++++++++++++++++++++---- webui-src/app/network/network_data.js | 57 +- webui-src/app/scss/pages/_network.scss | 574 ++++++++++++++++++-- webui-src/styles.css | 547 ++++++++++++++++++- 5 files changed, 1789 insertions(+), 185 deletions(-) diff --git a/webui-src/app/mail/mail_compose.js b/webui-src/app/mail/mail_compose.js index faff4f9..3fb7e16 100644 --- a/webui-src/app/mail/mail_compose.js +++ b/webui-src/app/mail/mail_compose.js @@ -27,37 +27,75 @@ const Layout = () => { }, }, }; - async function loadMailUserDetails(msgType, senderId, recipientList) { + async function loadMailUserDetails(msgType, senderId, recipientList, isDirectMail) { Data.allUsers = await peopleUtil.sortUsers(rs.userList.users); if (msgType === 'reply') { Data.allUsers.forEach(async (user) => { if (user.mGroupId === (await senderId)) Data.recipients.to.sendList.push(user); }); } - await peopleUtil.ownIds(async (data) => { - Data.ownId = await data; - for (let i = 0; i < Data.ownId.length; i++) { - if (Number(Data.ownId[i]) === 0) { - Data.ownId.splice(i, 1); // workaround for id '0' - } - } - if (msgType === 'reply') { - Data.identity = Data.ownId.filter((id) => - Object.prototype.hasOwnProperty.call(recipientList, id) - )[0]; - } + + // Wrap ownIds in a Promise + const gxsIds = await new Promise((resolve) => { + peopleUtil.ownIds((ids) => { + resolve(ids || []); + }); }); + + Data.ownId = gxsIds.filter((id) => id && id !== '0000000000000000' && Number(id) !== 0); + + // Fetch own Node GPG ID + const netStatus = await new Promise((resolve) => { + rs.rsJsonApiRequest('/rsConfig/getConfigNetStatus', {}, (res) => { + resolve(res || null); + }); + }); + + if (netStatus && netStatus.status) { + const ownNodeId = netStatus.status.ownId; + if (ownNodeId && !Data.ownId.includes(ownNodeId)) { + rs.userList.userMap[ownNodeId] = { + name: (netStatus.status.ownName || 'Node') + ' (Node GPG Key)', + isContact: false, + }; + Data.ownId.push(ownNodeId); + } + if (msgType === 'compose' && isDirectMail) { + Data.identity = ownNodeId; + } + } + + if (msgType === 'reply') { + Data.identity = Data.ownId.filter((id) => + Object.prototype.hasOwnProperty.call(recipientList, id) + )[0]; + } } async function loadDetails(attrs) { - const { msgType, senderId, recipientList } = await attrs; - await loadMailUserDetails(msgType, senderId, recipientList); + const { msgType, senderId, recipientList, isDirectMail } = await attrs; + await loadMailUserDetails(msgType, senderId, recipientList, isDirectMail); Object.keys(Data.recipients).forEach((item) => { Data.recipients[item].inputList = Data.allUsers; }); if (msgType === 'compose') { - Data.identity = Data.ownId[0]; + if (!isDirectMail) { + Data.identity = Data.ownId[0]; + } + if (attrs.toId) { + const matchingUser = Data.allUsers.find((user) => user.mGroupId === attrs.toId); + if (matchingUser) { + Data.recipients.to.sendList.push(matchingUser); + } else { + // If toId is a GPG ID (not in GXS list), add it manually as a GPG recipient + const friendName = attrs.friendName || 'Unknown Friend'; + Data.recipients.to.sendList.push({ + mGroupId: attrs.toId, + mGroupName: friendName + ' (Node GPG Key)', + }); + } + } } if (msgType === 'reply') { @@ -73,13 +111,13 @@ const Layout = () => { -----Original Message-----
From: - ${rs.userList.userMap[senderId]} + ${rs.userList.username(senderId)}
To: ${Object.keys(recipientList).map( (recip) => ` - ${rs.userList.userMap[recipientList[recip]._addr_string] || 'Unknown'}, + ${rs.userList.username(recipientList[recip]._addr_string) || 'Unknown'}, ` )} @@ -94,7 +132,7 @@ const Layout = () => {
On ${timeStamp.toLocaleDateString()} ${time}, - ${rs.userList.userMap[senderId]} + ${rs.userList.username(senderId)} wrote: `; @@ -172,13 +210,16 @@ const Layout = () => { Data.identity = Data.ownId[e.target.selectedIndex]; }, }, - Data.ownId && + Data.ownId && Data.ownId.map((id) => m( 'option', - { value: id }, + { + value: id, + selected: id === Data.identity, + }, rs.userList.userMap[id] - ? rs.userList.userMap[id].toLocaleString() + ' (' + id.slice(0, 12) + '...)' + ? (rs.userList.userMap[id].name || id) + ' (' + id.slice(0, 12) + '...)' : 'No Signature' ) ) diff --git a/webui-src/app/network/network.js b/webui-src/app/network/network.js index 2b3d494..1d9be87 100644 --- a/webui-src/app/network/network.js +++ b/webui-src/app/network/network.js @@ -2,7 +2,183 @@ const m = require('mithril'); const rs = require('rswebui'); const widget = require('widgets'); const Data = require('network/network_data'); +const peopleUtil = require('people/people_util'); +const compose = require('mail/mail_compose'); +// State variables for Network Page +const State = { + ownProfile: { + name: 'Loading...', + ssl_id: '', + gpg_id: '', + customState: '', + }, + ownGxsIds: [], + selectedOwnGxsId: '', + selectedOwnGxsDetails: null, + selectedFriendGpgId: null, + activeTab: 'details', // 'details' | 'chat' + searchString: '', + gpgToGxsIdMap: {}, + gxsIdToDetailsMap: {}, + currentChatPeerId: null, + chatMessages: [], + chatInputMsg: '', + showMailCompose: false, +}; + +// Fetch own node name using the same API as config_node.js +function loadOwnProfile() { + // Use rsConfig/getConfigNetStatus - the same proven endpoint used in config_node.js + rs.rsJsonApiRequest('/rsConfig/getConfigNetStatus', {}, (data) => { + if (data && data.status) { + State.ownProfile.name = data.status.ownName || 'Unknown'; + State.ownProfile.ssl_id = data.status.ownId || ''; + + // Fetch own custom status message using our own Location SSL ID + if (State.ownProfile.ssl_id) { + rs.rsJsonApiRequest('/rsChats/getCustomStateString', { peer_id: State.ownProfile.ssl_id }, (statusData) => { + if (statusData && statusData.retval) { + State.ownProfile.customState = statusData.retval; + m.redraw(); + } + }); + + // Also fetch our own node GPG ID via getPeerDetails using our own SSL ID + rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { sslId: State.ownProfile.ssl_id }, (detData) => { + if (detData && detData.det && detData.det.gpg_id) { + State.ownProfile.gpg_id = detData.det.gpg_id; + m.redraw(); + } + }); + } + m.redraw(); + } + }); + + // Load own GXS identities using the existing utility + 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(); + } + } + ); +} + +// Build map GPG ID -> GXS ID for all known identities +function loadGxsIdentities() { + rs.rsJsonApiRequest('/rsIdentity/getIdentitiesSummaries', {}, (data) => { + if (data && data.ids) { + data.ids.forEach((user) => { + const gxsId = user.mGroupId; + 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(); + } + }); + }); + } + }); +} + +// Start a direct chat with a friend using their SSL peer ID (type 1) +function startDirectChat(sslId) { + State.currentChatPeerId = sslId; + State.chatMessages = []; + loadDirectChatMessages(); +} + +// Get the first online SSL ID for a friend, or fallback to first location +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; +} + +// Load message history for direct chat (type 1 is not in the event handler, +// so we manage messages locally) +function loadDirectChatMessages() { + // Messages are received via the event system and stored locally + // Register for incoming chat messages + rs.events[15].notify = (chatMessage) => { + if ( + chatMessage.chat_id && + (chatMessage.chat_id.type === 1 || chatMessage.chat_id.type === 2) && + rs.idToHex(chatMessage.chat_id) === State.currentChatPeerId + ) { + State.chatMessages.push(chatMessage); + m.redraw(); + scrollChatToBottom(); + } + }; +} + +// Send direct chat message (type 1 / peer_id) +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: msg, + }, + (data, success) => { + if (success) { + // Add own message to local log + State.chatMessages.push({ + chat_id: { type: 1, peer_id: State.currentChatPeerId }, + msg, + sendTime: Date.now() / 1000, + incoming: false, + own: true, + }); + 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); +} + +// Popup confirmation to remove friend SSL connection const ConfirmRemove = () => { return { view: (vnode) => [ @@ -16,7 +192,9 @@ const ConfirmRemove = () => { rs.rsJsonApiRequest('/rsPeers/removeFriend', { pgpId: vnode.attrs.gpg_id, }); - m.redraw(); + State.selectedFriendGpgId = null; + Data.refreshGpgDetails().then(() => m.redraw()); + widget.popupMessage(m('p', 'Friend removed successfully.')); }, }, 'Confirm' @@ -25,125 +203,446 @@ const ConfirmRemove = () => { }; }; -const Locations = () => { - return { - view: (v) => [ - m('h4', 'Locations'), - v.attrs.locations.map((loc) => - m('.location', [ - m('i.fas.fa-user-tag', { style: 'margin-top:3px' }), - m('span', { style: 'margin-top:1px' }, loc.name), - m('p', 'ID :'), - m('p', loc.id), - m('p', 'Last contacted :'), - m('p', new Date(loc.lastSeen * 1000).toDateString()), - m('p', 'Online :'), - m('i.fas', { - class: loc.isOnline ? 'fa-check-circle' : 'fa-times-circle', - }), - m( - 'button.red', - { - onclick: () => - widget.popupMessage( - m(ConfirmRemove, { - gpg: loc.gpg_id, - }) - ), - }, - 'Remove node' - ), - ]) - ), - ], - }; -}; +// Helper: get avatar safely for UserAvatar (must pass undefined, not null) +function getSafeAvatar(details) { + if ( + details && + details.mAvatar && + details.mAvatar.mData && + details.mAvatar.mData.base64 !== '' + ) { + return details.mAvatar; + } + return undefined; +} -const Friend = () => { +const OwnProfileCard = () => { return { - isExpanded: false, + view: () => { + const ownGxsId = State.ownProfile.gpg_id ? State.gpgToGxsIdMap[State.ownProfile.gpg_id.toLowerCase()] : null; + const ownDetails = ownGxsId ? State.gxsIdToDetailsMap[ownGxsId] : null; + const avatar = getSafeAvatar(ownDetails); + const firstLetter = (State.ownProfile.name || 'U').slice(0, 1).toUpperCase(); - view: (vnode) => - m( - '.friend', - { - key: vnode.attrs.id, - class: Data.gpgDetails[vnode.attrs.id].isSearched ? '' : 'hidden', - }, - [ - m('i.fas.fa-angle-right', { - class: 'fa-rotate-' + (vnode.state.isExpanded ? '90' : '0'), - style: 'margin-top:12px', - onclick: () => (vnode.state.isExpanded = !vnode.state.isExpanded), - }), - m('.brief-info', { class: Data.gpgDetails[vnode.attrs.id].isOnline ? 'online' : '' }, [ - m('i.fas.fa-2x.fa-user-circle'), - m('span', Data.gpgDetails[vnode.attrs.id].name), + return m('.own-profile-card', [ + m('.profile-header', [ + m(peopleUtil.UserAvatar, { avatar, firstLetter }), + m('.profile-info', [ + m('.profile-name', State.ownProfile.name || 'Loading...'), + m('.profile-status', 'Online'), + State.ownProfile.customState && + m( + '.profile-custom-status', + { + style: 'font-size: 0.8rem; color: #94a3b8; font-style: italic; margin-top: 2px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 150px;', + title: State.ownProfile.customState, + }, + State.ownProfile.customState + ), ]), - m( - '.details', - { - style: 'display:' + (vnode.state.isExpanded ? 'block' : 'none'), - }, - [ - m(Locations, { - locations: Data.gpgDetails[vnode.attrs.id].locations, - }), - ] - ), - ] - ), - }; -}; - -const SearchBar = () => { - let searchString = ''; - return { - view: () => - m('input.searchbar', { - type: 'text', - placeholder: 'search', - value: searchString, - oninput: (e) => { - searchString = e.target.value.toLowerCase(); - for (const id in Data.gpgDetails) { - if (Data.gpgDetails[id].name.toLowerCase().indexOf(searchString) > -1) { - Data.gpgDetails[id].isSearched = true; - } else { - Data.gpgDetails[id].isSearched = false; - } - } - }, - }), + ]), + ]); + }, }; }; const FriendsList = () => { return { - oninit: () => { - Data.refreshGpgDetails(); - }, - view: () => - m('.widget', [ - m('.widget__heading', [m('h3', 'Friend nodes'), m(SearchBar)]), - m('.widget__body', [ - Object.entries(Data.gpgDetails) - .sort((a, b) => { - return a[1].isOnline === b[1].isOnline ? 0 : a[1].isOnline ? -1 : 1; - }) - .map((item) => { - const id = item[0]; - return m(Friend, { id }); - }), + view: () => { + const search = State.searchString.toLowerCase(); + const filteredFriends = Object.entries(Data.gpgDetails).filter( + ([gpgId, friend]) => (friend.name || '').toLowerCase().includes(search) + ); + + return m('.friends-list-container', [ + m('.searchbar-container', [ + m('input.searchbar', { + type: 'text', + placeholder: 'Search friends...', + value: State.searchString, + oninput: (e) => { + State.searchString = e.target.value; + }, + }), ]), - ]), + m('.friends-scroll', [ + filteredFriends.length === 0 + ? m('p', { style: 'padding: 1rem; color: #94a3b8; text-align: center;' }, 'No friends found') + : filteredFriends + .sort((a, b) => (a[1].isOnline === b[1].isOnline ? 0 : a[1].isOnline ? -1 : 1)) + .map(([gpgId, friend]) => { + const friendGxsId = State.gpgToGxsIdMap[gpgId.toLowerCase()]; + const friendDetails = friendGxsId ? State.gxsIdToDetailsMap[friendGxsId] : null; + const avatar = getSafeAvatar(friendDetails); + const firstLetter = (friend.name || '?').slice(0, 1).toUpperCase(); + const isSelected = State.selectedFriendGpgId === gpgId; + + return m( + `.friend-list-item${isSelected ? '.selected' : ''}`, + { + key: gpgId, + onclick: () => { + State.selectedFriendGpgId = gpgId; + State.currentChatPeerId = null; + State.chatMessages = []; + if (State.activeTab === 'chat') { + const sslId = getOnlineSslId(gpgId); + if (sslId) startDirectChat(sslId); + } + }, + }, + [ + m('.friend-avatar', m(peopleUtil.UserAvatar, { avatar, firstLetter })), + m('.friend-meta', [ + m('.friend-name', friend.name), + m( + `.friend-status${friend.isOnline ? '.online' : ''}`, + friend.isOnline ? 'Online' : 'Offline' + ), + friend.customState && + m( + '.friend-custom-status', + { + style: 'font-size: 0.85rem; color: #64748b; margin-top: 2px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 160px;', + title: friend.customState, + }, + friend.customState + ), + ]), + ] + ); + }), + ]), + ]); + }, }; }; -const Layout = () => { +// Right Pane Tabs and Tab Views +const DetailsTab = () => { return { - view: () => m('.node-panel', m(FriendsList)), + view: () => { + const gpgId = State.selectedFriendGpgId; + const friend = Data.gpgDetails[gpgId]; + if (!friend) return null; + + const friendGxsId = State.gpgToGxsIdMap[gpgId.toLowerCase()]; + + return m('.network-detail-view', [ + m('.detail-header', [ + m('.detail-title', [ + m('h2', friend.name), + m('.detail-subtitle', [ + m('i.fas.fa-fingerprint'), + m('span', 'GPG ID: ' + gpgId), + ]), + ]), + m('.detail-actions', [ + m( + 'button', + { + onclick: () => { + const sslId = getOnlineSslId(gpgId); + if (sslId) { + State.activeTab = 'chat'; + startDirectChat(sslId); + } + }, + }, + [m('i.fas.fa-comments'), ' Start Chat'] + ), + m( + 'button', + { + onclick: () => { + State.showMailCompose = true; + }, + }, + [m('i.fas.fa-envelope'), ' Send Mail'] + ), + ]), + ]), + + m('.detail-section', [ + m('h3', 'Profile Info'), + m('.info-grid', [ + m('.info-label', 'Status'), + m( + '.info-value', + { style: friend.isOnline ? 'color: #10b981; font-weight: 600;' : '' }, + friend.isOnline ? 'Online' : 'Offline' + ), + m('.info-label', 'Custom Status'), + m( + '.info-value', + { style: 'font-style: italic; color: #64748b;' }, + friend.customState || 'None' + ), + friendGxsId ? [ + m('.info-label', 'GXS Identity'), + m('.info-value', friendGxsId), + ] : null, + m('.info-label', 'Node GPG Key'), + m('.info-value', gpgId), + ]), + ]), + + m('.detail-section', [ + m('h3', 'Locations (' + friend.locations.length + ')'), + m( + '.locations-grid', + friend.locations + .slice() + .sort((a, b) => (a.isOnline === b.isOnline ? 0 : a.isOnline ? -1 : 1)) + .map((loc) => + m('.location-card', { key: loc.id }, [ + m('.loc-header', [ + m('.loc-name', loc.name), + m( + '.loc-status' + (loc.isOnline ? '.online' : '.offline'), + loc.isOnline ? 'Online' : 'Offline' + ), + ]), + m('.loc-body', [ + m('.loc-label', 'SSL ID'), + m('.loc-val', loc.id), + m('.loc-label', 'Last Seen'), + m('.loc-val', new Date(loc.lastSeen * 1000).toLocaleString()), + ]), + m('.loc-footer', [ + m( + 'button.red', + { + onclick: () => + widget.popupMessage( + m(ConfirmRemove, { + gpg_id: loc.gpg_id, + }) + ), + }, + 'Remove Location' + ), + ]), + ]) + ) + ), + ]), + ]); + }, }; }; -module.exports = Layout; +const ChatTab = () => { + return { + view: () => { + const gpgId = State.selectedFriendGpgId; + const friend = Data.gpgDetails[gpgId]; + if (!friend) return null; + + const sslId = getOnlineSslId(gpgId); + + if (!sslId) { + return m('.network-chat-view', [ + m('.chat-warning', [ + m('i.fas.fa-exclamation-triangle'), + m('h4', 'No Location Found'), + m('p', 'This friend has no known locations to start a direct chat with.'), + ]), + ]); + } + + if (!State.currentChatPeerId) { + return m('.network-chat-view', [ + m('.chat-warning', [ + m('i.fas.fa-comments'), + m('h4', 'Direct Chat'), + m('p', 'Click below to start a direct chat with ' + friend.name + '.'), + m( + 'button', + { + onclick: () => startDirectChat(sslId), + }, + 'Start Chat' + ), + ]), + ]); + } + + return m('.network-chat-view', [ + (() => { + const activeLoc = friend.locations.find((loc) => loc.id === State.currentChatPeerId); + const locName = activeLoc ? activeLoc.name : 'Unknown Location'; + const locOnline = activeLoc ? activeLoc.isOnline : false; + return m('.chat-header-bar', { + style: { + padding: '0.75rem 1rem', + backgroundColor: '#ffffff', + borderBottom: '1px solid #cbd5e1', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between' + } + }, [ + m('.chat-header-info', [ + m('.chat-header-name', { style: { fontWeight: '700', color: '#1e293b' } }, friend.name), + m('.chat-header-location', { style: { fontSize: '0.8rem', color: '#64748b', display: 'flex', alignItems: 'center', marginTop: '0.25rem' } }, [ + m('span', 'Location: ' + locName), + m('span.status-dot', { + style: { + display: 'inline-block', + width: '8px', + height: '8px', + borderRadius: '50%', + backgroundColor: locOnline ? '#10b981' : '#ef4444', + marginLeft: '6px', + marginRight: '4px' + } + }), + m('span', { style: { color: locOnline ? '#10b981' : '#ef4444', fontWeight: '500' } }, locOnline ? 'Online' : 'Offline') + ]) + ]) + ]); + })(), + m( + '.chat-messages[id=chat-messages-container]', + State.chatMessages.map((msg) => { + const isOwn = msg.own === true; + const senderName = isOwn + ? (State.ownProfile.name || 'Me') + : friend.name; + const time = new Date(msg.sendTime * 1000).toLocaleTimeString(); + const text = (msg.msg || '') + .replaceAll('
', '\n') + .replace(new RegExp('|<[^>]*>', 'gm'), ''); + + return m( + '.chat-bubble-container' + (isOwn ? '.outgoing' : '.incoming'), + [ + !isOwn && m('.chat-sender', senderName), + m('.chat-bubble', text), + m('.chat-time', time), + ] + ); + }) + ), + m('.chat-input-area', [ + m('textarea.chat-textarea', { + placeholder: 'Type your message... Press Enter to send', + value: State.chatInputMsg, + oninput: (e) => { + State.chatInputMsg = e.target.value; + }, + onkeydown: (e) => { + if (e.code === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendDirectChatMessage(); + } + }, + }), + m( + 'button.send-btn', + { + onclick: () => sendDirectChatMessage(), + }, + [m('i.fas.fa-paper-plane'), ' Send'] + ), + ]), + ]); + }, + }; +}; + +const NetworkLayout = () => { + return { + oninit: () => { + Data.refreshGpgDetails().then(() => m.redraw()); + loadOwnProfile(); + loadGxsIdentities(); + }, + onremove: () => { + // Clean up notify callback when page is left + if (rs.events[15]) { + rs.events[15].notify = () => {}; + } + }, + view: () => { + const selectedFriend = State.selectedFriendGpgId + ? Data.gpgDetails[State.selectedFriendGpgId] + : null; + + const selectedGxsId = State.selectedFriendGpgId + ? State.gpgToGxsIdMap[State.selectedFriendGpgId.toLowerCase()] + : null; + + return m('.network-container', [ + m('.network-left-pane', [m(OwnProfileCard), m(FriendsList)]), + m('.network-right-pane', [ + selectedFriend + ? [ + m('.network-tabs', [ + m( + 'button.tab-btn' + (State.activeTab === 'details' ? '.active' : ''), + { + onclick: () => { + State.activeTab = 'details'; + }, + }, + 'Details View' + ), + m( + 'button.tab-btn' + (State.activeTab === 'chat' ? '.active' : ''), + { + onclick: () => { + State.activeTab = 'chat'; + const sslId = getOnlineSslId(State.selectedFriendGpgId); + if (sslId && !State.currentChatPeerId) { + startDirectChat(sslId); + } + }, + }, + 'Chat Conversation' + ), + ]), + m('.network-tab-content', [ + State.activeTab === 'details' ? m(DetailsTab) : m(ChatTab), + ]), + ] + : m('.network-pane-placeholder', [ + m('i.fas.fa-network-wired'), + m('p', 'Select a friend node from the left side panel to view locations details or start a private chat.'), + ]), + ]), + // Mail composer overlay popup + State.showMailCompose && + State.selectedFriendGpgId && + m( + '.composePopupOverlay#mailComposerPopup', + { style: { display: 'block' } }, + m( + '.composePopup', + m(compose, { + msgType: 'compose', + toId: selectedGxsId || State.selectedFriendGpgId, + friendName: selectedFriend ? selectedFriend.name : 'Unknown Friend', + isDirectMail: true, + setShowCompose: (val) => { + State.showMailCompose = val; + }, + }), + m( + 'button.red.close-btn', + { + onclick: () => { + State.showMailCompose = false; + }, + }, + m('i.fas.fa-times') + ) + ) + ), + ]); + }, + }; +}; + +module.exports = NetworkLayout; diff --git a/webui-src/app/network/network_data.js b/webui-src/app/network/network_data.js index de6bc61..a626ed4 100644 --- a/webui-src/app/network/network_data.js +++ b/webui-src/app/network/network_data.js @@ -33,28 +33,49 @@ Data.refreshGpgDetails = async function () { (stat) => (isOnline = stat.retval) ) .then(() => { - const loc = { - name: data.location, - id: data.id, - lastSeen: data.lastConnect, - isOnline, - gpg_id: data.gpg_id, - }; + let customState = ''; + return rs + .rsJsonApiRequest( + '/rsChats/getCustomStateString', + { peer_id: data.id }, + (statusData) => { + if (statusData && statusData.retval) { + customState = statusData.retval; + } + } + ) + .catch(() => {}) + .then(() => { + const gpgId = (data.gpg_id || '').toLowerCase(); + const loc = { + name: data.location, + id: data.id, + lastSeen: data.lastConnect, + isOnline, + gpg_id: gpgId, + customState, + }; - if (details[data.gpg_id] === undefined) { - details[data.gpg_id] = { - name: data.name, - isSearched: true, - isOnline, - locations: [loc], - }; - } else { - details[data.gpg_id].locations.push(loc); - } - details[data.gpg_id].isOnline = details[data.gpg_id].isOnline || isOnline; + if (details[gpgId] === undefined) { + details[gpgId] = { + name: data.name, + isSearched: true, + isOnline, + locations: [loc], + customState, + }; + } else { + details[gpgId].locations.push(loc); + if (!details[gpgId].customState || (isOnline && customState)) { + details[gpgId].customState = customState; + } + } + details[gpgId].isOnline = details[gpgId].isOnline || isOnline; + }); }); }) ); + Data.gpgDetails = details; }; module.exports = Data; diff --git a/webui-src/app/scss/pages/_network.scss b/webui-src/app/scss/pages/_network.scss index 0510013..2ff74cc 100644 --- a/webui-src/app/scss/pages/_network.scss +++ b/webui-src/app/scss/pages/_network.scss @@ -1,46 +1,544 @@ @use '../abstracts' as *; -.friend { - color: #444; - font-size: 1.2em; - margin: 1rem 0.5rem; - padding: 1.5rem; - border: 1px solid #aaa; - border-radius: 20px; - & i { - float: left; - padding: 0 10px; - cursor: pointer; - } - & h4 { - margin-bottom: 5px; - } - & button { - font-size: 0.9em; - } - &.hidden { - display: none; - } - & .brief-info.online { - color: green; +.network-container { + display: flex; + height: 100%; + width: 100%; + overflow: hidden; + background-color: #f1f5f9; +} + +.network-left-pane { + width: 320px; + min-width: 300px; + max-width: 350px; + border-right: 1px solid #cbd5e1; + display: flex; + flex-direction: column; + background: #ffffff; + box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05); +} + +.own-profile-card { + padding: 1.25rem; + border-bottom: 1px solid #e2e8f0; + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); + display: flex; + flex-direction: column; + gap: 0.75rem; + + .profile-header { + display: flex; + align-items: center; + gap: 1rem; } - & .location { - margin: 5px; - border-top: 1px solid #bbb; - display: grid; - grid-template-columns: auto auto; - justify-content: start; - } - & .brief-info { - @include flex($align: center); - justify-self: start; + .profile-info { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; + + .profile-name { + font-weight: 700; + color: #1e293b; + font-size: 1.1rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .profile-status { + font-size: 0.85rem; + color: #10b981; + font-weight: 500; + display: flex; + align-items: center; + gap: 0.35rem; + + &::before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + background-color: #10b981; + border-radius: 50%; + } + } } - & .fa-times-circle { - color: #555; - } - & .fa-check-circle { - color: green; + .own-identity-select-container { + display: flex; + flex-direction: column; + gap: 0.25rem; + + label { + font-size: 0.75rem; + color: #64748b; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + select.own-identity-select { + width: 100%; + padding: 0.375rem 0.5rem; + font-size: 0.85rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + background-color: #ffffff; + color: #334155; + outline: none; + cursor: pointer; + transition: border-color 0.2s; + + &:focus { + border-color: #3ba4d7; + } + } + } +} + +.friends-list-container { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + + .searchbar-container { + padding: 0.75rem 1rem; + border-bottom: 1px solid #e2e8f0; + + input.searchbar { + width: 100%; + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + background-color: #f8fafc; + outline: none; + transition: all 0.2s; + + &:focus { + background-color: #ffffff; + border-color: #3ba4d7; + box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); + } + } + } + + .friends-scroll { + flex: 1; + overflow-y: auto; + padding: 0.5rem 0; + } +} + +.friend-list-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + margin: 0.125rem 0.5rem; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background-color: #f1f5f9; + } + + &.selected { + background-color: #e0f2fe; + + .friend-name { + color: #0369a1; + font-weight: 600; + } + } + + .friend-avatar { + flex-shrink: 0; + } + + .friend-meta { + flex: 1; + min-width: 0; + + .friend-name { + font-size: 0.95rem; + color: #334155; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: color 0.2s; + } + + .friend-status { + font-size: 0.8rem; + color: #94a3b8; + + &.online { + color: #10b981; + font-weight: 500; + } + } + } +} + +.network-right-pane { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: #f8fafc; +} + +.network-pane-placeholder { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: #94a3b8; + gap: 1rem; + padding: 2rem; + text-align: center; + + i { + font-size: 4rem; + color: #cbd5e1; + } + + p { + font-size: 1.1rem; + max-width: 400px; + } +} + +.network-tabs { + display: flex; + background-color: #ffffff; + border-bottom: 1px solid #cbd5e1; + padding: 0.5rem 1rem 0; + gap: 0.5rem; + + .tab-btn { + padding: 0.625rem 1.25rem; + font-size: 0.95rem; + font-weight: 600; + color: #64748b; + background: transparent; + border: none; + border-radius: 0.375rem 0.375rem 0 0; + border-bottom: 3px solid transparent; + cursor: pointer; + box-shadow: none; + transition: all 0.2s; + + &:hover { + color: #334155; + background-color: #f1f5f9; + } + + &.active { + color: #3ba4d7; + border-bottom-color: #3ba4d7; + background-color: transparent; + } + } +} + +.network-tab-content { + flex: 1; + overflow-y: auto; + padding: 1.5rem; +} + +.network-detail-view { + display: flex; + flex-direction: column; + gap: 1.5rem; + + .detail-header { + display: flex; + align-items: center; + gap: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid #e2e8f0; + + .detail-title { + flex: 1; + + h2 { + font-size: 1.75rem; + font-weight: 800; + color: #1e293b; + margin-bottom: 0.25rem; + } + + .detail-subtitle { + font-size: 0.9rem; + color: #64748b; + display: flex; + align-items: center; + gap: 0.5rem; + } + } + + .detail-actions { + display: flex; + gap: 0.75rem; + + button { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + font-size: 0.9rem; + } + } + } + + .detail-section { + background-color: #ffffff; + border-radius: 0.5rem; + border: 1px solid #e2e8f0; + padding: 1.25rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); + + h3 { + font-size: 1.1rem; + font-weight: 700; + color: #334155; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid #f1f5f9; + } + + .info-grid { + display: grid; + grid-template-columns: 120px 1fr; + row-gap: 0.75rem; + font-size: 0.9rem; + + .info-label { + font-weight: 600; + color: #64748b; + } + + .info-value { + color: #1e293b; + word-break: break-all; + } + } + } + + .locations-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; + } + + .location-card { + background-color: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 0.5rem; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); + + .loc-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #f1f5f9; + padding-bottom: 0.5rem; + margin-bottom: 0.25rem; + + .loc-name { + font-weight: 700; + color: #334155; + font-size: 0.95rem; + } + + .loc-status { + font-size: 0.75rem; + font-weight: 600; + padding: 0.125rem 0.5rem; + border-radius: 0.25rem; + + &.online { + background-color: #d1fae5; + color: #065f46; + } + + &.offline { + background-color: #f1f5f9; + color: #475569; + } + } + } + + .loc-body { + font-size: 0.85rem; + display: grid; + grid-template-columns: 80px 1fr; + row-gap: 0.25rem; + + .loc-label { + color: #64748b; + } + + .loc-val { + color: #334155; + word-break: break-all; + } + } + + .loc-footer { + margin-top: 0.5rem; + display: flex; + justify-content: flex-end; + + button { + font-size: 0.8rem; + padding: 0.25rem 0.75rem; + } + } + } +} + +.network-chat-view { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + background-color: #f8fafc; + + .chat-messages { + flex: 1; + overflow-y: auto; + padding: 1.25rem; + display: flex; + flex-direction: column; + gap: 1rem; + } + + .chat-bubble-container { + display: flex; + flex-direction: column; + max-width: 70%; + + &.outgoing { + align-self: flex-end; + align-items: flex-end; + + .chat-bubble { + background-color: #3ba4d7; + color: #ffffff; + border-bottom-right-radius: 0.125rem; + } + } + + &.incoming { + align-self: flex-start; + align-items: flex-start; + + .chat-bubble { + background-color: #ffffff; + color: #1e293b; + border: 1px solid #e2e8f0; + border-bottom-left-radius: 0.125rem; + } + } + + .chat-sender { + font-size: 0.75rem; + color: #64748b; + margin-bottom: 0.25rem; + padding: 0 0.25rem; + } + + .chat-bubble { + padding: 0.625rem 0.875rem; + border-radius: 0.75rem; + font-size: 0.925rem; + line-height: 1.4; + white-space: break-spaces; + word-break: break-word; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); + } + + .chat-time { + font-size: 0.7rem; + color: #94a3b8; + margin-top: 0.25rem; + padding: 0 0.25rem; + } + } + + .chat-input-area { + padding: 1rem; + background-color: #ffffff; + border-top: 1px solid #cbd5e1; + display: flex; + gap: 0.75rem; + align-items: center; + + textarea.chat-textarea { + flex: 1; + resize: none; + height: 40px; + padding: 0.5rem 0.75rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + font-size: 0.9rem; + outline: none; + transition: all 0.2s; + + &:focus { + border-color: #3ba4d7; + box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); + } + } + + button.send-btn { + padding: 0.5rem 1.25rem; + font-size: 0.9rem; + height: 40px; + display: flex; + align-items: center; + gap: 0.5rem; + } + } + + .chat-warning { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: #64748b; + text-align: center; + padding: 2rem; + gap: 1rem; + + i { + font-size: 3rem; + color: #cbd5e1; + } + + h4 { + font-weight: 700; + color: #334155; + } + + p { + max-width: 350px; + font-size: 0.9rem; + } } } diff --git a/webui-src/styles.css b/webui-src/styles.css index 7930463..a77548a 100644 --- a/webui-src/styles.css +++ b/webui-src/styles.css @@ -1,7 +1,552 @@ -h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem}h5{font-size:1.25rem}h6{font-size:1.125rem}p{font-size:1rem}.small{font-size:.75rem}.bold{font-weight:bold}h1,h2,h3,h4,h5,h6,p{font-weight:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Bold.woff2") format("woff2"),url("./webfonts/Roboto-Bold.woff") format("woff"),url("./webfonts/Roboto-Bold.ttf") format("truetype");font-weight:700;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Bold.woff2") format("woff2"),url("./webfonts/Roboto-Bold.woff") format("woff"),url("./webfonts/Roboto-Bold.ttf") format("truetype");font-weight:bold;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-BoldItalic.woff2") format("woff2"),url("./webfonts/Roboto-BoldItalic.woff") format("woff"),url("./webfonts/Roboto-BoldItalic.ttf") format("truetype");font-weight:700;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-BoldItalic.woff2") format("woff2"),url("./webfonts/Roboto-BoldItalic.woff") format("woff"),url("./webfonts/Roboto-BoldItalic.ttf") format("truetype");font-weight:bold;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Medium.woff2") format("woff2"),url("./webfonts/Roboto-Medium.woff") format("woff"),url("./webfonts/Roboto-Medium.ttf") format("truetype");font-weight:500;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-MediumItalic.woff2") format("woff2"),url("./webfonts/Roboto-MediumItalic.woff") format("woff"),url("./webfonts/Roboto-MediumItalic.ttf") format("truetype");font-weight:500;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Regular.woff2") format("woff2"),url("./webfonts/Roboto-Regular.woff") format("woff"),url("./webfonts/Roboto-Regular.ttf") format("truetype");font-weight:400;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Regular.woff2") format("woff2"),url("./webfonts/Roboto-Regular.woff") format("woff"),url("./webfonts/Roboto-Regular.ttf") format("truetype");font-weight:normal;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Italic.woff2") format("woff2"),url("./webfonts/Roboto-Italic.woff") format("woff"),url("./webfonts/Roboto-Italic.ttf") format("truetype");font-weight:400;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Italic.woff2") format("woff2"),url("./webfonts/Roboto-Italic.woff") format("woff"),url("./webfonts/Roboto-Italic.ttf") format("truetype");font-weight:normal;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Light.woff2") format("woff2"),url("./webfonts/Roboto-Light.woff") format("woff"),url("./webfonts/Roboto-Light.ttf") format("truetype");font-weight:300;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-LightItalic.woff2") format("woff2"),url("./webfonts/Roboto-LightItalic.woff") format("woff"),url("./webfonts/Roboto-LightItalic.ttf") format("truetype");font-weight:300;font-style:italic}/*! +h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem}h5{font-size:1.25rem}h6{font-size:1.125rem}p{font-size:1rem}.small{font-size:.75rem}.bold{font-weight:bold}h1,h2,h3,h4,h5,h6,p{font-weight:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Bold.woff2") format("woff2"),url("./webfonts/Roboto-Bold.woff") format("woff"),url("./webfonts/Roboto-Bold.ttf") format("truetype");font-weight:700;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Bold.woff2") format("woff2"),url("./webfonts/Roboto-Bold.woff") format("woff"),url("./webfonts/Roboto-Bold.ttf") format("truetype");font-weight:bold;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-BoldItalic.woff2") format("woff2"),url("./webfonts/Roboto-BoldItalic.woff") format("woff"),url("./webfonts/Roboto-BoldItalic.ttf") format("truetype");font-weight:700;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-BoldItalic.woff2") format("woff2"),url("./webfonts/Roboto-BoldItalic.woff") format("woff"),url("./webfonts/Roboto-BoldItalic.ttf") format("truetype");font-weight:bold;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Medium.woff2") format("woff2"),url("./webfonts/Roboto-Medium.woff") format("woff"),url("./webfonts/Roboto-Medium.ttf") format("truetype");font-weight:500;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-MediumItalic.woff2") format("woff2"),url("./webfonts/Roboto-MediumItalic.woff") format("woff"),url("./webfonts/Roboto-MediumItalic.ttf") format("truetype");font-weight:500;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Regular.woff2") format("woff2"),url("./webfonts/Roboto-Regular.woff") format("woff"),url("./webfonts/Roboto-Regular.ttf") format("truetype");font-weight:400;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Regular.woff2") format("woff2"),url("./webfonts/Roboto-Regular.woff") format("woff"),url("./webfonts/Roboto-Regular.ttf") format("truetype");font-weight:normal;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Italic.woff2") format("woff2"),url("./webfonts/Roboto-Italic.woff") format("woff"),url("./webfonts/Roboto-Italic.ttf") format("truetype");font-weight:400;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Italic.woff2") format("woff2"),url("./webfonts/Roboto-Italic.woff") format("woff"),url("./webfonts/Roboto-Italic.ttf") format("truetype");font-weight:normal;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Light.woff2") format("woff2"),url("./webfonts/Roboto-Light.woff") format("woff"),url("./webfonts/Roboto-Light.ttf") format("truetype");font-weight:300;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-LightItalic.woff2") format("woff2"),url("./webfonts/Roboto-LightItalic.woff") format("woff"),url("./webfonts/Roboto-LightItalic.ttf") format("truetype");font-weight:300;font-style:italic}/*! * Font Awesome Free 5.9.0 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) */.fa,.fas,.far,.fal,.fab{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-0.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:solid .08em #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fas.fa-pull-left,.far.fa-pull-left,.fal.fa-pull-left,.fab.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fas.fa-pull-right,.far.fa-pull-right,.fal.fa-pull-right,.fab.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);transform:scale(1, -1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(-1, -1);transform:scale(-1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-flip-both{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:""}.fa-accessible-icon:before{content:""}.fa-accusoft:before{content:""}.fa-acquisitions-incorporated:before{content:""}.fa-ad:before{content:""}.fa-address-book:before{content:""}.fa-address-card:before{content:""}.fa-adjust:before{content:""}.fa-adn:before{content:""}.fa-adobe:before{content:""}.fa-adversal:before{content:""}.fa-affiliatetheme:before{content:""}.fa-air-freshener:before{content:""}.fa-airbnb:before{content:""}.fa-algolia:before{content:""}.fa-align-center:before{content:""}.fa-align-justify:before{content:""}.fa-align-left:before{content:""}.fa-align-right:before{content:""}.fa-alipay:before{content:""}.fa-allergies:before{content:""}.fa-amazon:before{content:""}.fa-amazon-pay:before{content:""}.fa-ambulance:before{content:""}.fa-american-sign-language-interpreting:before{content:""}.fa-amilia:before{content:""}.fa-anchor:before{content:""}.fa-android:before{content:""}.fa-angellist:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angry:before{content:""}.fa-angrycreative:before{content:""}.fa-angular:before{content:""}.fa-ankh:before{content:""}.fa-app-store:before{content:""}.fa-app-store-ios:before{content:""}.fa-apper:before{content:""}.fa-apple:before{content:""}.fa-apple-alt:before{content:""}.fa-apple-pay:before{content:""}.fa-archive:before{content:""}.fa-archway:before{content:""}.fa-arrow-alt-circle-down:before{content:""}.fa-arrow-alt-circle-left:before{content:""}.fa-arrow-alt-circle-right:before{content:""}.fa-arrow-alt-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-arrow-circle-left:before{content:""}.fa-arrow-circle-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-down:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrows-alt:before{content:""}.fa-arrows-alt-h:before{content:""}.fa-arrows-alt-v:before{content:""}.fa-artstation:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-asterisk:before{content:""}.fa-asymmetrik:before{content:""}.fa-at:before{content:""}.fa-atlas:before{content:""}.fa-atlassian:before{content:""}.fa-atom:before{content:""}.fa-audible:before{content:""}.fa-audio-description:before{content:""}.fa-autoprefixer:before{content:""}.fa-avianex:before{content:""}.fa-aviato:before{content:""}.fa-award:before{content:""}.fa-aws:before{content:""}.fa-baby:before{content:""}.fa-baby-carriage:before{content:""}.fa-backspace:before{content:""}.fa-backward:before{content:""}.fa-bacon:before{content:""}.fa-balance-scale:before{content:""}.fa-balance-scale-left:before{content:""}.fa-balance-scale-right:before{content:""}.fa-ban:before{content:""}.fa-band-aid:before{content:""}.fa-bandcamp:before{content:""}.fa-barcode:before{content:""}.fa-bars:before{content:""}.fa-baseball-ball:before{content:""}.fa-basketball-ball:before{content:""}.fa-bath:before{content:""}.fa-battery-empty:before{content:""}.fa-battery-full:before{content:""}.fa-battery-half:before{content:""}.fa-battery-quarter:before{content:""}.fa-battery-three-quarters:before{content:""}.fa-battle-net:before{content:""}.fa-bed:before{content:""}.fa-beer:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-bell:before{content:""}.fa-bell-slash:before{content:""}.fa-bezier-curve:before{content:""}.fa-bible:before{content:""}.fa-bicycle:before{content:""}.fa-biking:before{content:""}.fa-bimobject:before{content:""}.fa-binoculars:before{content:""}.fa-biohazard:before{content:""}.fa-birthday-cake:before{content:""}.fa-bitbucket:before{content:""}.fa-bitcoin:before{content:""}.fa-bity:before{content:""}.fa-black-tie:before{content:""}.fa-blackberry:before{content:""}.fa-blender:before{content:""}.fa-blender-phone:before{content:""}.fa-blind:before{content:""}.fa-blog:before{content:""}.fa-blogger:before{content:""}.fa-blogger-b:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-bold:before{content:""}.fa-bolt:before{content:""}.fa-bomb:before{content:""}.fa-bone:before{content:""}.fa-bong:before{content:""}.fa-book:before{content:""}.fa-book-dead:before{content:""}.fa-book-medical:before{content:""}.fa-book-open:before{content:""}.fa-book-reader:before{content:""}.fa-bookmark:before{content:""}.fa-bootstrap:before{content:""}.fa-border-all:before{content:""}.fa-border-none:before{content:""}.fa-border-style:before{content:""}.fa-bowling-ball:before{content:""}.fa-box:before{content:""}.fa-box-open:before{content:""}.fa-boxes:before{content:""}.fa-braille:before{content:""}.fa-brain:before{content:""}.fa-bread-slice:before{content:""}.fa-briefcase:before{content:""}.fa-briefcase-medical:before{content:""}.fa-broadcast-tower:before{content:""}.fa-broom:before{content:""}.fa-brush:before{content:""}.fa-btc:before{content:""}.fa-buffer:before{content:""}.fa-bug:before{content:""}.fa-building:before{content:""}.fa-bullhorn:before{content:""}.fa-bullseye:before{content:""}.fa-burn:before{content:""}.fa-buromobelexperte:before{content:""}.fa-bus:before{content:""}.fa-bus-alt:before{content:""}.fa-business-time:before{content:""}.fa-buysellads:before{content:""}.fa-calculator:before{content:""}.fa-calendar:before{content:""}.fa-calendar-alt:before{content:""}.fa-calendar-check:before{content:""}.fa-calendar-day:before{content:""}.fa-calendar-minus:before{content:""}.fa-calendar-plus:before{content:""}.fa-calendar-times:before{content:""}.fa-calendar-week:before{content:""}.fa-camera:before{content:""}.fa-camera-retro:before{content:""}.fa-campground:before{content:""}.fa-canadian-maple-leaf:before{content:""}.fa-candy-cane:before{content:""}.fa-cannabis:before{content:""}.fa-capsules:before{content:""}.fa-car:before{content:""}.fa-car-alt:before{content:""}.fa-car-battery:before{content:""}.fa-car-crash:before{content:""}.fa-car-side:before{content:""}.fa-caret-down:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-caret-square-down:before{content:""}.fa-caret-square-left:before{content:""}.fa-caret-square-right:before{content:""}.fa-caret-square-up:before{content:""}.fa-caret-up:before{content:""}.fa-carrot:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-cart-plus:before{content:""}.fa-cash-register:before{content:""}.fa-cat:before{content:""}.fa-cc-amazon-pay:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-apple-pay:before{content:""}.fa-cc-diners-club:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-cc-visa:before{content:""}.fa-centercode:before{content:""}.fa-centos:before{content:""}.fa-certificate:before{content:""}.fa-chair:before{content:""}.fa-chalkboard:before{content:""}.fa-chalkboard-teacher:before{content:""}.fa-charging-station:before{content:""}.fa-chart-area:before{content:""}.fa-chart-bar:before{content:""}.fa-chart-line:before{content:""}.fa-chart-pie:before{content:""}.fa-check:before{content:""}.fa-check-circle:before{content:""}.fa-check-double:before{content:""}.fa-check-square:before{content:""}.fa-cheese:before{content:""}.fa-chess:before{content:""}.fa-chess-bishop:before{content:""}.fa-chess-board:before{content:""}.fa-chess-king:before{content:""}.fa-chess-knight:before{content:""}.fa-chess-pawn:before{content:""}.fa-chess-queen:before{content:""}.fa-chess-rook:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-down:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-chevron-up:before{content:""}.fa-child:before{content:""}.fa-chrome:before{content:""}.fa-chromecast:before{content:""}.fa-church:before{content:""}.fa-circle:before{content:""}.fa-circle-notch:before{content:""}.fa-city:before{content:""}.fa-clinic-medical:before{content:""}.fa-clipboard:before{content:""}.fa-clipboard-check:before{content:""}.fa-clipboard-list:before{content:""}.fa-clock:before{content:""}.fa-clone:before{content:""}.fa-closed-captioning:before{content:""}.fa-cloud:before{content:""}.fa-cloud-download-alt:before{content:""}.fa-cloud-meatball:before{content:""}.fa-cloud-moon:before{content:""}.fa-cloud-moon-rain:before{content:""}.fa-cloud-rain:before{content:""}.fa-cloud-showers-heavy:before{content:""}.fa-cloud-sun:before{content:""}.fa-cloud-sun-rain:before{content:""}.fa-cloud-upload-alt:before{content:""}.fa-cloudscale:before{content:""}.fa-cloudsmith:before{content:""}.fa-cloudversify:before{content:""}.fa-cocktail:before{content:""}.fa-code:before{content:""}.fa-code-branch:before{content:""}.fa-codepen:before{content:""}.fa-codiepie:before{content:""}.fa-coffee:before{content:""}.fa-cog:before{content:""}.fa-cogs:before{content:""}.fa-coins:before{content:""}.fa-columns:before{content:""}.fa-comment:before{content:""}.fa-comment-alt:before{content:""}.fa-comment-dollar:before{content:""}.fa-comment-dots:before{content:""}.fa-comment-medical:before{content:""}.fa-comment-slash:before{content:""}.fa-comments:before{content:""}.fa-comments-dollar:before{content:""}.fa-compact-disc:before{content:""}.fa-compass:before{content:""}.fa-compress:before{content:""}.fa-compress-arrows-alt:before{content:""}.fa-concierge-bell:before{content:""}.fa-confluence:before{content:""}.fa-connectdevelop:before{content:""}.fa-contao:before{content:""}.fa-cookie:before{content:""}.fa-cookie-bite:before{content:""}.fa-copy:before{content:""}.fa-copyright:before{content:""}.fa-couch:before{content:""}.fa-cpanel:before{content:""}.fa-creative-commons:before{content:""}.fa-creative-commons-by:before{content:""}.fa-creative-commons-nc:before{content:""}.fa-creative-commons-nc-eu:before{content:""}.fa-creative-commons-nc-jp:before{content:""}.fa-creative-commons-nd:before{content:""}.fa-creative-commons-pd:before{content:""}.fa-creative-commons-pd-alt:before{content:""}.fa-creative-commons-remix:before{content:""}.fa-creative-commons-sa:before{content:""}.fa-creative-commons-sampling:before{content:""}.fa-creative-commons-sampling-plus:before{content:""}.fa-creative-commons-share:before{content:""}.fa-creative-commons-zero:before{content:""}.fa-credit-card:before{content:""}.fa-critical-role:before{content:""}.fa-crop:before{content:""}.fa-crop-alt:before{content:""}.fa-cross:before{content:""}.fa-crosshairs:before{content:""}.fa-crow:before{content:""}.fa-crown:before{content:""}.fa-crutch:before{content:""}.fa-css3:before{content:""}.fa-css3-alt:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-cut:before{content:""}.fa-cuttlefish:before{content:""}.fa-d-and-d:before{content:""}.fa-d-and-d-beyond:before{content:""}.fa-dashcube:before{content:""}.fa-database:before{content:""}.fa-deaf:before{content:""}.fa-delicious:before{content:""}.fa-democrat:before{content:""}.fa-deploydog:before{content:""}.fa-deskpro:before{content:""}.fa-desktop:before{content:""}.fa-dev:before{content:""}.fa-deviantart:before{content:""}.fa-dharmachakra:before{content:""}.fa-dhl:before{content:""}.fa-diagnoses:before{content:""}.fa-diaspora:before{content:""}.fa-dice:before{content:""}.fa-dice-d20:before{content:""}.fa-dice-d6:before{content:""}.fa-dice-five:before{content:""}.fa-dice-four:before{content:""}.fa-dice-one:before{content:""}.fa-dice-six:before{content:""}.fa-dice-three:before{content:""}.fa-dice-two:before{content:""}.fa-digg:before{content:""}.fa-digital-ocean:before{content:""}.fa-digital-tachograph:before{content:""}.fa-directions:before{content:""}.fa-discord:before{content:""}.fa-discourse:before{content:""}.fa-divide:before{content:""}.fa-dizzy:before{content:""}.fa-dna:before{content:""}.fa-dochub:before{content:""}.fa-docker:before{content:""}.fa-dog:before{content:""}.fa-dollar-sign:before{content:""}.fa-dolly:before{content:""}.fa-dolly-flatbed:before{content:""}.fa-donate:before{content:""}.fa-door-closed:before{content:""}.fa-door-open:before{content:""}.fa-dot-circle:before{content:""}.fa-dove:before{content:""}.fa-download:before{content:""}.fa-draft2digital:before{content:""}.fa-drafting-compass:before{content:""}.fa-dragon:before{content:""}.fa-draw-polygon:before{content:""}.fa-dribbble:before{content:""}.fa-dribbble-square:before{content:""}.fa-dropbox:before{content:""}.fa-drum:before{content:""}.fa-drum-steelpan:before{content:""}.fa-drumstick-bite:before{content:""}.fa-drupal:before{content:""}.fa-dumbbell:before{content:""}.fa-dumpster:before{content:""}.fa-dumpster-fire:before{content:""}.fa-dungeon:before{content:""}.fa-dyalog:before{content:""}.fa-earlybirds:before{content:""}.fa-ebay:before{content:""}.fa-edge:before{content:""}.fa-edit:before{content:""}.fa-egg:before{content:""}.fa-eject:before{content:""}.fa-elementor:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-ello:before{content:""}.fa-ember:before{content:""}.fa-empire:before{content:""}.fa-envelope:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-text:before{content:""}.fa-envelope-square:before{content:""}.fa-envira:before{content:""}.fa-equals:before{content:""}.fa-eraser:before{content:""}.fa-erlang:before{content:""}.fa-ethereum:before{content:""}.fa-ethernet:before{content:""}.fa-etsy:before{content:""}.fa-euro-sign:before{content:""}.fa-evernote:before{content:""}.fa-exchange-alt:before{content:""}.fa-exclamation:before{content:""}.fa-exclamation-circle:before{content:""}.fa-exclamation-triangle:before{content:""}.fa-expand:before{content:""}.fa-expand-arrows-alt:before{content:""}.fa-expeditedssl:before{content:""}.fa-external-link-alt:before{content:""}.fa-external-link-square-alt:before{content:""}.fa-eye:before{content:""}.fa-eye-dropper:before{content:""}.fa-eye-slash:before{content:""}.fa-facebook:before{content:""}.fa-facebook-f:before{content:""}.fa-facebook-messenger:before{content:""}.fa-facebook-square:before{content:""}.fa-fan:before{content:""}.fa-fantasy-flight-games:before{content:""}.fa-fast-backward:before{content:""}.fa-fast-forward:before{content:""}.fa-fax:before{content:""}.fa-feather:before{content:""}.fa-feather-alt:before{content:""}.fa-fedex:before{content:""}.fa-fedora:before{content:""}.fa-female:before{content:""}.fa-fighter-jet:before{content:""}.fa-figma:before{content:""}.fa-file:before{content:""}.fa-file-alt:before{content:""}.fa-file-archive:before{content:""}.fa-file-audio:before{content:""}.fa-file-code:before{content:""}.fa-file-contract:before{content:""}.fa-file-csv:before{content:""}.fa-file-download:before{content:""}.fa-file-excel:before{content:""}.fa-file-export:before{content:""}.fa-file-image:before{content:""}.fa-file-import:before{content:""}.fa-file-invoice:before{content:""}.fa-file-invoice-dollar:before{content:""}.fa-file-medical:before{content:""}.fa-file-medical-alt:before{content:""}.fa-file-pdf:before{content:""}.fa-file-powerpoint:before{content:""}.fa-file-prescription:before{content:""}.fa-file-signature:before{content:""}.fa-file-upload:before{content:""}.fa-file-video:before{content:""}.fa-file-word:before{content:""}.fa-fill:before{content:""}.fa-fill-drip:before{content:""}.fa-film:before{content:""}.fa-filter:before{content:""}.fa-fingerprint:before{content:""}.fa-fire:before{content:""}.fa-fire-alt:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-firefox:before{content:""}.fa-first-aid:before{content:""}.fa-first-order:before{content:""}.fa-first-order-alt:before{content:""}.fa-firstdraft:before{content:""}.fa-fish:before{content:""}.fa-fist-raised:before{content:""}.fa-flag:before{content:""}.fa-flag-checkered:before{content:""}.fa-flag-usa:before{content:""}.fa-flask:before{content:""}.fa-flickr:before{content:""}.fa-flipboard:before{content:""}.fa-flushed:before{content:""}.fa-fly:before{content:""}.fa-folder:before{content:""}.fa-folder-minus:before{content:""}.fa-folder-open:before{content:""}.fa-folder-plus:before{content:""}.fa-font:before{content:""}.fa-font-awesome:before{content:""}.fa-font-awesome-alt:before{content:""}.fa-font-awesome-flag:before{content:""}.fa-font-awesome-logo-full:before{content:""}.fa-fonticons:before{content:""}.fa-fonticons-fi:before{content:""}.fa-football-ball:before{content:""}.fa-fort-awesome:before{content:""}.fa-fort-awesome-alt:before{content:""}.fa-forumbee:before{content:""}.fa-forward:before{content:""}.fa-foursquare:before{content:""}.fa-free-code-camp:before{content:""}.fa-freebsd:before{content:""}.fa-frog:before{content:""}.fa-frown:before{content:""}.fa-frown-open:before{content:""}.fa-fulcrum:before{content:""}.fa-funnel-dollar:before{content:""}.fa-futbol:before{content:""}.fa-galactic-republic:before{content:""}.fa-galactic-senate:before{content:""}.fa-gamepad:before{content:""}.fa-gas-pump:before{content:""}.fa-gavel:before{content:""}.fa-gem:before{content:""}.fa-genderless:before{content:""}.fa-get-pocket:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-ghost:before{content:""}.fa-gift:before{content:""}.fa-gifts:before{content:""}.fa-git:before{content:""}.fa-git-alt:before{content:""}.fa-git-square:before{content:""}.fa-github:before{content:""}.fa-github-alt:before{content:""}.fa-github-square:before{content:""}.fa-gitkraken:before{content:""}.fa-gitlab:before{content:""}.fa-gitter:before{content:""}.fa-glass-cheers:before{content:""}.fa-glass-martini:before{content:""}.fa-glass-martini-alt:before{content:""}.fa-glass-whiskey:before{content:""}.fa-glasses:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-globe:before{content:""}.fa-globe-africa:before{content:""}.fa-globe-americas:before{content:""}.fa-globe-asia:before{content:""}.fa-globe-europe:before{content:""}.fa-gofore:before{content:""}.fa-golf-ball:before{content:""}.fa-goodreads:before{content:""}.fa-goodreads-g:before{content:""}.fa-google:before{content:""}.fa-google-drive:before{content:""}.fa-google-play:before{content:""}.fa-google-plus:before{content:""}.fa-google-plus-g:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-wallet:before{content:""}.fa-gopuram:before{content:""}.fa-graduation-cap:before{content:""}.fa-gratipay:before{content:""}.fa-grav:before{content:""}.fa-greater-than:before{content:""}.fa-greater-than-equal:before{content:""}.fa-grimace:before{content:""}.fa-grin:before{content:""}.fa-grin-alt:before{content:""}.fa-grin-beam:before{content:""}.fa-grin-beam-sweat:before{content:""}.fa-grin-hearts:before{content:""}.fa-grin-squint:before{content:""}.fa-grin-squint-tears:before{content:""}.fa-grin-stars:before{content:""}.fa-grin-tears:before{content:""}.fa-grin-tongue:before{content:""}.fa-grin-tongue-squint:before{content:""}.fa-grin-tongue-wink:before{content:""}.fa-grin-wink:before{content:""}.fa-grip-horizontal:before{content:""}.fa-grip-lines:before{content:""}.fa-grip-lines-vertical:before{content:""}.fa-grip-vertical:before{content:""}.fa-gripfire:before{content:""}.fa-grunt:before{content:""}.fa-guitar:before{content:""}.fa-gulp:before{content:""}.fa-h-square:before{content:""}.fa-hacker-news:before{content:""}.fa-hacker-news-square:before{content:""}.fa-hackerrank:before{content:""}.fa-hamburger:before{content:""}.fa-hammer:before{content:""}.fa-hamsa:before{content:""}.fa-hand-holding:before{content:""}.fa-hand-holding-heart:before{content:""}.fa-hand-holding-usd:before{content:""}.fa-hand-lizard:before{content:""}.fa-hand-middle-finger:before{content:""}.fa-hand-paper:before{content:""}.fa-hand-peace:before{content:""}.fa-hand-point-down:before{content:""}.fa-hand-point-left:before{content:""}.fa-hand-point-right:before{content:""}.fa-hand-point-up:before{content:""}.fa-hand-pointer:before{content:""}.fa-hand-rock:before{content:""}.fa-hand-scissors:before{content:""}.fa-hand-spock:before{content:""}.fa-hands:before{content:""}.fa-hands-helping:before{content:""}.fa-handshake:before{content:""}.fa-hanukiah:before{content:""}.fa-hard-hat:before{content:""}.fa-hashtag:before{content:""}.fa-hat-wizard:before{content:""}.fa-haykal:before{content:""}.fa-hdd:before{content:""}.fa-heading:before{content:""}.fa-headphones:before{content:""}.fa-headphones-alt:before{content:""}.fa-headset:before{content:""}.fa-heart:before{content:""}.fa-heart-broken:before{content:""}.fa-heartbeat:before{content:""}.fa-helicopter:before{content:""}.fa-highlighter:before{content:""}.fa-hiking:before{content:""}.fa-hippo:before{content:""}.fa-hips:before{content:""}.fa-hire-a-helper:before{content:""}.fa-history:before{content:""}.fa-hockey-puck:before{content:""}.fa-holly-berry:before{content:""}.fa-home:before{content:""}.fa-hooli:before{content:""}.fa-hornbill:before{content:""}.fa-horse:before{content:""}.fa-horse-head:before{content:""}.fa-hospital:before{content:""}.fa-hospital-alt:before{content:""}.fa-hospital-symbol:before{content:""}.fa-hot-tub:before{content:""}.fa-hotdog:before{content:""}.fa-hotel:before{content:""}.fa-hotjar:before{content:""}.fa-hourglass:before{content:""}.fa-hourglass-end:before{content:""}.fa-hourglass-half:before{content:""}.fa-hourglass-start:before{content:""}.fa-house-damage:before{content:""}.fa-houzz:before{content:""}.fa-hryvnia:before{content:""}.fa-html5:before{content:""}.fa-hubspot:before{content:""}.fa-i-cursor:before{content:""}.fa-ice-cream:before{content:""}.fa-icicles:before{content:""}.fa-icons:before{content:""}.fa-id-badge:before{content:""}.fa-id-card:before{content:""}.fa-id-card-alt:before{content:""}.fa-igloo:before{content:""}.fa-image:before{content:""}.fa-images:before{content:""}.fa-imdb:before{content:""}.fa-inbox:before{content:""}.fa-indent:before{content:""}.fa-industry:before{content:""}.fa-infinity:before{content:""}.fa-info:before{content:""}.fa-info-circle:before{content:""}.fa-instagram:before{content:""}.fa-intercom:before{content:""}.fa-internet-explorer:before{content:""}.fa-invision:before{content:""}.fa-ioxhost:before{content:""}.fa-italic:before{content:""}.fa-itch-io:before{content:""}.fa-itunes:before{content:""}.fa-itunes-note:before{content:""}.fa-java:before{content:""}.fa-jedi:before{content:""}.fa-jedi-order:before{content:""}.fa-jenkins:before{content:""}.fa-jira:before{content:""}.fa-joget:before{content:""}.fa-joint:before{content:""}.fa-joomla:before{content:""}.fa-journal-whills:before{content:""}.fa-js:before{content:""}.fa-js-square:before{content:""}.fa-jsfiddle:before{content:""}.fa-kaaba:before{content:""}.fa-kaggle:before{content:""}.fa-key:before{content:""}.fa-keybase:before{content:""}.fa-keyboard:before{content:""}.fa-keycdn:before{content:""}.fa-khanda:before{content:""}.fa-kickstarter:before{content:""}.fa-kickstarter-k:before{content:""}.fa-kiss:before{content:""}.fa-kiss-beam:before{content:""}.fa-kiss-wink-heart:before{content:""}.fa-kiwi-bird:before{content:""}.fa-korvue:before{content:""}.fa-landmark:before{content:""}.fa-language:before{content:""}.fa-laptop:before{content:""}.fa-laptop-code:before{content:""}.fa-laptop-medical:before{content:""}.fa-laravel:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-laugh:before{content:""}.fa-laugh-beam:before{content:""}.fa-laugh-squint:before{content:""}.fa-laugh-wink:before{content:""}.fa-layer-group:before{content:""}.fa-leaf:before{content:""}.fa-leanpub:before{content:""}.fa-lemon:before{content:""}.fa-less:before{content:""}.fa-less-than:before{content:""}.fa-less-than-equal:before{content:""}.fa-level-down-alt:before{content:""}.fa-level-up-alt:before{content:""}.fa-life-ring:before{content:""}.fa-lightbulb:before{content:""}.fa-line:before{content:""}.fa-link:before{content:""}.fa-linkedin:before{content:""}.fa-linkedin-in:before{content:""}.fa-linode:before{content:""}.fa-linux:before{content:""}.fa-lira-sign:before{content:""}.fa-list:before{content:""}.fa-list-alt:before{content:""}.fa-list-ol:before{content:""}.fa-list-ul:before{content:""}.fa-location-arrow:before{content:""}.fa-lock:before{content:""}.fa-lock-open:before{content:""}.fa-long-arrow-alt-down:before{content:""}.fa-long-arrow-alt-left:before{content:""}.fa-long-arrow-alt-right:before{content:""}.fa-long-arrow-alt-up:before{content:""}.fa-low-vision:before{content:""}.fa-luggage-cart:before{content:""}.fa-lyft:before{content:""}.fa-magento:before{content:""}.fa-magic:before{content:""}.fa-magnet:before{content:""}.fa-mail-bulk:before{content:""}.fa-mailchimp:before{content:""}.fa-male:before{content:""}.fa-mandalorian:before{content:""}.fa-map:before{content:""}.fa-map-marked:before{content:""}.fa-map-marked-alt:before{content:""}.fa-map-marker:before{content:""}.fa-map-marker-alt:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-markdown:before{content:""}.fa-marker:before{content:""}.fa-mars:before{content:""}.fa-mars-double:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mask:before{content:""}.fa-mastodon:before{content:""}.fa-maxcdn:before{content:""}.fa-medal:before{content:""}.fa-medapps:before{content:""}.fa-medium:before{content:""}.fa-medium-m:before{content:""}.fa-medkit:before{content:""}.fa-medrt:before{content:""}.fa-meetup:before{content:""}.fa-megaport:before{content:""}.fa-meh:before{content:""}.fa-meh-blank:before{content:""}.fa-meh-rolling-eyes:before{content:""}.fa-memory:before{content:""}.fa-mendeley:before{content:""}.fa-menorah:before{content:""}.fa-mercury:before{content:""}.fa-meteor:before{content:""}.fa-microchip:before{content:""}.fa-microphone:before{content:""}.fa-microphone-alt:before{content:""}.fa-microphone-alt-slash:before{content:""}.fa-microphone-slash:before{content:""}.fa-microscope:before{content:""}.fa-microsoft:before{content:""}.fa-minus:before{content:""}.fa-minus-circle:before{content:""}.fa-minus-square:before{content:""}.fa-mitten:before{content:""}.fa-mix:before{content:""}.fa-mixcloud:before{content:""}.fa-mizuni:before{content:""}.fa-mobile:before{content:""}.fa-mobile-alt:before{content:""}.fa-modx:before{content:""}.fa-monero:before{content:""}.fa-money-bill:before{content:""}.fa-money-bill-alt:before{content:""}.fa-money-bill-wave:before{content:""}.fa-money-bill-wave-alt:before{content:""}.fa-money-check:before{content:""}.fa-money-check-alt:before{content:""}.fa-monument:before{content:""}.fa-moon:before{content:""}.fa-mortar-pestle:before{content:""}.fa-mosque:before{content:""}.fa-motorcycle:before{content:""}.fa-mountain:before{content:""}.fa-mouse-pointer:before{content:""}.fa-mug-hot:before{content:""}.fa-music:before{content:""}.fa-napster:before{content:""}.fa-neos:before{content:""}.fa-network-wired:before{content:""}.fa-neuter:before{content:""}.fa-newspaper:before{content:""}.fa-nimblr:before{content:""}.fa-node:before{content:""}.fa-node-js:before{content:""}.fa-not-equal:before{content:""}.fa-notes-medical:before{content:""}.fa-npm:before{content:""}.fa-ns8:before{content:""}.fa-nutritionix:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-oil-can:before{content:""}.fa-old-republic:before{content:""}.fa-om:before{content:""}.fa-opencart:before{content:""}.fa-openid:before{content:""}.fa-opera:before{content:""}.fa-optin-monster:before{content:""}.fa-osi:before{content:""}.fa-otter:before{content:""}.fa-outdent:before{content:""}.fa-page4:before{content:""}.fa-pagelines:before{content:""}.fa-pager:before{content:""}.fa-paint-brush:before{content:""}.fa-paint-roller:before{content:""}.fa-palette:before{content:""}.fa-palfed:before{content:""}.fa-pallet:before{content:""}.fa-paper-plane:before{content:""}.fa-paperclip:before{content:""}.fa-parachute-box:before{content:""}.fa-paragraph:before{content:""}.fa-parking:before{content:""}.fa-passport:before{content:""}.fa-pastafarianism:before{content:""}.fa-paste:before{content:""}.fa-patreon:before{content:""}.fa-pause:before{content:""}.fa-pause-circle:before{content:""}.fa-paw:before{content:""}.fa-paypal:before{content:""}.fa-peace:before{content:""}.fa-pen:before{content:""}.fa-pen-alt:before{content:""}.fa-pen-fancy:before{content:""}.fa-pen-nib:before{content:""}.fa-pen-square:before{content:""}.fa-pencil-alt:before{content:""}.fa-pencil-ruler:before{content:""}.fa-penny-arcade:before{content:""}.fa-people-carry:before{content:""}.fa-pepper-hot:before{content:""}.fa-percent:before{content:""}.fa-percentage:before{content:""}.fa-periscope:before{content:""}.fa-person-booth:before{content:""}.fa-phabricator:before{content:""}.fa-phoenix-framework:before{content:""}.fa-phoenix-squadron:before{content:""}.fa-phone:before{content:""}.fa-phone-alt:before{content:""}.fa-phone-slash:before{content:""}.fa-phone-square:before{content:""}.fa-phone-square-alt:before{content:""}.fa-phone-volume:before{content:""}.fa-photo-video:before{content:""}.fa-php:before{content:""}.fa-pied-piper:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-pied-piper-hat:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-piggy-bank:before{content:""}.fa-pills:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-p:before{content:""}.fa-pinterest-square:before{content:""}.fa-pizza-slice:before{content:""}.fa-place-of-worship:before{content:""}.fa-plane:before{content:""}.fa-plane-arrival:before{content:""}.fa-plane-departure:before{content:""}.fa-play:before{content:""}.fa-play-circle:before{content:""}.fa-playstation:before{content:""}.fa-plug:before{content:""}.fa-plus:before{content:""}.fa-plus-circle:before{content:""}.fa-plus-square:before{content:""}.fa-podcast:before{content:""}.fa-poll:before{content:""}.fa-poll-h:before{content:""}.fa-poo:before{content:""}.fa-poo-storm:before{content:""}.fa-poop:before{content:""}.fa-portrait:before{content:""}.fa-pound-sign:before{content:""}.fa-power-off:before{content:""}.fa-pray:before{content:""}.fa-praying-hands:before{content:""}.fa-prescription:before{content:""}.fa-prescription-bottle:before{content:""}.fa-prescription-bottle-alt:before{content:""}.fa-print:before{content:""}.fa-procedures:before{content:""}.fa-product-hunt:before{content:""}.fa-project-diagram:before{content:""}.fa-pushed:before{content:""}.fa-puzzle-piece:before{content:""}.fa-python:before{content:""}.fa-qq:before{content:""}.fa-qrcode:before{content:""}.fa-question:before{content:""}.fa-question-circle:before{content:""}.fa-quidditch:before{content:""}.fa-quinscape:before{content:""}.fa-quora:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-quran:before{content:""}.fa-r-project:before{content:""}.fa-radiation:before{content:""}.fa-radiation-alt:before{content:""}.fa-rainbow:before{content:""}.fa-random:before{content:""}.fa-raspberry-pi:before{content:""}.fa-ravelry:before{content:""}.fa-react:before{content:""}.fa-reacteurope:before{content:""}.fa-readme:before{content:""}.fa-rebel:before{content:""}.fa-receipt:before{content:""}.fa-recycle:before{content:""}.fa-red-river:before{content:""}.fa-reddit:before{content:""}.fa-reddit-alien:before{content:""}.fa-reddit-square:before{content:""}.fa-redhat:before{content:""}.fa-redo:before{content:""}.fa-redo-alt:before{content:""}.fa-registered:before{content:""}.fa-remove-format:before{content:""}.fa-renren:before{content:""}.fa-reply:before{content:""}.fa-reply-all:before{content:""}.fa-replyd:before{content:""}.fa-republican:before{content:""}.fa-researchgate:before{content:""}.fa-resolving:before{content:""}.fa-restroom:before{content:""}.fa-retweet:before{content:""}.fa-rev:before{content:""}.fa-ribbon:before{content:""}.fa-ring:before{content:""}.fa-road:before{content:""}.fa-robot:before{content:""}.fa-rocket:before{content:""}.fa-rocketchat:before{content:""}.fa-rockrms:before{content:""}.fa-route:before{content:""}.fa-rss:before{content:""}.fa-rss-square:before{content:""}.fa-ruble-sign:before{content:""}.fa-ruler:before{content:""}.fa-ruler-combined:before{content:""}.fa-ruler-horizontal:before{content:""}.fa-ruler-vertical:before{content:""}.fa-running:before{content:""}.fa-rupee-sign:before{content:""}.fa-sad-cry:before{content:""}.fa-sad-tear:before{content:""}.fa-safari:before{content:""}.fa-salesforce:before{content:""}.fa-sass:before{content:""}.fa-satellite:before{content:""}.fa-satellite-dish:before{content:""}.fa-save:before{content:""}.fa-schlix:before{content:""}.fa-school:before{content:""}.fa-screwdriver:before{content:""}.fa-scribd:before{content:""}.fa-scroll:before{content:""}.fa-sd-card:before{content:""}.fa-search:before{content:""}.fa-search-dollar:before{content:""}.fa-search-location:before{content:""}.fa-search-minus:before{content:""}.fa-search-plus:before{content:""}.fa-searchengin:before{content:""}.fa-seedling:before{content:""}.fa-sellcast:before{content:""}.fa-sellsy:before{content:""}.fa-server:before{content:""}.fa-servicestack:before{content:""}.fa-shapes:before{content:""}.fa-share:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-share-square:before{content:""}.fa-shekel-sign:before{content:""}.fa-shield-alt:before{content:""}.fa-ship:before{content:""}.fa-shipping-fast:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-shoe-prints:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-shopping-cart:before{content:""}.fa-shopware:before{content:""}.fa-shower:before{content:""}.fa-shuttle-van:before{content:""}.fa-sign:before{content:""}.fa-sign-in-alt:before{content:""}.fa-sign-language:before{content:""}.fa-sign-out-alt:before{content:""}.fa-signal:before{content:""}.fa-signature:before{content:""}.fa-sim-card:before{content:""}.fa-simplybuilt:before{content:""}.fa-sistrix:before{content:""}.fa-sitemap:before{content:""}.fa-sith:before{content:""}.fa-skating:before{content:""}.fa-sketch:before{content:""}.fa-skiing:before{content:""}.fa-skiing-nordic:before{content:""}.fa-skull:before{content:""}.fa-skull-crossbones:before{content:""}.fa-skyatlas:before{content:""}.fa-skype:before{content:""}.fa-slack:before{content:""}.fa-slack-hash:before{content:""}.fa-slash:before{content:""}.fa-sleigh:before{content:""}.fa-sliders-h:before{content:""}.fa-slideshare:before{content:""}.fa-smile:before{content:""}.fa-smile-beam:before{content:""}.fa-smile-wink:before{content:""}.fa-smog:before{content:""}.fa-smoking:before{content:""}.fa-smoking-ban:before{content:""}.fa-sms:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-snowboarding:before{content:""}.fa-snowflake:before{content:""}.fa-snowman:before{content:""}.fa-snowplow:before{content:""}.fa-socks:before{content:""}.fa-solar-panel:before{content:""}.fa-sort:before{content:""}.fa-sort-alpha-down:before{content:""}.fa-sort-alpha-down-alt:before{content:""}.fa-sort-alpha-up:before{content:""}.fa-sort-alpha-up-alt:before{content:""}.fa-sort-amount-down:before{content:""}.fa-sort-amount-down-alt:before{content:""}.fa-sort-amount-up:before{content:""}.fa-sort-amount-up-alt:before{content:""}.fa-sort-down:before{content:""}.fa-sort-numeric-down:before{content:""}.fa-sort-numeric-down-alt:before{content:""}.fa-sort-numeric-up:before{content:""}.fa-sort-numeric-up-alt:before{content:""}.fa-sort-up:before{content:""}.fa-soundcloud:before{content:""}.fa-sourcetree:before{content:""}.fa-spa:before{content:""}.fa-space-shuttle:before{content:""}.fa-speakap:before{content:""}.fa-speaker-deck:before{content:""}.fa-spell-check:before{content:""}.fa-spider:before{content:""}.fa-spinner:before{content:""}.fa-splotch:before{content:""}.fa-spotify:before{content:""}.fa-spray-can:before{content:""}.fa-square:before{content:""}.fa-square-full:before{content:""}.fa-square-root-alt:before{content:""}.fa-squarespace:before{content:""}.fa-stack-exchange:before{content:""}.fa-stack-overflow:before{content:""}.fa-stackpath:before{content:""}.fa-stamp:before{content:""}.fa-star:before{content:""}.fa-star-and-crescent:before{content:""}.fa-star-half:before{content:""}.fa-star-half-alt:before{content:""}.fa-star-of-david:before{content:""}.fa-star-of-life:before{content:""}.fa-staylinked:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-steam-symbol:before{content:""}.fa-step-backward:before{content:""}.fa-step-forward:before{content:""}.fa-stethoscope:before{content:""}.fa-sticker-mule:before{content:""}.fa-sticky-note:before{content:""}.fa-stop:before{content:""}.fa-stop-circle:before{content:""}.fa-stopwatch:before{content:""}.fa-store:before{content:""}.fa-store-alt:before{content:""}.fa-strava:before{content:""}.fa-stream:before{content:""}.fa-street-view:before{content:""}.fa-strikethrough:before{content:""}.fa-stripe:before{content:""}.fa-stripe-s:before{content:""}.fa-stroopwafel:before{content:""}.fa-studiovinari:before{content:""}.fa-stumbleupon:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-subscript:before{content:""}.fa-subway:before{content:""}.fa-suitcase:before{content:""}.fa-suitcase-rolling:before{content:""}.fa-sun:before{content:""}.fa-superpowers:before{content:""}.fa-superscript:before{content:""}.fa-supple:before{content:""}.fa-surprise:before{content:""}.fa-suse:before{content:""}.fa-swatchbook:before{content:""}.fa-swimmer:before{content:""}.fa-swimming-pool:before{content:""}.fa-symfony:before{content:""}.fa-synagogue:before{content:""}.fa-sync:before{content:""}.fa-sync-alt:before{content:""}.fa-syringe:before{content:""}.fa-table:before{content:""}.fa-table-tennis:before{content:""}.fa-tablet:before{content:""}.fa-tablet-alt:before{content:""}.fa-tablets:before{content:""}.fa-tachometer-alt:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-tape:before{content:""}.fa-tasks:before{content:""}.fa-taxi:before{content:""}.fa-teamspeak:before{content:""}.fa-teeth:before{content:""}.fa-teeth-open:before{content:""}.fa-telegram:before{content:""}.fa-telegram-plane:before{content:""}.fa-temperature-high:before{content:""}.fa-temperature-low:before{content:""}.fa-tencent-weibo:before{content:""}.fa-tenge:before{content:""}.fa-terminal:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-th:before{content:""}.fa-th-large:before{content:""}.fa-th-list:before{content:""}.fa-the-red-yeti:before{content:""}.fa-theater-masks:before{content:""}.fa-themeco:before{content:""}.fa-themeisle:before{content:""}.fa-thermometer:before{content:""}.fa-thermometer-empty:before{content:""}.fa-thermometer-full:before{content:""}.fa-thermometer-half:before{content:""}.fa-thermometer-quarter:before{content:""}.fa-thermometer-three-quarters:before{content:""}.fa-think-peaks:before{content:""}.fa-thumbs-down:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbtack:before{content:""}.fa-ticket-alt:before{content:""}.fa-times:before{content:""}.fa-times-circle:before{content:""}.fa-tint:before{content:""}.fa-tint-slash:before{content:""}.fa-tired:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-toilet:before{content:""}.fa-toilet-paper:before{content:""}.fa-toolbox:before{content:""}.fa-tools:before{content:""}.fa-tooth:before{content:""}.fa-torah:before{content:""}.fa-torii-gate:before{content:""}.fa-tractor:before{content:""}.fa-trade-federation:before{content:""}.fa-trademark:before{content:""}.fa-traffic-light:before{content:""}.fa-train:before{content:""}.fa-tram:before{content:""}.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-trash:before{content:""}.fa-trash-alt:before{content:""}.fa-trash-restore:before{content:""}.fa-trash-restore-alt:before{content:""}.fa-tree:before{content:""}.fa-trello:before{content:""}.fa-tripadvisor:before{content:""}.fa-trophy:before{content:""}.fa-truck:before{content:""}.fa-truck-loading:before{content:""}.fa-truck-monster:before{content:""}.fa-truck-moving:before{content:""}.fa-truck-pickup:before{content:""}.fa-tshirt:before{content:""}.fa-tty:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-tv:before{content:""}.fa-twitch:before{content:""}.fa-twitter:before{content:""}.fa-twitter-square:before{content:""}.fa-typo3:before{content:""}.fa-uber:before{content:""}.fa-ubuntu:before{content:""}.fa-uikit:before{content:""}.fa-umbrella:before{content:""}.fa-umbrella-beach:before{content:""}.fa-underline:before{content:""}.fa-undo:before{content:""}.fa-undo-alt:before{content:""}.fa-uniregistry:before{content:""}.fa-universal-access:before{content:""}.fa-university:before{content:""}.fa-unlink:before{content:""}.fa-unlock:before{content:""}.fa-unlock-alt:before{content:""}.fa-untappd:before{content:""}.fa-upload:before{content:""}.fa-ups:before{content:""}.fa-usb:before{content:""}.fa-user:before{content:""}.fa-user-alt:before{content:""}.fa-user-alt-slash:before{content:""}.fa-user-astronaut:before{content:""}.fa-user-check:before{content:""}.fa-user-circle:before{content:""}.fa-user-clock:before{content:""}.fa-user-cog:before{content:""}.fa-user-edit:before{content:""}.fa-user-friends:before{content:""}.fa-user-graduate:before{content:""}.fa-user-injured:before{content:""}.fa-user-lock:before{content:""}.fa-user-md:before{content:""}.fa-user-minus:before{content:""}.fa-user-ninja:before{content:""}.fa-user-nurse:before{content:""}.fa-user-plus:before{content:""}.fa-user-secret:before{content:""}.fa-user-shield:before{content:""}.fa-user-slash:before{content:""}.fa-user-tag:before{content:""}.fa-user-tie:before{content:""}.fa-user-times:before{content:""}.fa-users:before{content:""}.fa-users-cog:before{content:""}.fa-usps:before{content:""}.fa-ussunnah:before{content:""}.fa-utensil-spoon:before{content:""}.fa-utensils:before{content:""}.fa-vaadin:before{content:""}.fa-vector-square:before{content:""}.fa-venus:before{content:""}.fa-venus-double:before{content:""}.fa-venus-mars:before{content:""}.fa-viacoin:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-vial:before{content:""}.fa-vials:before{content:""}.fa-viber:before{content:""}.fa-video:before{content:""}.fa-video-slash:before{content:""}.fa-vihara:before{content:""}.fa-vimeo:before{content:""}.fa-vimeo-square:before{content:""}.fa-vimeo-v:before{content:""}.fa-vine:before{content:""}.fa-vk:before{content:""}.fa-vnv:before{content:""}.fa-voicemail:before{content:""}.fa-volleyball-ball:before{content:""}.fa-volume-down:before{content:""}.fa-volume-mute:before{content:""}.fa-volume-off:before{content:""}.fa-volume-up:before{content:""}.fa-vote-yea:before{content:""}.fa-vr-cardboard:before{content:""}.fa-vuejs:before{content:""}.fa-walking:before{content:""}.fa-wallet:before{content:""}.fa-warehouse:before{content:""}.fa-water:before{content:""}.fa-wave-square:before{content:""}.fa-waze:before{content:""}.fa-weebly:before{content:""}.fa-weibo:before{content:""}.fa-weight:before{content:""}.fa-weight-hanging:before{content:""}.fa-weixin:before{content:""}.fa-whatsapp:before{content:""}.fa-whatsapp-square:before{content:""}.fa-wheelchair:before{content:""}.fa-whmcs:before{content:""}.fa-wifi:before{content:""}.fa-wikipedia-w:before{content:""}.fa-wind:before{content:""}.fa-window-close:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-windows:before{content:""}.fa-wine-bottle:before{content:""}.fa-wine-glass:before{content:""}.fa-wine-glass-alt:before{content:""}.fa-wix:before{content:""}.fa-wizards-of-the-coast:before{content:""}.fa-wolf-pack-battalion:before{content:""}.fa-won-sign:before{content:""}.fa-wordpress:before{content:""}.fa-wordpress-simple:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpexplorer:before{content:""}.fa-wpforms:before{content:""}.fa-wpressr:before{content:""}.fa-wrench:before{content:""}.fa-x-ray:before{content:""}.fa-xbox:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-y-combinator:before{content:""}.fa-yahoo:before{content:""}.fa-yammer:before{content:""}.fa-yandex:before{content:""}.fa-yandex-international:before{content:""}.fa-yarn:before{content:""}.fa-yelp:before{content:""}.fa-yen-sign:before{content:""}.fa-yin-yang:before{content:""}.fa-yoast:before{content:""}.fa-youtube:before{content:""}.fa-youtube-square:before{content:""}.fa-zhihu:before{content:""}.sr-only{border:0;clip:rect(0, 0, 0, 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}/*! * Font Awesome Free 5.9.0 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) */@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:auto;src:url("./webfonts/fa-solid-900.eot");src:url("./webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"),url("./webfonts/fa-solid-900.woff2") format("woff2"),url("./webfonts/fa-solid-900.woff") format("woff"),url("./webfonts/fa-solid-900.ttf") format("truetype"),url("./webfonts/fa-solid-900.svg#fontawesome") format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900}html{font-size:87.5%;box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}body,h1,h2,h3,h4,h5,h6,p,figure,blockquote,dl,dd{margin:0;padding:0}ul[role=list],ol[role=list]{list-style:none}html:focus-within{scroll-behavior:smooth}body{text-rendering:optimizeSpeed;line-height:1.5;font-family:"Roboto",Arial,Helvetica,sans-serif !important;letter-spacing:-0.025ch}a:not([class]){text-decoration-skip-ink:auto}img,picture{max-width:100%;display:block}input,button,textarea,select{font:inherit}@media(prefers-reduced-motion: reduce){html:focus-within{scroll-behavior:auto}*,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important;scroll-behavior:auto !important}}#main{height:100vh}.content{display:flex;height:100%;overflow:hidden}.tab-content{display:flex;height:100%;width:100%;background-color:#eef3f6;animation:fadein .3s;overflow:auto}input[type=text],input[type=password],input[type=number],textarea{box-sizing:border-box;background:#fff;max-width:100%;font-size:1rem;font-weight:400;border:1px solid #ccc;border-radius:.25rem;padding:.25rem .5rem;outline:rgba(0,0,0,0)}input:focus{border:1px solid #3ba4d7;box-shadow:inset 0 0 5px #ccc}input.stretched{width:90%}input.small{max-width:70%;padding:.1rem}input.searchbar{width:40%}a{cursor:pointer}a[title=Back]{width:max-content;height:max-content;padding:.475rem .75rem;border-radius:50%;transition:100ms}a[title=Back]:hover{background:#eef3f6}table{padding:20px;table-layout:fixed;width:100%;border-collapse:collapse;text-align:center;color:#333;font-size:1.125rem}table th{font-size:1.125rem;color:#000;border-bottom:2px solid #eee}table tr{border-bottom:1px solid #eee}h3{color:#444}hr{margin-left:0;color:#aaa}.grid-2col{display:grid;grid-template-columns:auto auto;gap:1rem;justify-content:start}.grid-2col input[type=checkbox]{margin-top:20px}.error{color:red}.tooltip{color:#333;position:relative;display:inline-block;margin:0 .25rem}.tooltiptext{visibility:hidden;position:absolute;top:100%;left:50%;min-width:250px;margin-left:-120px;z-index:1;color:#ccc;background-color:#333;font-size:.875rem;text-align:center;padding:.25rem;border-radius:.5rem}.tooltip:hover .tooltiptext{visibility:visible;animation:fadein .5s}blockquote{color:#14141b;padding:.75rem 1rem .75rem 2rem;border-radius:.25rem}blockquote.info{position:relative;line-height:1.2;color:rgba(20,20,27,.8);border:1px solid rgba(17,143,204,.8)}blockquote.info::before{font-family:"Font Awesome 5 Free";position:absolute;top:.5rem;left:.5rem;content:"";color:#019dff}@keyframes fadein{from{opacity:0}to{opacity:1}}.fadein{animation:fadein .5s}@keyframes swipe-from-left{from{margin-left:100%}to{margin-left:0}}button{width:max-content;height:max-content;color:#fff;background:#019dff;font-size:1rem;padding:.4rem 1rem;border:0;border-radius:5px;cursor:pointer;box-shadow:inset -3px -3px 0 rgb(0,94.5826771654,154)}button:active{outline:none;box-shadow:inset 3px 3px 0 rgb(0,94.5826771654,154)}button.red{width:max-content;height:max-content;color:#fff;background:#ff3a4a;font-size:1rem;padding:.4rem 1rem;border:0;border-radius:5px;cursor:pointer;box-shadow:inset -3px -3px 0 rgb(211,0,17.1370558376)}button.red:active{outline:none;box-shadow:inset 3px 3px 0 rgb(211,0,17.1370558376)}.media-item{display:flex;margin-top:.5rem;padding:1rem;border:1px solid rgba(20,20,27,.1);border-radius:4px}.media-item__details{flex-basis:40%;display:flex;align-items:start;gap:.5rem}.media-item__details img{width:6rem;object-fit:contain}.media-item__desc{flex-basis:60%}.active-link{background:hsla(0,0%,100%,.1) !important}.nav-menu{background-color:#14141b;box-shadow:0 5px 5px #222;display:flex;flex-direction:column;align-items:center;height:100%;padding:.5rem .25rem;margin-right:0rem}.nav-menu__logo{padding:1.2rem 0;display:flex;align-items:center;gap:.3rem}.nav-menu__logo img{width:1.6rem}.nav-menu__logo h5{line-height:1;color:#fff}.nav-menu__box{padding:2rem .125rem;display:flex;flex-direction:column;gap:.5rem;position:relative}.nav-menu__box .item{margin:0;padding:.675rem .5rem;width:10rem;display:flex;align-items:center;line-height:1;border-radius:.5rem;text-decoration:none;color:#ccc;text-transform:capitalize;transition:0ms}.nav-menu__box .item:hover{background-color:rgba(238,243,246,.15)}.nav-menu__box .item i.sidenav-icon{width:2.5rem;height:1.4rem;display:grid;place-items:center}.nav-menu__box .item.item-selected{color:#9bdaff;background-color:rgba(155,218,255,.15);font-weight:medium}.nav-menu__box button.toggle-nav{display:none;position:absolute;padding:0;top:0;right:-1rem;background:rgb(77.5,186.5157480315,255);width:1.5rem;height:1.5rem;aspect-ratio:1;justify-content:center;align-items:center;border-radius:50%;box-shadow:none}.nav-menu.collapsed .nav-menu__logo .logo-container{display:flex;flex-direction:column;align-items:center;gap:.5rem}.nav-menu.collapsed .nav-menu__logo .logo-container>*:not(img){display:block}.nav-menu.collapsed .nav-menu__logo .nav-menu__logo-text{display:none !important}.nav-menu.collapsed .nav-menu__box .item{padding:.675rem 0;width:2.5rem;justify-content:center;transition:300ms}.nav-menu.collapsed .nav-menu__box .item span,.nav-menu.collapsed .nav-menu__box .item p{display:none !important}.nav-menu.collapsed button i{rotate:180deg}.nav-menu:hover button.toggle-nav{display:flex}.sidebar{width:13rem;background-color:#fff;display:flex;flex-direction:column}.sidebar a{text-decoration:none;text-transform:capitalize;padding:1rem;cursor:pointer;color:#999}.sidebar a:hover{color:#222}.sidebar .selected-sidebar-link{font-weight:bold;color:#222;border-left:5px solid #3ba4d7;animation:expand-left-border .1s}.sidebarquickview>h6{padding:.5rem}.sidebarquickview a{text-decoration:none;text-transform:capitalize;padding:.5rem 1rem;display:block;color:#999}.sidebarquickview a a:hover{color:#222}.sidebarquickview .selected-sidebarquickview-link{font-weight:bold;color:#222;border-left:5px solid #3ba4d7;animation:expand-left-border .1s}.node-panel{width:100%;padding:.5rem;animation:fadein .5s}@keyframes expand-left-border{from{border-left:0}to{border-left:5px solid #3ba4d7}}@media(max-width: 700px){.tab-content{flex-direction:column}.sidebar{width:100% !important;flex-direction:row !important;overflow-x:auto !important;overflow-y:hidden !important;white-space:nowrap !important;border-bottom:1px solid rgba(20,20,27,.1) !important;background:#fff !important;z-index:50 !important;flex-shrink:0 !important;height:auto !important;padding:0 !important}.sidebar a{display:inline-block !important;padding:.8rem 1.2rem !important;border-bottom:3px solid rgba(0,0,0,0) !important;border-left:none !important}.sidebar .selected-sidebar-link{border-left:none !important;border-bottom:3px solid #3ba4d7 !important;animation:none !important}.sidebarquickview>h4,.sidebarquickview>h6{display:none !important}}.posts{height:100%;margin-top:1rem;flex-direction:column;overflow:auto}.posts__heading{display:flex;flex-direction:column;justify-content:space-between}.posts-container{height:100%;padding:1rem;display:grid;grid-template-columns:repeat(auto-fill, minmax(150px, 1fr));gap:2rem;border:1px solid rgba(20,20,27,.1);border-radius:4px;overflow:auto}.posts-container-card{min-height:240px;flex-direction:column;border:1px solid rgba(20,20,27,.5);border-radius:4px;cursor:pointer;text-align:center}.posts-container-card img{flex-basis:90%;object-fit:cover}.posts-container-card p{padding:0 .125rem;flex-basis:10%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.progress-bar{width:100%;height:2rem;position:relative;text-align:center;background-color:#eef3f6;border-radius:20px;overflow:hidden}.progress-bar__status{position:absolute;top:0;left:0;height:100%;color:#14141b;background-color:#019dff}.progress-bar__percent{position:absolute;inset:0;margin:auto;width:fit-content;height:fit-content}.progress-bar-chunks{position:relative;margin-top:.5rem;width:100%;height:2rem;display:flex;border-radius:.25rem;overflow:hidden;background-color:#eef3f6}.progress-bar-chunks .chunk{width:100%}.progress-bar-chunks .chunk[data-chunkVal="0"]{background-color:rgba(155,218,255,.2)}.progress-bar-chunks .chunk[data-chunkVal="1"]{background-color:#ff3a4a}.progress-bar-chunks .chunk[data-chunkVal="2"]{background-color:#019dff}.progress-bar-chunks .chunk[data-chunkVal="3"]{background-color:#fcba03}.progress-bar-chunks__percent{position:absolute;inset:0;margin:auto;width:fit-content;height:fit-content}.widget{height:100%;padding:1rem;display:flex;flex-direction:column;gap:.5rem;background-color:#fff;border-radius:.5rem;overflow:auto}.widget .top-heading{display:flex;justify-content:space-between}.widget__heading{display:flex;justify-content:space-between;align-items:center;border-bottom:2px solid #999}.widget__body{height:100%;display:flex;flex-direction:column;overflow:auto}.widget__body-heading{display:flex;justify-content:space-between;align-items:center}.widget__body-heading .action{display:flex;gap:.5rem}.widget__body-content{height:100%;overflow:auto}.widget__body-box{display:flex;flex-direction:column;gap:.5rem}.widget-half{max-width:50%}#modal-container{display:none;position:fixed;z-index:1;height:100%;top:0;left:0;width:100%;background-color:rgba(0,0,0,.2)}.modal-content{position:absolute;color:#555;width:40%;min-height:10rem;height:max-content;padding:1.5rem;inset:0;margin:auto;background-color:#fff;border-radius:.5rem;animation:fadein .5s;display:flex;flex-direction:column}.modal-content button:last-child{margin-top:auto}.modal-content .close-btn{position:absolute;right:1.5rem}.modal-content .widget{padding:0}#notification-container{position:absolute;bottom:0;right:0}.login-page{background-image:linear-gradient(-45deg, rgba(1, 157, 255, 0.75), rgba(17, 143, 204, 0.75));height:100%;animation:fadein .5s}.login-page .login-container{background-color:#fff;box-shadow:3px 3px 5px rgba(20,20,27,.4);margin:auto;position:relative;top:100px;max-width:400px;max-height:500px;border-radius:5px;display:flex;flex-direction:column;align-items:center}.login-page .login-container input{padding:.375rem .75rem;border-radius:.275rem}.login-page .login-container *{margin-bottom:1rem}.login-page .login-container>img{margin:1rem 0 2rem}.login-page .login-container extra{margin:0}.login-page .login-container>a{text-decoration:underline;cursor:pointer}.login-page .extra>label,.login-page .extra>br,.login-page .extra>input{margin-bottom:0}.homepage{margin:2rem auto 0;display:flex;flex-direction:column;gap:4rem}.homepage .logo{display:flex;justify-content:center;align-items:center}.homepage .logo img{width:90px}.homepage .logo .retroshareText{display:flex;flex-direction:column;align-items:center}.homepage .logo .retroshareText .retrotext{font-size:36px;font-weight:600;line-height:1.125}.homepage .logo .retroshareText .retrotext>span{color:#118fcc}.homepage .logo .retroshareText>b{font-size:14px;line-height:1}.homepage .certificate{display:flex;flex-direction:column;gap:4rem}.homepage .certificate__heading{text-align:center}.homepage .certificate__heading>h1{margin-bottom:1rem}.homepage .certificate__content{display:flex;flex-direction:column;gap:2rem;padding:2rem;text-align:center;border:1.5px solid rgba(17,143,204,.2);border-radius:6px;box-shadow:0px 0px 8px 2px rgba(20,20,27,.05)}.homepage .certificate__content .rsId>p{margin-bottom:.5rem;color:#118fcc}.homepage .certificate__content .retroshareID{padding:.25rem;display:flex;align-items:center;justify-self:start;font-size:1.25rem;border-radius:4px;background:rgba(20,20,27,.05)}.homepage .certificate__content .retroshareID .textArea{padding:0;width:100%;min-height:75px;font-size:1rem;font-family:monospace;background:rgba(0,0,0,0);border:none;resize:none}.homepage .certificate__content .retroshareID i{color:#118fcc}.homepage .certificate__content .retroshareID>i{margin:0 .5rem;cursor:pointer}.homepage .certificate__content .webhelp{padding:.5rem;background:#f5f5f5;display:flex;justify-content:center;align-items:center;gap:.5rem;border-radius:4px;border:1px solid rgba(20,20,27,.5);width:fit-content;cursor:pointer}.homepage .certificate__content .webhelp-container{display:grid;place-items:center}.homepage .certificate__content .webhelp:hover{background:#eef3f6;border:1px solid #14141b}.homepage .certificate__content .webhelp>i{font-size:1.2rem;color:green}.homepage .certificate__content .add-friend>h6,.homepage .certificate__content .webhelp-container>h6{font-weight:normal;margin-bottom:.5rem}.friend{color:#444;font-size:1.2em;margin:1rem .5rem;padding:1.5rem;border:1px solid #aaa;border-radius:20px}.friend i{float:left;padding:0 10px;cursor:pointer}.friend h4{margin-bottom:5px}.friend button{font-size:.9em}.friend.hidden{display:none}.friend .brief-info.online{color:green}.friend .location{margin:5px;border-top:1px solid #bbb;display:grid;grid-template-columns:auto auto;justify-content:start}.friend .brief-info{display:flex;align-items:center;justify-self:start}.friend .fa-times-circle{color:#555}.friend .fa-check-circle{color:green}.identity{color:#444;font-size:1.1em;margin:20px;padding:10px;border:1px solid #aaa;border-radius:20px}.identity>h4{margin:5px;font-size:1.3em}.identity button{font-size:.9em}.identity .details{display:grid;grid-template-columns:140px auto;grid-row-gap:5px;justify-content:left}.defaultAvatar{width:3rem;height:3rem;aspect-ratio:1;background:#b0c4de;border-radius:50%;display:grid;place-items:center}.defaultAvatar p{font-weight:900;color:#666f7f;transform:translateY(1px)}img.avatar{display:block;width:3rem;height:max-content;aspect-ratio:1;margin-right:.3em;border-radius:50%}.counter{margin-left:.5em}.counter:before{content:"("}.counter:after{content:")"}.chatInit{margin-left:.5em;color:green;cursor:pointer}.lobby{margin:10px;border:1px solid #aaa;border-radius:20px}.lobby .mainname{margin:20px;font-weight:100;font-size:1.2em}.topic{color:#666}.lobby>.topic{font-size:.95em;margin-left:25px;margin-bottom:5px}.lefttitle{margin-top:15px;margin-bottom:0;font-weight:100;font-size:1.2em}.leftname{margin-top:5px;margin-bottom:5px;padding:5px;font-weight:100;font-size:1em}.leftlobby>.topic{font-size:.75em;margin-left:15px;margin-bottom:5px}.subscribed,.public{cursor:pointer}.leftlobby{border:1px solid #aaa;border-radius:10px;margin-top:5px;background-color:#fff}.leftlobby.selected-lobby,.selectedidentity{color:#fff;background-color:#3ba4d7}.rightbar{position:absolute;width:185px;background-color:#fff;overflow:auto;top:130px;bottom:15px;right:15px}.user{padding:5px}.lobbyName{padding:15px;margin-top:2rem}.lobbies{position:absolute;width:185px;left:165px;bottom:15px;top:130px;overflow:auto}.messages,.setup{position:absolute;background-color:#fff;top:130px;left:360px;right:215px;overflow:auto}.messages{bottom:115px}.messagetext{white-space:break-spaces;margin-right:5px}.message>*{margin-left:5px}.username{color:#006400;font-weight:bolder}.chatMessage{position:absolute;background-color:#fff;height:85px;bottom:15px;right:215px;left:360px}textarea.chatMsg{height:100%;width:100%}.chatatchar{margin-left:.2em;margin-right:.2em;color:silver}.setupicon{margin-left:1em;cursor:pointer}.leaveicon{margin-left:1em;cursor:pointer;color:#d40000}.selectidentity{margin:15px;font-size:1.2em}.setup>.identity{cursor:pointer}.setup{bottom:15px}.createDistantChat{margin-top:1em}.no-lobbies .messages,.no-lobbies .chatMessage,.no-lobbies .setup{left:165px}@media(min-width: 900px){.node-panel.chat-room{display:grid !important;grid-template-columns:250px 1fr 200px !important;grid-template-rows:auto 1fr auto !important;grid-template-areas:"lobbies header rightbar" "lobbies messages rightbar" "lobbies input rightbar" !important;padding:0 !important;height:100% !important}.node-panel.chat-room .lobbyName{grid-area:header;padding:10px;border-bottom:1px solid #eee;margin:0;z-index:10;background:#fff}.node-panel.chat-room .lobbies{grid-area:lobbies;position:static !important;width:auto !important;height:auto !important;border-right:1px solid #ccc;overflow-y:auto;display:block !important;top:auto !important;bottom:auto !important;left:auto !important}.node-panel.chat-room .messages{grid-area:messages;position:static !important;width:auto !important;height:auto !important;overflow-y:auto;padding:10px;left:auto !important;right:auto !important;top:auto !important;bottom:auto !important;margin:0 !important}.node-panel.chat-room .rightbar{grid-area:rightbar;position:static !important;width:auto !important;border-left:1px solid #ccc;overflow-y:auto;display:block !important}.node-panel.chat-room .chatMessage{grid-area:input;position:static !important;width:auto !important;height:auto !important;border-top:1px solid #eee;left:auto !important;right:auto !important;bottom:auto !important;flex:0 0 auto;padding:10px !important;background:#fff;z-index:10}}@media(max-width: 899px){.node-panel.chat-room{display:flex !important;flex-direction:column !important;height:100% !important;position:relative !important}.node-panel.chat-room .lobbyName{flex:0 0 auto}.node-panel.chat-room .messages{flex:1 !important;overflow-y:auto !important;position:relative !important;top:0 !important;bottom:0 !important;left:0 !important;right:0 !important;width:100% !important;height:auto !important;margin:0 !important}.node-panel.chat-room .chatMessage{flex:0 0 auto !important;position:relative !important;bottom:0 !important;left:0 !important;right:0 !important;width:100% !important;height:auto !important;z-index:100}.node-panel.chat-room .rightbar,.node-panel.chat-room .lobbies{display:none !important;position:fixed !important;top:60px !important;bottom:0 !important;width:80% !important;background:#fff !important;z-index:200 !important;box-shadow:2px 0 10px rgba(0,0,0,.2) !important}.node-panel.chat-room.show-lobbies .lobbies{display:block !important;left:0 !important}.node-panel.chat-room.show-users .rightbar{display:block !important;right:0 !important}.chat-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.4);z-index:150}.show-lobbies .chat-overlay,.show-users .chat-overlay{display:block}.mobile-menu-icons{display:flex;gap:15px;font-size:1.2rem}.mobile-menu-icons i{cursor:pointer;padding:5px}}@media(min-width: 900px){.mobile-menu-icons{display:none}}.side-bar{display:flex;flex-direction:column;background:#fff}.side-bar .mail-compose-btn{width:96%;margin:.25rem;padding:.75rem 0}.compose-mail__from{display:flex;justify-content:space-between;padding-bottom:.5rem;border-bottom:2px solid #eef3f6}.compose-mail__recipients{padding:.5rem 0;display:flex;flex-direction:column;gap:.5rem;border-bottom:2px solid #eef3f6}.compose-mail__recipients__container{display:flex;gap:.5rem}.compose-mail__recipients__container>label{text-transform:capitalize}.compose-mail__recipients__container .recipients{width:100%;display:flex;gap:.5rem;flex-wrap:wrap}.compose-mail__recipients__container .recipients__selected{padding:.125rem .5rem;display:flex;align-items:center;gap:.5rem;border:1px solid #eef3f6;border-radius:3px;cursor:default}.compose-mail__recipients__container .recipients__selected i{cursor:pointer;padding:.25rem}.compose-mail__recipients__container .recipients__input{display:flex;position:relative;flex-grow:1}.compose-mail__recipients__container .recipients__input-field{flex-grow:1;min-width:200px;padding:0;border:none;box-shadow:none}.compose-mail__recipients__container .recipients__input-field:focus+.recipients__input-list{display:flex}.compose-mail__recipients__container .recipients__input-list{z-index:1;position:absolute;top:1rem;padding:0;width:100%;max-height:15rem;flex-direction:column;overflow:auto;display:none;background:#fff;border-top:1px solid #eef3f6;border-bottom:1px solid #eef3f6}.compose-mail__recipients__container .recipients__input-list:hover{display:flex}.compose-mail__recipients__container .recipients__input-list li{list-style:none;padding:.25rem .5rem;cursor:pointer;background:#fff;border:1px solid #eef3f6;border-top:0px}.compose-mail__recipients__container .recipients__input-list li:hover{background:#eef3f6}.compose-mail__recipients__container .recipients__input-list li:last-child{border-bottom:0px}.compose-mail__recipients .remove-recipient{padding:.125rem .5rem}.compose-mail input[type=text].compose-mail__subject{padding:.5rem 0;border:none;box-shadow:none;border-bottom:2px solid #eef3f6;border-radius:0}.compose-mail__message{margin:.5rem 0;height:100%;display:flex;flex-direction:column;overflow:auto}.compose-mail__message-body{height:100%;outline:rgba(0,0,0,0)}.compose-mail__send-btn{display:flex;align-items:center;gap:.5rem}.compose-mail__send-btn i{transform:translateY(-1px)}.msg-view{height:100%;display:flex;flex-direction:column;gap:1rem;overflow:auto}.msg-view-nav{display:flex;justify-content:space-between;align-items:column}.msg-view-nav__action{display:flex;gap:.5rem}.msg-view__header{display:flex;flex-direction:column;gap:1rem}.msg-view__header>h3{line-height:1}.msg-view__header .msg-details{display:flex;gap:1rem}.msg-view__header .msg-details__avatar{height:max-content}.msg-view__header .msg-details__info{display:flex;flex-direction:column}.msg-view__header .msg-details__info-item{display:flex;gap:.5rem}.msg-view__body{height:100%;overflow:auto;font-size:14px !important}.msg-view__attachment{height:50%;overflow:auto;display:flex;flex-direction:column}.msg-view__attachment-items{height:100%;overflow:auto}.mail-tag{width:8rem;padding:.5rem}.msgHeader{display:flex}.msgHeaderDetails{display:flex;flex-direction:column}table.mails th:nth-child(1){width:5%;color:#fcba03}table.mails th:nth-child(2){width:5%;color:hsl(202.5,30.7692307692%,44.9019607843%)}table.mails th:nth-child(3){width:50%;text-align:start}table.mails th:nth-child(4),table.mails th:nth-child(5){width:20%;text-align:start}table.mails td:nth-child(3){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.mails td:nth-child(4),table.mails td:nth-child(5){text-align:start}table.mails tr:hover{background-color:#eef3f6;cursor:pointer}table.mails tr.unread{color:#000;background-color:#eef3f6}table.mails>tr:hover{cursor:auto;background-color:#fff}input.star-check{display:none}input.star-check+label.star-check{color:gray}input.star-check:checked+label.star-check{color:#fcba03}#truncate{height:6rem;overflow:auto}#truncate.truncated-view{height:1.75rem;overflow:hidden}.toggle-truncate{font-size:.75rem;padding:0 .25rem;background:#999;color:#14141b;box-shadow:none;border-radius:2px}table.attachment-container{padding:0}table.attachment-container>tr{border:0}table.attachment-container .attachment-header{width:100%;display:flex;justify-content:space-between}table.attachment-container .attachment-header th{text-align:start}table.attachment-container .attachment-header th:nth-child(1){flex-basis:45%}table.attachment-container .attachment-header th:nth-child(2){flex-basis:15%}table.attachment-container .attachment-header th:nth-child(3){flex-basis:10%}table.attachment-container .attachment-header th:nth-child(4){flex-basis:20%}table.attachment-container .attachment-header th:nth-child(5){text-align:center;flex-basis:10%}table.attachment-container .attachment{width:100%;display:flex;justify-content:space-between;text-align:start}table.attachment-container .attachment__name{flex-basis:45%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}table.attachment-container .attachment__name span{margin-left:8px}table.attachment-container .attachment__from{flex-basis:15%}table.attachment-container .attachment__size{flex-basis:10%}table.attachment-container .attachment__date{flex-basis:20%}table.attachment-container .attachment td:nth-child(5){display:flex;justify-content:center;align-items:center;flex-basis:10%}table.attachment-container .attachment td:nth-child(5) button{font-size:.875rem}.view-toggle{height:max-content;border:1px solid #019dff;border-radius:4px;display:flex}.view-toggle *{padding:4px 12px;border-radius:4px}.composePopupOverlay{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1;background-color:rgba(0,0,0,.2)}.composePopupOverlay .composePopup{position:absolute;inset:0;margin:auto;width:80%;height:90%}.composePopupOverlay .composePopup>.widget{padding:2rem}.composePopupOverlay .composePopup .close-btn{position:absolute;top:1.5rem;right:1.5rem}.file-view{width:100%;padding:1rem;margin-top:1.5rem;border-radius:8px;border:1px solid #ccc;animation:fadein .5s}.file-view__heading{display:flex;justify-content:space-between;margin-bottom:.5rem}.file-view__heading-chunk{display:flex;gap:1rem}.file-view__body{display:flex;flex-direction:column;gap:1rem}.file-view__body-details{display:flex;align-items:center}.file-view__body-details-stat{width:100%;display:grid;grid-template-columns:repeat(5, 1fr)}.file-view__body-details-stat span>i{margin-right:.5rem}.file-view__body-details-action{display:flex;gap:1rem;height:100%}.file-view__body-details-action button,.file-view__body-details-action button.red{padding:.25rem .75rem}table.myfiles td{word-wrap:break-word}table.myfiles th:nth-child(1){width:2%}table.myfiles th:nth-child(2){width:50%}table.myfiles td:nth-child(2){text-align:start}table.friendsfiles td{word-wrap:break-word}table.friendsfiles th:nth-child(1){width:2%}table.friendsfiles th:nth-child(2){width:50%}table.friendsfiles th:nth-child(4){width:40%}table.friendsfiles td:nth-child(2){text-align:start}.file-search-container{margin-top:1rem;padding:8px;display:flex;gap:8px;border:1px solid rgba(20,20,27,.2);border-radius:6px;height:100%;overflow:auto}.file-search-container__keywords{flex-basis:15%;padding-right:.25rem;border-right:1px solid rgba(20,20,27,.1)}.file-search-container__keywords .keywords-container{display:flex;flex-direction:column;border-top:2.5px solid rgba(20,20,27,.08);margin-top:.125rem;padding-top:.25rem}.file-search-container__keywords .keywords-container a{font-size:1.2rem;text-decoration:none;color:#14141b}.file-search-container__keywords .keywords-container a.selected{color:#019dff}.file-search-container__results{flex-basis:85%;height:100%;overflow:auto}.file-search-container__results .results-container .results-header tr{display:flex}.file-search-container__results .results-container .results-header tr th{font-size:1.25rem;font-weight:bold;text-align:left}.file-search-container__results .results-container .results-header tr th:nth-child(1){flex-basis:40%}.file-search-container__results .results-container .results-header tr th:nth-child(2){flex-basis:10%;text-align:center}.file-search-container__results .results-container .results-header tr th:nth-child(3){flex-basis:40%}.file-search-container__results .results-container .results-header tr th:nth-child(4){flex-basis:10%}.file-search-container__results .results-container .results{height:100%;overflow:auto}.file-search-container__results .results-container .results tr{display:flex}.file-search-container__results .results-container .results tr .results__hash,.file-search-container__results .results-container .results tr .results__name{text-align:left;flex-basis:40%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.file-search-container__results .results-container .results tr .results__hash span,.file-search-container__results .results-container .results tr .results__name span{margin-left:8px}.file-search-container__results .results-container .results tr .results__size{flex-basis:10%}.file-search-container__results .results-container .results tr .results__download{flex-basis:10%;display:flex;justify-content:start;align-items:center}.search-form{display:flex;width:40%}.search-form input{width:100%}.search-form button{margin-left:.5rem}.shareManagerPopupOverlay{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1;background-color:rgba(0,0,0,.2)}.shareManagerPopupOverlay .shareManagerPopup{position:absolute;inset:0;margin:auto;width:80%;height:90%}.shareManagerPopupOverlay .shareManagerPopup>.widget{padding:1.5rem}.shareManagerPopupOverlay .shareManagerPopup .close-btn{position:absolute;top:1.5rem;right:1.5rem}.share-manager{display:flex;flex-direction:column;justify-content:space-between}.share-manager__table{margin:1rem 0 auto}.share-manager__table thead{font-weight:bold;text-align:left}.share-manager__table thead td:nth-child(1),.share-manager__table thead td:nth-child(2){padding-left:.5rem}.share-manager__table thead td:nth-child(3) .tooltip,.share-manager__table thead td:nth-child(4) .tooltip{font-weight:normal;font-size:1rem}.share-manager__table tbody{text-align:left}.share-manager__table tbody td:nth-child(4){font-size:1rem}.share-manager__table td input{border:0 !important}.share-manager__table td input[type=text]{width:100%}.share-manager__table td:nth-child(1){width:45%}.share-manager__table td:nth-child(2){width:20%}.share-manager__table td:nth-child(3){width:10%}.share-manager__table td:nth-child(4){width:25%}.share-manager__actions{display:flex;justify-content:space-between}.share-manager__form{display:flex;flex-direction:column;gap:.5rem}.share-manager__form_input{display:flex;flex-direction:column;gap:.5rem}.share-manager__form_input input{flex-grow:1}.share-manager .share-flags input.share-flags-check{display:none}.share-manager .share-flags input.share-flags-check+label.share-flags-label{color:gray;margin-right:.25rem;padding:.25rem .25rem .125rem;border:1px solid #6d6d6d;border-radius:.5rem}.share-manager .share-flags input.share-flags-check:checked+label.share-flags-label{color:#118fcc}.share-manager label span{display:inline-block;width:1.125rem}.manage-visibility label{width:100%;cursor:pointer}.manage-visibility{display:flex;justify-content:space-between}@media(max-width: 700px){.file-view__body-details{flex-direction:column;align-items:flex-start;gap:1rem}.file-view__body-details-stat{grid-template-columns:1fr;gap:.5rem}.file-view__body-details-stat span{display:flex;align-items:center}.share-manager__table,.share-manager__table thead,.share-manager__table tbody,.share-manager__table tr,.share-manager__table td{display:block;width:100% !important}.share-manager__table thead{display:none}.share-manager__table tr{border:1px solid #ccc;border-radius:8px;margin-bottom:1rem;padding:.5rem;background:#fff}.share-manager__table td{margin-bottom:.5rem;border:none !important;padding-left:0 !important}table.myfiles,table.myfiles tr,table.myfiles td,table.friendsfiles,table.friendsfiles tr,table.friendsfiles td{display:block;width:100% !important}table.myfiles th,table.friendsfiles th{display:none}table.myfiles tr,table.friendsfiles tr{border:1px solid #ccc;border-radius:8px;margin-bottom:1rem;padding:.5rem;background:#fff}.file-search-container{flex-direction:column}.file-search-container__keywords{flex-basis:auto;width:100%;border-right:none;border-bottom:1px solid rgba(20,20,27,.1);padding-bottom:1rem;margin-bottom:1rem}.results-container,.results-container thead,.results-container tbody,.results-container tr,.results-container td{display:block;width:100% !important}.results-container thead{display:none}.results-container tr{border-bottom:1px solid #eee;padding:1rem 0}.results-container td{margin-bottom:.5rem;word-break:break-all}}.file-section{margin-top:2rem;display:flex;flex-direction:column}.comments-section{margin-top:2rem;display:flex;justify-content:space-between}.comments-section__menu{display:flex;gap:1rem}.comments-section__menu-id{display:flex;align-items:center;gap:.25rem}#toggleunsub{position:relative;background:gray}table.channels th:nth-child(1){width:50%;text-align:start}table.channels td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.channels tr:hover{background-color:#eef3f6;cursor:pointer}table.channels tr.hidden{display:none}table{padding:.5rem}table.comments{border:1px solid #eee}table.comments th{height:40px}table.comments th:nth-child(1){width:2%}table.comments th:nth-child(2){width:40%}table.comments td{word-wrap:break-word}table.comments td:nth-child(2){text-align:start}table.files th:first-child{text-align:start;width:60%}table.files tr td:first-child{text-align:start}table.files td{word-wrap:break-word}#mtags{width:160px;text-align:center;font-size:medium;margin-left:10px;height:40px}.forums-node-panel{position:relative;bottom:200px;margin-left:200px;animation:fadein .5s}table.forums th:nth-child(1){width:50%;text-align:start}table.forums td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.forums tr:hover{background-color:#eef3f6;cursor:pointer}table.forums tr.hidden{display:none}#searchforum{position:relative;margin-left:250px}#forumdetails{position:relative;padding:10px}.p{margin:0}#toggleunsub{position:relative;background:gray}table.threads tr:hover{background-color:#eef3f6;cursor:pointer}table.threads td{word-wrap:break-word}table.threadreply th:nth-child(2){width:50%}table.threadreply th:nth-child(1){width:2%}table.threadreply td:nth-child(2){width:50%;text-align:start}table.threadreply td{word-wrap:break-word}table.threadreply tr:hover{background-color:#eef3f6;cursor:pointer}table.boards th:nth-child(1){width:50%;text-align:start}table.boards td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.boards tr:hover{background-color:#eef3f6;cursor:pointer}table.boards tr.hidden{display:none}#toggleunsub{position:relative;background:gray}#options{width:100px;text-align:center;font-size:medium;margin-left:20px;height:40px}#composepopup{height:80%;width:70%}#mtags{width:160px;text-align:center;font-size:medium;margin-left:10px;height:40px}.mail .permission-flag{margin-bottom:1rem;display:flex;gap:1rem}.mail-tags{padding:.5rem;border:1px solid rgba(20,20,27,.2);border-radius:6px}.mail-tags__container{display:flex;flex-direction:column}.mail-tags__container .tag-item{display:flex;align-items:center;gap:4px;border-bottom:1px solid rgba(20,20,27,.1);padding:2px 0}.mail-tags__container .tag-item:last-child{border:none}.mail-tags__container .tag-item__color{width:1.25rem;height:1.25rem;aspect-ratio:1}.mail-tags__container .tag-item__name{font-size:1.125rem}.mail-tags__container .tag-item__modify{margin-left:auto;font-size:.75rem;display:flex;gap:4px}.mail-tags__container .tag-item:hover{background-color:#eef3f6}.mail-tags__container .tag-item button,.mail-tags__container .tag-item button.red{padding:.25rem .6rem}.mail-tags-form .input-field{margin-bottom:.5rem}.mail-tags-form .input-field label{margin-right:.5rem}.external-address{margin:0;padding-left:1rem;height:100px;overflow:hidden auto}.external-address::-webkit-scrollbar{display:none}.proxy-server{display:flex;flex-direction:column;gap:4px}.proxy-server__tor>h4,.proxy-server__i2p>h4{margin-bottom:.25rem}.proxy-server__tor>input,.proxy-server__i2p>input{margin-right:.5rem}.proxy-server__tor .proxy-outgoing,.proxy-server__i2p .proxy-outgoing{display:inline-flex;align-items:center;gap:.5rem}.proxy-server__tor .proxy-outgoing__status,.proxy-server__i2p .proxy-outgoing__status{width:1rem;height:1rem;aspect-ratio:1;border:1px solid #000;border-radius:50%}.config-files{display:flex;flex-direction:column;gap:1rem} + +/* Custom improvements for Network Page */ + +.network-container { + display: flex; + height: 100%; + width: 100%; + overflow: hidden; + background-color: #f1f5f9; +} + +.network-left-pane { + width: 320px; + min-width: 300px; + max-width: 350px; + border-right: 1px solid #cbd5e1; + display: flex; + flex-direction: column; + background: #ffffff; + box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05); +} + +.own-profile-card { + padding: 1.25rem; + border-bottom: 1px solid #e2e8f0; + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.own-profile-card .profile-header { + display: flex; + align-items: center; + gap: 1rem; +} + +.own-profile-card .profile-info { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; +} + +.own-profile-card .profile-info .profile-name { + font-weight: 700; + color: #1e293b; + font-size: 1.1rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.own-profile-card .profile-info .profile-status { + font-size: 0.85rem; + color: #10b981; + font-weight: 500; + display: flex; + align-items: center; + gap: 0.35rem; +} + +.own-profile-card .profile-info .profile-status::before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + background-color: #10b981; + border-radius: 50%; +} + +.own-profile-card .own-identity-select-container { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.own-profile-card .own-identity-select-container label { + font-size: 0.75rem; + color: #64748b; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.own-profile-card .own-identity-select-container select.own-identity-select { + width: 100%; + padding: 0.375rem 0.5rem; + font-size: 0.85rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + background-color: #ffffff; + color: #334155; + outline: none; + cursor: pointer; + transition: border-color 0.2s; +} + +.own-profile-card .own-identity-select-container select.own-identity-select:focus { + border-color: #3ba4d7; +} + +.friends-list-container { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.friends-list-container .searchbar-container { + padding: 0.75rem 1rem; + border-bottom: 1px solid #e2e8f0; +} + +.friends-list-container .searchbar-container input.searchbar { + width: 100%; + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + background-color: #f8fafc; + outline: none; + transition: all 0.2s; +} + +.friends-list-container .searchbar-container input.searchbar:focus { + background-color: #ffffff; + border-color: #3ba4d7; + box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); +} + +.friends-list-container .friends-scroll { + flex: 1; + overflow-y: auto; + padding: 0.5rem 0; +} + +.friend-list-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + margin: 0.125rem 0.5rem; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s; +} + +.friend-list-item:hover { + background-color: #f1f5f9; +} + +.friend-list-item.selected { + background-color: #e0f2fe; +} + +.friend-list-item.selected .friend-meta .friend-name { + color: #0369a1; + font-weight: 600; +} + +.friend-list-item .friend-avatar { + flex-shrink: 0; +} + +.friend-list-item .friend-meta { + flex: 1; + min-width: 0; +} + +.friend-list-item .friend-meta .friend-name { + font-size: 0.95rem; + color: #334155; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: color 0.2s; +} + +.friend-list-item .friend-meta .friend-status { + font-size: 0.8rem; + color: #94a3b8; +} + +.friend-list-item .friend-meta .friend-status.online { + color: #10b981; + font-weight: 500; +} + +.network-right-pane { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: #f8fafc; +} + +.network-pane-placeholder { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: #94a3b8; + gap: 1rem; + padding: 2rem; + text-align: center; +} + +.network-pane-placeholder i { + font-size: 4rem; + color: #cbd5e1; +} + +.network-pane-placeholder p { + font-size: 1.1rem; + max-width: 400px; +} + +.network-tabs { + display: flex; + background-color: #ffffff; + border-bottom: 1px solid #cbd5e1; + padding: 0.5rem 1rem 0; + gap: 0.5rem; +} + +.network-tabs .tab-btn { + padding: 0.625rem 1.25rem; + font-size: 0.95rem; + font-weight: 600; + color: #64748b; + background: transparent; + border: none; + border-radius: 0.375rem 0.375rem 0 0; + border-bottom: 3px solid transparent; + cursor: pointer; + box-shadow: none; + transition: all 0.2s; +} + +.network-tabs .tab-btn:hover { + color: #334155; + background-color: #f1f5f9; +} + +.network-tabs .tab-btn.active { + color: #3ba4d7; + border-bottom-color: #3ba4d7; + background-color: transparent; +} + +.network-tab-content { + flex: 1; + overflow-y: auto; + padding: 1.5rem; +} + +.network-detail-view { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.network-detail-view .detail-header { + display: flex; + align-items: center; + gap: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid #e2e8f0; +} + +.network-detail-view .detail-header .detail-title { + flex: 1; +} + +.network-detail-view .detail-header .detail-title h2 { + font-size: 1.75rem; + font-weight: 800; + color: #1e293b; + margin-bottom: 0.25rem; +} + +.network-detail-view .detail-header .detail-title .detail-subtitle { + font-size: 0.9rem; + color: #64748b; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.network-detail-view .detail-header .detail-actions { + display: flex; + gap: 0.75rem; +} + +.network-detail-view .detail-header .detail-actions button { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + font-size: 0.9rem; +} + +.network-detail-view .detail-section { + background-color: #ffffff; + border-radius: 0.5rem; + border: 1px solid #e2e8f0; + padding: 1.25rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); +} + +.network-detail-view .detail-section h3 { + font-size: 1.1rem; + font-weight: 700; + color: #334155; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid #f1f5f9; +} + +.network-detail-view .detail-section .info-grid { + display: grid; + grid-template-columns: 120px 1fr; + row-gap: 0.75rem; + font-size: 0.9rem; +} + +.network-detail-view .detail-section .info-grid .info-label { + font-weight: 600; + color: #64748b; +} + +.network-detail-view .detail-section .info-grid .info-value { + color: #1e293b; + word-break: break-all; +} + +.network-detail-view .locations-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; +} + +.location-card { + background-color: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 0.5rem; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); +} + +.location-card .loc-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #f1f5f9; + padding-bottom: 0.5rem; + margin-bottom: 0.25rem; +} + +.location-card .loc-header .loc-name { + font-weight: 700; + color: #334155; + font-size: 0.95rem; +} + +.location-card .loc-header .loc-status { + font-size: 0.75rem; + font-weight: 600; + padding: 0.125rem 0.5rem; + border-radius: 0.25rem; +} + +.location-card .loc-header .loc-status.online { + background-color: #d1fae5; + color: #065f46; +} + +.location-card .loc-header .loc-status.offline { + background-color: #f1f5f9; + color: #475569; +} + +.location-card .loc-body { + font-size: 0.85rem; + display: grid; + grid-template-columns: 80px 1fr; + row-gap: 0.25rem; +} + +.location-card .loc-body .loc-label { + color: #64748b; +} + +.location-card .loc-body .loc-val { + color: #334155; + word-break: break-all; +} + +.location-card .loc-footer { + margin-top: 0.5rem; + display: flex; + justify-content: flex-end; +} + +.location-card .loc-footer button { + font-size: 0.8rem; + padding: 0.25rem 0.75rem; +} + +.network-chat-view { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + background-color: #f8fafc; +} + +.network-chat-view .chat-messages { + flex: 1; + overflow-y: auto; + padding: 1.25rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.chat-bubble-container { + display: flex; + flex-direction: column; + max-width: 70%; +} + +.chat-bubble-container.outgoing { + align-self: flex-end; + align-items: flex-end; +} + +.chat-bubble-container.outgoing .chat-bubble { + background-color: #3ba4d7; + color: #ffffff; + border-bottom-right-radius: 0.125rem; +} + +.chat-bubble-container.incoming { + align-self: flex-start; + align-items: flex-start; +} + +.chat-bubble-container.incoming .chat-bubble { + background-color: #ffffff; + color: #1e293b; + border: 1px solid #e2e8f0; + border-bottom-left-radius: 0.125rem; +} + +.chat-bubble-container .chat-sender { + font-size: 0.75rem; + color: #64748b; + margin-bottom: 0.25rem; + padding: 0 0.25rem; +} + +.chat-bubble-container .chat-bubble { + padding: 0.625rem 0.875rem; + border-radius: 0.75rem; + font-size: 0.925rem; + line-height: 1.4; + white-space: break-spaces; + word-break: break-word; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.chat-bubble-container .chat-time { + font-size: 0.7rem; + color: #94a3b8; + margin-top: 0.25rem; + padding: 0 0.25rem; +} + +.network-chat-view .chat-input-area { + padding: 1rem; + background-color: #ffffff; + border-top: 1px solid #cbd5e1; + display: flex; + gap: 0.75rem; + align-items: center; +} + +.network-chat-view .chat-input-area textarea.chat-textarea { + flex: 1; + resize: none; + height: 40px; + padding: 0.5rem 0.75rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + font-size: 0.9rem; + outline: none; + transition: all 0.2s; +} + +.network-chat-view .chat-input-area textarea.chat-textarea:focus { + border-color: #3ba4d7; + box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); +} + +.network-chat-view .chat-input-area button.send-btn { + padding: 0.5rem 1.25rem; + font-size: 0.9rem; + height: 40px; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.network-chat-view .chat-warning { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: #64748b; + text-align: center; + padding: 2rem; + gap: 1rem; +} + +.network-chat-view .chat-warning i { + font-size: 3rem; + color: #cbd5e1; +} + +.network-chat-view .chat-warning h4 { + font-weight: 700; + color: #334155; +} + +.network-chat-view .chat-warning p { + max-width: 350px; + font-size: 0.9rem; +} From 320886a10d9caa81148f8d4bb2177a638ab88867 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sun, 5 Jul 2026 21:34:50 +0200 Subject: [PATCH 02/40] Improved People page --- webui-src/app/people/people.js | 672 +++++++++++++++++++++++- webui-src/app/people/people_ownids.js | 4 + webui-src/app/people/people_resolver.js | 22 +- webui-src/styles.css | 73 +++ 4 files changed, 740 insertions(+), 31 deletions(-) diff --git a/webui-src/app/people/people.js b/webui-src/app/people/people.js index 4c64942..d270465 100644 --- a/webui-src/app/people/people.js +++ b/webui-src/app/people/people.js @@ -1,25 +1,673 @@ const m = require('mithril'); const rs = require('rswebui'); - +const widget = require('widgets'); +const Data = require('network/network_data'); const peopleUtil = require('people/people_util'); +const compose = require('mail/mail_compose'); +const ownIdsLayout = require('people/people_ownids'); +const { CreateIdentity, EditIdentity, DeleteIdentity } = ownIdsLayout; -const AllContacts = () => { - const list = peopleUtil.sortUsers(rs.userList.users); +// State variables for People Page +const State = { + searchString: '', + selectedId: null, // GXS ID of the selected identity + activeFilter: 'all', // 'all' | 'contacts' | 'own' + gxsIdToDetailsMap: {}, + ownGxsIds: [], + gpgToGxsIdMap: {}, + showMailCompose: false, + activeTab: 'details', + selectedOwnGxsIdForChat: '', + chatPid: null, + chatMessages: [], + chatInputMsg: '', +}; + +// Build map GPG ID -> GXS ID for all known identities +function loadGxsIdentities() { + rs.rsJsonApiRequest('/rsIdentity/getIdentitiesSummaries', {}, (data) => { + if (data && data.ids) { + data.ids.forEach((user) => { + const gxsId = user.mGroupId; + 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 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(); + }); + }); +} + +// Helpers +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) { + 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 initializeDistantChat() { + if (!State.selectedId || !State.selectedOwnGxsIdForChat) return; + + State.chatPid = null; + State.chatMessages = []; + 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); + loadChatMessages(); + } + } + ); +} + +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(); + // Scroll to bottom + 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 = ''; + + // Optimistic echo + 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'); + } + } + ); +} + +const DetailsTab = () => { return { view: () => { - return m('.widget', [ - m('.widget__heading', [ - m('h3', 'Contacts', m('span.counter', list.length)), - m(peopleUtil.SearchBar), + 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('.friend-avatar', m(peopleUtil.UserAvatar, { avatar: getSafeAvatar(details), firstLetter: (name || '?').slice(0, 1).toUpperCase() })), + 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('.widget__body', [list.map((id) => m(peopleUtil.regularcontactInfo, { id }))]), ]); }, }; }; -module.exports = { - view: () => { - return m(AllContacts); - }, +const ChatTab = () => { + return { + view: () => { + 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.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...'), + ]); + } + + 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('span', { style: 'color: #64748b; font-weight: 500;' }, 'Distant Chat Tunnel'), + 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)))), + ]), + ]), + + // Messages area + 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) => { + 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()), + ]); + }), + ]), + + // Input area + m('.chat-input-area', [ + m('textarea.chat-textarea[placeholder=Type your encrypted message here...]', { + value: State.chatInputMsg, + oninput: (e) => { + State.chatInputMsg = e.target.value; + }, + onkeydown: (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendDistantChatMessage(); + } + }, + }), + m( + 'button.send-btn.blue', + { + onclick: () => sendDistantChatMessage(), + }, + [m('i.fas.fa-paper-plane'), ' Send'] + ), + ]), + ]); + }, + }; }; + +const PeopleLayout = () => { + return { + oninit: (vnode) => { + syncFilter(vnode.attrs.tab); + Data.refreshGpgDetails().then(() => m.redraw()); + loadGxsIdentities(); + loadOwnGxsIds(); + + // Register for chatEvents to receive live incoming messages + rs.events[15].notify = (chatMessage) => { + const msgCid = chatMessage.chat_id; + if (msgCid && msgCid.type === 2 && State.chatPid) { + const msgPid = rs.idToHex(msgCid.distant_chat_id); + if (msgPid === State.chatPid) { + const isNearDuplicate = State.chatMessages.some( + (m) => (m.msg || m.message) === chatMessage.msg && Math.abs(m.sendTime - chatMessage.sendTime) < 5 + ); + if (!isNearDuplicate) { + State.chatMessages.push(chatMessage); + State.chatMessages.sort((a, b) => a.sendTime - b.sendTime); + m.redraw(); + setTimeout(() => { + const element = document.querySelector('.chat-messages'); + if (element) element.scrollTop = element.scrollHeight; + }, 100); + } + } + } + }; + }, + onremove: () => { + // Clean up notify callback when page is left + if (rs.events[15]) { + rs.events[15].notify = () => {}; + } + }, + onupdate: (vnode) => { + syncFilter(vnode.attrs.tab); + }, + 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); + }); + + // 3. Selected details details info + const details = State.selectedId ? State.gxsIdToDetailsMap[State.selectedId] : null; + const name = details ? details.mNickname || details.mGroupName || 'Unknown' : ''; + + return m('.people-container', [ + // Left Side Panel + m('.people-left-pane', [ + // Filter Tabs Group + m('.people-filter-group', [ + m( + 'button.filter-btn' + (State.activeFilter === 'all' ? '.active' : ''), + { + onclick: () => { + m.route.set('/people/All'); + }, + }, + 'All' + ), + 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' + ), + ]), + + // 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'; + } + + 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: () => { + const idChanged = State.selectedId !== gxsId; + State.selectedId = gxsId; + if (idChanged) { + State.chatPid = null; + State.chatMessages = []; + if (State.activeTab === 'chat') { + initializeDistantChat(); + } + } + }, + }, + [ + m('.friend-avatar', m(peopleUtil.UserAvatar, { avatar: itemAvatar, firstLetter: itemFirstLetter })), + m('.friend-meta', [ + m('.friend-name', displayName), + m( + '.friend-status', + itemIsOwn + ? 'My Identity' + : itemIsContact + ? 'Contact' + : 'Identity' + ), + ]), + ] + ); + }), + ]), + ]), + ]), + + // Right Side Details / Actions Pane + m('.people-right-pane', [ + State.selectedId && details + ? [ + m('.network-tabs', [ + m( + 'button.tab-btn' + (State.activeTab === 'details' ? '.active' : ''), + { + onclick: () => { + State.activeTab = 'details'; + }, + }, + 'Profile Details' + ), + m( + 'button.tab-btn' + (State.activeTab === 'chat' ? '.active' : ''), + { + onclick: () => { + State.activeTab = 'chat'; + initializeDistantChat(); + }, + }, + 'Chat Conversation' + ), + ]), + m('.network-tab-content', [ + State.activeTab === 'details' ? m(DetailsTab) : m(ChatTab), + ]), + ] + : m('.network-pane-placeholder', [ + m('i.fas.fa-users'), + m('p', 'Select an identity from the left panel to view profile details or perform actions.'), + ]), + ]), + + // Mail composer overlay popup + State.showMailCompose && + State.selectedId && + m( + '.composePopupOverlay#mailComposerPopup', + { style: { display: 'block' } }, + m( + '.composePopup', + m(compose, { + msgType: 'compose', + toId: State.selectedId, + friendName: name, + isDirectMail: false, + setShowCompose: (val) => { + State.showMailCompose = val; + }, + }), + m( + 'button.red.close-btn', + { + onclick: () => { + State.showMailCompose = false; + }, + }, + m('i.fas.fa-times') + ) + ) + ), + ]); + }, + }; +}; + +module.exports = PeopleLayout; diff --git a/webui-src/app/people/people_ownids.js b/webui-src/app/people/people_ownids.js index ae02731..cb8590f 100644 --- a/webui-src/app/people/people_ownids.js +++ b/webui-src/app/people/people_ownids.js @@ -360,4 +360,8 @@ const Layout = () => { }; }; +Layout.CreateIdentity = CreateIdentity; +Layout.EditIdentity = EditIdentity; +Layout.DeleteIdentity = DeleteIdentity; + module.exports = Layout; diff --git a/webui-src/app/people/people_resolver.js b/webui-src/app/people/people_resolver.js index 07f5efc..7a583f1 100644 --- a/webui-src/app/people/people_resolver.js +++ b/webui-src/app/people/people_resolver.js @@ -1,25 +1,9 @@ const m = require('mithril'); -const widget = require('widgets'); - -const sections = { - OwnIdentity: require('people/people_ownids'), - MyContacts: require('people/people_own_contacts'), - All: require('people/people'), -}; - -const Layout = { - view: (vnode) => [ - m(widget.Sidebar, { - tabs: Object.keys(sections), - baseRoute: '/people/', - }), - m('.node-panel .', vnode.children), - ], -}; +const PeopleLayout = require('people/people'); module.exports = { view: (vnode) => { - const tab = vnode.attrs.tab; - return m(Layout, m(sections[tab])); + const tab = vnode.attrs.tab || 'All'; + return m(PeopleLayout, { tab }); }, }; diff --git a/webui-src/styles.css b/webui-src/styles.css index a77548a..d7f2c37 100644 --- a/webui-src/styles.css +++ b/webui-src/styles.css @@ -550,3 +550,76 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem max-width: 350px; font-size: 0.9rem; } + +/* People Page Modern Split-Pane Layout */ +.people-container { + display: flex; + height: calc(100vh - 55px); + width: 100%; + overflow: hidden; +} + +.people-left-pane { + width: 320px; + border-right: 1px solid #cbd5e1; + display: flex; + flex-direction: column; + background-color: #ffffff; + overflow: hidden; +} + +.people-right-pane { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: #f8fafc; +} + +.people-filter-group { + display: flex; + padding: 0.75rem 1rem 0.25rem 1rem; + gap: 0.25rem; + border-bottom: 1px solid #e2e8f0; +} + +.people-filter-group button.filter-btn { + flex: 1; + padding: 0.375rem 0.5rem; + font-size: 0.85rem; + font-weight: 600; + color: #64748b; + background-color: #f1f5f9; + border: none; + border-radius: 0.375rem; + cursor: pointer; + box-shadow: none; + transition: all 0.2s; +} + +.people-filter-group button.filter-btn:hover { + background-color: #e2e8f0; + color: #334155; +} + +.people-filter-group button.filter-btn.active { + background-color: #3ba4d7; + color: #ffffff; +} + +.people-left-pane .create-id-container { + padding: 0.75rem 1rem; + border-bottom: 1px solid #e2e8f0; + display: flex; +} + +.people-left-pane .create-id-container button.create-id-btn { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.5rem; + font-weight: 600; + font-size: 0.9rem; +} From 4ddbf1ee2a4d567cd297a405cb3b0ef994a8914f Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sun, 5 Jul 2026 23:49:52 +0200 Subject: [PATCH 03/40] Redesign Chat rooms --- webui-src/app/chat/chat.js | 621 +++++++++--- webui-src/app/scss/pages/_chat.scss | 1413 ++++++++++++++++++++------- webui-src/styles.css | 722 ++++++++++++++ 3 files changed, 2262 insertions(+), 494 deletions(-) diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index 0069b84..987dbe6 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -192,7 +192,7 @@ const Message = () => { .replaceAll('
', '\n') .replace(new RegExp('|<[^>]*>', 'gm'), ''); return m( - '.message', + '.message' + (msg.incoming ? '.incoming' : '.outgoing'), m('span.datetime', datetime), m('span.username', username), m('span.messagetext', text) @@ -395,20 +395,23 @@ const ChatLobbyModel = { } }; - // Lookup for chat-user names (Only for lobbies for now) // Lookup for chat-user names if (detail.gxs_ids) { - let names = []; + let list = []; if (Array.isArray(detail.gxs_ids)) { - names = detail.gxs_ids.reduce((a, u) => a.concat(rs.userList.username(u.key)), []); + list = detail.gxs_ids.map((u) => { + const key = u.key; + return { key, name: rs.userList.username(key) }; + }); } else if (typeof detail.gxs_ids === 'object') { - names = Object.keys(detail.gxs_ids).map(key => rs.userList.username(key)); + list = Object.keys(detail.gxs_ids).map((key) => { + return { key, name: rs.userList.username(key) }; + }); } - names.sort((a, b) => a.localeCompare(b)); - this.users = []; - names.forEach((name) => (this.users = this.users.concat([m('.user', name)]))); + list.sort((a, b) => a.name.localeCompare(b.name)); + this.users = list; } else { - this.users = [m('.user', detail.lobby_name)]; + this.users = [{ key: detail.gxs_id || '', name: detail.lobby_name }]; } m.redraw(); }; @@ -484,6 +487,38 @@ const ChatLobbyModel = { }, }; +// ************************* Chat Hub State **************************** + +function getSafeAvatar(details) { + if ( + details && + details.mAvatar && + details.mAvatar.mData && + details.mAvatar.mData.base64 !== '' + ) { + return details.mAvatar; + } + return undefined; +} + +const ChatHubState = { + selectedRoomId: null, + selectedRoom: null, + selectedRoomType: null, + searchString: '', + ownProfile: { name: 'Loading...' }, + gxsDetails: {}, +}; + +function loadOwnChatProfile() { + rs.rsJsonApiRequest('/rsConfig/getConfigNetStatus', {}, (data) => { + if (data && data.status) { + ChatHubState.ownProfile.name = data.status.ownName || 'Unknown'; + m.redraw(); + } + }); +} + // ************************* views **************************** const Lobby = () => { @@ -523,20 +558,6 @@ const LobbyList = { }, }; -const SubscribedLeftLobbies = { - view() { - return [ - m('h5.lefttitle', 'subscribed:'), - m(LobbyList, { - rooms: sortLobbies(Object.values(ChatRoomsModel.subscribedRooms)), - tagname: '.leftlobby.subscribed', - lobbytagname: 'leftname', - onclick: ChatLobbyModel.switchToEvent, - }), - ]; - }, -}; - const SubscribedLobbies = { view() { return m('.widget', [ @@ -552,22 +573,6 @@ const SubscribedLobbies = { }, }; -const PublicLeftLobbies = { - view() { - return [ - m('h5.lefttitle', 'public:'), - m(LobbyList, { - rooms: Object.values(ChatRoomsModel.allRooms || {}).filter( - (info) => !ChatRoomsModel.subscribed(info) - ), - tagname: '.leftlobby.public', - lobbytagname: 'leftname', - onclick: ChatLobbyModel.setupEvent, - }), - ]; - }, -}; - const PublicLobbies = { view() { return m('.widget', [ @@ -583,78 +588,438 @@ const PublicLobbies = { }, }; -const LobbyName = () => { - return m( - 'h3.lobbyName', - m('.mobile-menu-icons', [ - m('i.fas.fa-bars', { onclick: () => MobileState.toggleLobbies() }), - ]), - ChatLobbyModel.isSubscribed - ? [m('span.chatusername', ChatLobbyModel.lobby_user), m('span.chatatchar', '@')] - : [], - ChatLobbyModel.currentLobby.chatType === 2 - ? m('i.fas.fa-circle', { - style: { - color: - ChatLobbyModel.currentLobby.status === 2 - ? '#2ecc71' // Green (Can Talk) - : ChatLobbyModel.currentLobby.status === 1 - ? '#f39c12' // Orange (Tunnel Down) - : ChatLobbyModel.currentLobby.status === 3 - ? '#e74c3c' // Red (Remotely Closed) - : '#95a5a6', // Grey (Unknown) - fontSize: '0.6em', - marginRight: '10px', - verticalAlign: 'middle', - }, - title: - ChatLobbyModel.currentLobby.status === 2 - ? 'Tunnel Active (Can Talk)' - : ChatLobbyModel.currentLobby.status === 1 - ? 'Tunnel Down (Negotiating...)' - : ChatLobbyModel.currentLobby.status === 3 - ? 'Remotely Closed' - : 'Status Unknown', - }) - : [], - m('span.chatlobbyname', ChatLobbyModel.currentLobby.lobby_name), - m('.mobile-menu-icons', [ - m('i.fas.fa-users', { onclick: () => MobileState.toggleUsers() }), - ]), - m.route.param('subaction') !== 'setup' && ChatLobbyModel.currentLobby.chatType === 3 - ? [ - m('i.fas.fa-cog.setupicon', { - title: 'configure lobby', - onclick: () => - m.route.set( - '/chat/:lobby/:subaction', - { - lobby: m.route.param('lobby'), - subaction: 'setup', +// ************************* Chat Hub Sub-Components **************************** + +const ChatRoomHeader = () => { + return { + view: (vnode) => { + const room = vnode.attrs.room; + const lobbyHexId = rs.idToHex(room.lobby_id); + return m('.chat-hub-header-bar', [ + m('.chat-header-info', [ + m('.chat-header-name', room.lobby_name || ''), + m('.chat-header-topic', room.lobby_topic || 'No topic'), + ]), + m('.chat-header-actions', [ + m( + 'button.red', + { + title: 'Leave Room', + onclick: () => { + ChatLobbyModel.unsubscribeChatLobby(lobbyHexId, () => { + ChatHubState.selectedRoom = null; + ChatHubState.selectedRoomId = null; + ChatHubState.selectedRoomType = null; + m.route.set('/chat'); + }); }, - { replace: true } - ), - }), - ] - : [], - ChatLobbyModel.isSubscribed - ? [ - m('i.fas.fa-sign-out-alt.leaveicon', { - title: 'leaving lobby', - onclick: () => - ChatLobbyModel.unsubscribeChatLobby(m.route.param('lobby'), () => { - m.route.set('/chat', null, { replace: true }); - }), - }), - ] - : [] - ); + }, + [m('i.fas.fa-sign-out-alt'), ' Leave'] + ), + ]), + ]); + }, + }; +}; + +function scrollChatToBottom() { + setTimeout(() => { + const element = document.querySelector('.chat-hub-messages'); + if (element) { + element.scrollTop = element.scrollHeight; + } + }, 50); +} + +const ChatConversationView = () => { + return { + oninit: () => { + scrollChatToBottom(); + }, + view: () => { + return m('.chat-hub-conversation-layout', [ + m('.chat-hub-conversation-main', [ + m( + '.chat-hub-messages', + { + oncreate: () => scrollChatToBottom(), + onupdate: () => scrollChatToBottom(), + }, + ChatLobbyModel.messages + ), + m( + '.chat-hub-input-area', + [ + m('textarea.chat-hub-textarea', { + placeholder: 'Type a message... Press Enter to send', + enterkeyhint: 'send', + onkeydown: (e) => { + if ((e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey) { + const msg = e.target.value; + if (msg.trim() === '') return false; + e.target.value = ' sending ... '; + ChatLobbyModel.sendMessage(msg, () => { + e.target.value = ''; + scrollChatToBottom(); + }); + return false; + } + }, + }), + m( + 'button.chat-hub-send-btn', + { + onclick: (e) => { + const textarea = e.target.closest('.chat-hub-input-area').querySelector('textarea'); + const msg = textarea.value; + if (msg.trim() === '') return; + textarea.value = ' sending ... '; + ChatLobbyModel.sendMessage(msg, () => { + textarea.value = ''; + scrollChatToBottom(); + }); + }, + }, + m('i.fas.fa-paper-plane') + ), + ] + ), + ]), + m('.chat-hub-rightbar', [ + m('.rightbar-title', 'Participants'), + m('.rightbar-users-list', ChatLobbyModel.users.map((user) => { + const gxsId = user.key; + const name = user.name; + + // Load details for avatar if not cached + if (gxsId && ChatHubState.gxsDetails[gxsId] === undefined) { + ChatHubState.gxsDetails[gxsId] = null; // Mark as loading + rs.rsJsonApiRequest('/rsIdentity/getIdDetails', { id: gxsId }, (data) => { + if (data && data.details) { + ChatHubState.gxsDetails[gxsId] = data.details; + m.redraw(); + } + }); + } + + const avatar = getSafeAvatar(ChatHubState.gxsDetails[gxsId]); + const firstLetter = (name || '?').slice(0, 1).toUpperCase(); + + return m('.user', [ + m(peopleUtil.UserAvatar, { avatar, firstLetter }), + m('span.user-name', name), + ]); + })) + ]) + ]); + }, + }; }; // ***************************** Page Layouts ****************************** +const ChatRoomDetailView = () => { + return { + view: () => { + const room = ChatHubState.selectedRoom; + if (!room) return null; + + let participantCount = 0; + let participantNames = []; + if (room.gxs_ids) { + if (Array.isArray(room.gxs_ids)) { + participantCount = room.gxs_ids.length; + participantNames = room.gxs_ids.map((u) => rs.userList.username(u.key) || u.key); + } else if (typeof room.gxs_ids === 'object') { + const keys = Object.keys(room.gxs_ids); + participantCount = keys.length; + participantNames = keys.map((key) => rs.userList.username(key) || key); + } + } + participantNames.sort((a, b) => a.localeCompare(b)); + + const lobbyHexId = rs.idToHex(room.lobby_id); + + return m('.chat-room-detail-view', [ + m('.detail-section', [ + m('h3', 'Room Info'), + m('.info-grid', [ + m('.info-label', 'Room Name'), + m('.info-value', room.lobby_name || ''), + m('.info-label', 'Topic'), + m('.info-value', room.lobby_topic || 'None'), + m('.info-label', 'Participants'), + m('.info-value', participantCount + ' users'), + m('.info-label', 'Your Identity'), + m('.info-value', rs.userList.username(room.gxs_id) || room.gxs_id || '???'), + m('.info-label', 'Lobby ID'), + m('.info-value', lobbyHexId), + ]), + ]), + + m('.detail-section', [ + m('h3', 'Participants (' + participantCount + ')'), + participantNames.length > 0 + ? m( + '.participants-grid', + participantNames.map((name) => + m('.participant-card', m('.participant-name', name)) + ) + ) + : m('p.no-participants', 'No participant information available'), + ]), + ]); + }, + }; +}; + +const ChatRoomJoinView = () => { + let ownIds = []; + return { + oninit: () => peopleUtil.ownIds((data) => (ownIds = data)), + view: () => { + const room = ChatHubState.selectedRoom; + if (!room) return null; + + const lobbyHexId = rs.idToHex(room.lobby_id); + const participantCount = room.total_number_of_peers || 0; + + return m('.chat-room-detail-view', [ + m('.detail-section', [ + m('h3', 'Room Info'), + m('.info-grid', [ + m('.info-label', 'Room Name'), + m('.info-value', room.lobby_name || ''), + m('.info-label', 'Topic'), + m('.info-value', room.lobby_topic || 'None'), + m('.info-label', 'Participants'), + m('.info-value', participantCount + ' users'), + ]), + ]), + + m('.detail-section', [ + m('h3', 'Join Room'), + m('p.join-description', 'Select an identity to join this chat room:'), + m( + '.identities-grid', + ownIds.map((nick) => + m( + '.identity-card', + { onclick: () => ChatLobbyModel.enterPublicLobby(lobbyHexId, nick) }, + [ + m('.identity-name', rs.userList.username(nick) || nick), + m('i.fas.fa-sign-in-alt'), + ] + ) + ) + ), + ]), + ]); + }, + }; +}; + const Layout = { - view: () => m('.node-panel.chat-panel.chat-hub', [m(SubscribedLobbies), m(PublicLobbies)]), + oninit: () => { + ChatHubState.activeTab = 'chat'; + const lobbyId = m.route.param('lobby'); + if (lobbyId) { + ChatHubState.selectedRoomId = lobbyId; + ChatLobbyModel.loadLobby(lobbyId); + } + }, + onupdate: () => { + const lobbyId = m.route.param('lobby'); + if (lobbyId && ChatHubState.selectedRoomId !== lobbyId) { + ChatHubState.selectedRoomId = lobbyId; + ChatLobbyModel.loadLobby(lobbyId); + } + }, + view: () => { + const search = ChatHubState.searchString.toLowerCase(); + + const subscribedRooms = sortLobbies( + Object.values(ChatRoomsModel.subscribedRooms) + ).filter((info) => (info.lobby_name || '').toLowerCase().includes(search)); + + const publicRooms = (ChatRoomsModel.allRooms || []) + .filter((info) => !ChatRoomsModel.subscribed(info)) + .filter((info) => (info.lobby_name || '').toLowerCase().includes(search)); + + const isSelected = (info, type) => + ChatHubState.selectedRoomId === rs.idToHex(info.lobby_id); + + const lobbyId = ChatHubState.selectedRoomId; + let selectedRoom = null; + let selectedRoomType = null; + + if (lobbyId) { + if (ChatRoomsModel.subscribedRooms[lobbyId]) { + selectedRoom = ChatRoomsModel.subscribedRooms[lobbyId]; + selectedRoomType = 'subscribed'; + } else { + selectedRoom = ChatRoomsModel.allRooms.find( + (r) => rs.idToHex(r.lobby_id) === lobbyId + ); + if (selectedRoom) { + selectedRoomType = 'public'; + } + } + } + + if (selectedRoom) { + ChatHubState.selectedRoom = selectedRoom; + ChatHubState.selectedRoomType = selectedRoomType; + } else if (!m.route.param('lobby')) { + ChatHubState.selectedRoom = null; + ChatHubState.selectedRoomId = null; + ChatHubState.selectedRoomType = null; + } + + return m('.chat-hub-container', [ + m('.chat-hub-left-pane', [ + m('.chat-own-profile-card', [ + m('.profile-header', [ + m('i.fas.fa-comments', { style: { fontSize: '1.5rem', color: '#3ba4d7' } }), + m('.profile-info', [ + m('.profile-name', 'Chat rooms'), + ]), + ]), + ]), + + m('.chat-rooms-list-container', [ + m('.searchbar-container', [ + m('input.searchbar', { + type: 'text', + placeholder: 'Search chat rooms...', + value: ChatHubState.searchString, + oninput: (e) => { + ChatHubState.searchString = e.target.value; + }, + }), + ]), + m('.rooms-scroll', [ + subscribedRooms.length > 0 && [ + m('.rooms-section-title', [ + m('i.fas.fa-bookmark'), + m('span', 'Subscribed (' + subscribedRooms.length + ')'), + ]), + subscribedRooms.map((info) => { + const hexId = rs.idToHex(info.lobby_id); + let count = 0; + if (info.gxs_ids) { + if (Array.isArray(info.gxs_ids)) count = info.gxs_ids.length; + else if (typeof info.gxs_ids === 'object') + count = Object.keys(info.gxs_ids).length; + } + return m( + '.chat-room-list-item' + + (isSelected(info, 'subscribed') ? '.selected' : ''), + { + key: hexId, + onclick: () => { + m.route.set('/chat/:lobby', { lobby: hexId }); + }, + }, + [ + m('.room-icon', m('i.fas.fa-comments')), + m('.room-meta', [ + m('.room-name', info.lobby_name || ''), + m('.room-topic', info.lobby_topic || 'No topic'), + ]), + count > 0 && m('.room-badge', count), + ] + ); + }), + ], + + publicRooms.length > 0 && [ + m('.rooms-section-title', [ + m('i.fas.fa-globe'), + m('span', 'Public (' + publicRooms.length + ')'), + ]), + publicRooms.map((info) => { + const hexId = rs.idToHex(info.lobby_id); + const count = info.total_number_of_peers || 0; + return m( + '.chat-room-list-item.public-room' + + (isSelected(info, 'public') ? '.selected' : ''), + { + key: hexId, + onclick: () => { + m.route.set('/chat/:lobby', { lobby: hexId }); + }, + }, + [ + m('.room-icon', m('i.fas.fa-globe')), + m('.room-meta', [ + m('.room-name', info.lobby_name || ''), + m('.room-topic', info.lobby_topic || 'No topic'), + ]), + count > 0 && m('.room-badge', count), + ] + ); + }), + ], + + subscribedRooms.length === 0 && + publicRooms.length === 0 && + m('p.no-rooms', 'No chat rooms found'), + ]), + ]), + ]), + + m('.chat-hub-right-pane', [ + ChatHubState.selectedRoom + ? [ + ChatHubState.selectedRoomType === 'subscribed' + ? [ + m(ChatRoomHeader, { room: ChatHubState.selectedRoom }), + m('.chat-hub-tabs-container', [ + m('.chat-hub-tabs', [ + m( + 'button.tab-btn' + + (ChatHubState.activeTab === 'chat' ? '.active' : ''), + { + onclick: () => { + ChatHubState.activeTab = 'chat'; + scrollChatToBottom(); + }, + }, + [m('i.fas.fa-comments'), ' Chat'] + ), + m( + 'button.tab-btn' + + (ChatHubState.activeTab === 'details' ? '.active' : ''), + { + onclick: () => { + ChatHubState.activeTab = 'details'; + }, + }, + [m('i.fas.fa-info-circle'), ' Details'] + ), + ]), + ]), + m('.chat-hub-tab-content', { style: { padding: ChatHubState.activeTab === 'chat' ? '0' : '1.5rem' } }, [ + ChatHubState.activeTab === 'chat' + ? m(ChatConversationView) + : m(ChatRoomDetailView), + ]), + ] + : [ + m('.chat-hub-tab-content', m(ChatRoomJoinView)), + ], + ] + : m('.chat-pane-placeholder', [ + m('i.fas.fa-comments'), + m( + 'p', + 'Select a chat room from the left panel to view details or join a conversation.' + ), + ]), + ]), + ]); + }, }; const LayoutSingle = () => { @@ -681,10 +1046,7 @@ const LayoutSingle = () => { }, [ m('.chat-overlay', { onclick: () => MobileState.closeAll() }), - LobbyName(), - !isPrivate && m('.lobbies', m(SubscribedLeftLobbies), m(PublicLeftLobbies)), m('.messages', { onclick: () => MobileState.closeAll() }, ChatLobbyModel.messages), - m('.rightbar', ChatLobbyModel.users), m( '.chatMessage', {}, @@ -723,40 +1085,6 @@ const LayoutSingle = () => { }; }; -const LayoutSetup = () => { - let ownIds = []; - return { - oninit: () => peopleUtil.ownIds((data) => (ownIds = data)), - view: (vnode) => - m( - '.node-panel.chat-panel.chat-room.chat-setup', - { - class: - (MobileState.showLobbies ? 'show-lobbies ' : '') + - (MobileState.showUsers ? 'show-users' : ''), - }, - [ - m('.chat-overlay', { onclick: () => MobileState.closeAll() }), - LobbyName(), - m('.lobbies', m(SubscribedLeftLobbies), m(PublicLeftLobbies)), - m('.setup', [ - m('h5.selectidentity', 'Select identity to use'), - ownIds.map((nick) => - m( - '.identity' + - (ChatLobbyModel.currentLobby.gxs_id === nick ? '.selectedidentity' : ''), - { - onclick: () => ChatLobbyModel.setupAction(m.route.param('lobby'), nick), - }, - rs.userList.username(nick) - ) - ), - ]), - ] - ), - }; -}; - /* /rsChats/initiateDistantChatConnexion * @param[in] to_pid RsGxsId to start the connection @@ -802,16 +1130,13 @@ const LayoutCreateDistant = () => { module.exports = { oninit: () => { ChatRoomsModel.loadSubscribedRooms(); + loadOwnChatProfile(); }, view: (vnode) => { - if (m.route.param('lobby') === undefined) { - return m(Layout); - } else if (m.route.param('subaction') === 'setup') { - return m(LayoutSetup); - } else if (m.route.param('subaction') === 'createdistantchat') { + if (m.route.param('subaction') === 'createdistantchat') { return m(LayoutCreateDistant); } else { - return m(LayoutSingle); + return m(Layout); } }, }; diff --git a/webui-src/app/scss/pages/_chat.scss b/webui-src/app/scss/pages/_chat.scss index 84293e5..9f7958e 100644 --- a/webui-src/app/scss/pages/_chat.scss +++ b/webui-src/app/scss/pages/_chat.scss @@ -1,346 +1,1067 @@ -@use '../abstracts' as *; - -.lobby { - margin: 10px; - border: 1px solid #aaa; - border-radius: 20px; -} - -.lobby .mainname { - margin: 20px; - font-weight: 100; - font-size: 1.2em; -} - -.topic { - color: #666; -} - -.lobby>.topic { - font-size: 0.95em; - margin-left: 25px; - margin-bottom: 5px; -} - -.lefttitle { - margin-top: 15px; - margin-bottom: 0; - font-weight: 100; - font-size: 1.2em; -} - -.leftname { - margin-top: 5px; - margin-bottom: 5px; - padding: 5px; - font-weight: 100; - font-size: 1em; -} - -.leftlobby>.topic { - font-size: 0.75em; - margin-left: 15px; - margin-bottom: 5px; -} - -.subscribed, -.public { - cursor: pointer; -} - -.leftlobby { - border: 1px solid #aaa; - border-radius: 10px; - margin-top: 5px; - background-color: white; -} - -.leftlobby.selected-lobby, -.selectedidentity { - color: white; - background-color: #3ba4d7; -} - -.rightbar { - position: absolute; - width: 185px; - background-color: white; - overflow: auto; - top: 130px; - bottom: 15px; - right: 15px; -} - -.user { - padding: 5px; -} - -.lobbyName { - padding: 15px; - margin-top: 2rem; -} - -.lobbies { - position: absolute; - width: 185px; - left: 165px; - bottom: 15px; - top: 130px; - overflow: auto; -} - -.messages, -.setup { - position: absolute; - background-color: white; - top: 130px; - left: 360px; - right: 215px; - overflow: auto; -} - -.messages { - bottom: 115px; -} - -.messagetext { - white-space: break-spaces; - margin-right: 5px; -} - -.message>* { - margin-left: 5px; -} - -.username { - color: darkgreen; - font-weight: bolder; -} - -.chatMessage { - position: absolute; - background-color: white; - height: 85px; - bottom: 15px; - right: 215px; - left: 360px; -} - -textarea.chatMsg { - height: 100%; - width: 100%; -} - -.chatatchar { - margin-left: 0.2em; - margin-right: 0.2em; - color: silver; -} - -.setupicon { - margin-left: 1em; - cursor: pointer; -} - -.leaveicon { - margin-left: 1em; - cursor: pointer; - color: #d40000; -} - -.selectidentity { - margin: 15px; - font-size: 1.2em; -} - -.setup>.identity { - cursor: pointer; -} - -.setup { - bottom: 15px; -} - -.createDistantChat { - margin-top: 1em; -} - -.no-lobbies { - - .messages, - .chatMessage, - .setup { - left: 165px; - } -} - -/* CHAT ROOM (Single Chat) - Desktop Grid Layout */ -@media (min-width: 900px) { - .node-panel.chat-room { - display: grid !important; - grid-template-columns: 250px 1fr 200px !important; - /* Lobbies, Chat, Users */ - grid-template-rows: auto 1fr auto !important; - /* Header, Messages, Input */ - grid-template-areas: - "lobbies header rightbar" - "lobbies messages rightbar" - "lobbies input rightbar" !important; - padding: 0 !important; - height: 100% !important; - } - - .node-panel.chat-room .lobbyName { - grid-area: header; - padding: 10px; - border-bottom: 1px solid #eee; - margin: 0; - z-index: 10; - background: white; - } - - .node-panel.chat-room .lobbies { - grid-area: lobbies; - position: static !important; - width: auto !important; - height: auto !important; - border-right: 1px solid #ccc; - overflow-y: auto; - display: block !important; - top: auto !important; - bottom: auto !important; - left: auto !important; - } - - .node-panel.chat-room .messages { - grid-area: messages; - position: static !important; - width: auto !important; - height: auto !important; - overflow-y: auto; - padding: 10px; - left: auto !important; - right: auto !important; - top: auto !important; - bottom: auto !important; - margin: 0 !important; - } - - .node-panel.chat-room .rightbar { - grid-area: rightbar; - position: static !important; - width: auto !important; - border-left: 1px solid #ccc; - overflow-y: auto; - display: block !important; - } - - .node-panel.chat-room .chatMessage { - grid-area: input; - position: static !important; - width: auto !important; - height: auto !important; - border-top: 1px solid #eee; - left: auto !important; - right: auto !important; - bottom: auto !important; - flex: 0 0 auto; - padding: 10px !important; - background: white; - z-index: 10; - } -} - -/* Mobile Overrides - Ensure Flex Column */ -@media (max-width: 899px) { - .node-panel.chat-room { - display: flex !important; - flex-direction: column !important; - height: 100% !important; - position: relative !important; - } - - .node-panel.chat-room .lobbyName { - flex: 0 0 auto; - } - - .node-panel.chat-room .messages { - flex: 1 !important; - overflow-y: auto !important; - position: relative !important; - top: 0 !important; - bottom: 0 !important; - left: 0 !important; - right: 0 !important; - width: 100% !important; - height: auto !important; - margin: 0 !important; - } - - .node-panel.chat-room .chatMessage { - flex: 0 0 auto !important; - position: relative !important; - bottom: 0 !important; - left: 0 !important; - right: 0 !important; - width: 100% !important; - height: auto !important; - z-index: 100; - } - - .node-panel.chat-room .rightbar, - .node-panel.chat-room .lobbies { - display: none !important; - position: fixed !important; - top: 60px !important; - bottom: 0 !important; - width: 80% !important; - background: white !important; - z-index: 200 !important; - box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2) !important; - } - - .node-panel.chat-room.show-lobbies .lobbies { - display: block !important; - left: 0 !important; - } - - .node-panel.chat-room.show-users .rightbar { - display: block !important; - right: 0 !important; - } - - .chat-overlay { - display: none; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.4); - z-index: 150; - } - - .show-lobbies .chat-overlay, - .show-users .chat-overlay { - display: block; - } - - /* Mobile Icons in Header */ - .mobile-menu-icons { - display: flex; - gap: 15px; - font-size: 1.2rem; - } - - .mobile-menu-icons i { - cursor: pointer; - padding: 5px; - } -} - -@media (min-width: 900px) { - .mobile-menu-icons { - display: none; - } -} \ No newline at end of file +@use '../abstracts' as *; + +.lobby { + margin: 10px; + border: 1px solid #aaa; + border-radius: 20px; +} + +.lobby .mainname { + margin: 20px; + font-weight: 100; + font-size: 1.2em; +} + +.topic { + color: #666; +} + +.lobby>.topic { + font-size: 0.95em; + margin-left: 25px; + margin-bottom: 5px; +} + +.lefttitle { + margin-top: 15px; + margin-bottom: 0; + font-weight: 100; + font-size: 1.2em; +} + +.leftname { + margin-top: 5px; + margin-bottom: 5px; + padding: 5px; + font-weight: 100; + font-size: 1em; +} + +.leftlobby>.topic { + font-size: 0.75em; + margin-left: 15px; + margin-bottom: 5px; +} + +.subscribed, +.public { + cursor: pointer; +} + +.leftlobby { + border: 1px solid #aaa; + border-radius: 10px; + margin-top: 5px; + background-color: white; +} + +.leftlobby.selected-lobby, +.selectedidentity { + color: white; + background-color: #3ba4d7; +} + +.rightbar { + position: absolute; + width: 185px; + background-color: white; + overflow: auto; + top: 130px; + bottom: 15px; + right: 15px; +} + +.user { + padding: 5px; +} + +.lobbyName { + padding: 15px; + margin-top: 2rem; +} + +.lobbies { + position: absolute; + width: 185px; + left: 165px; + bottom: 15px; + top: 130px; + overflow: auto; +} + +.messages, +.setup { + position: absolute; + background-color: white; + top: 130px; + left: 360px; + right: 215px; + overflow: auto; +} + +.messages { + bottom: 115px; +} + +.messagetext { + white-space: break-spaces; + margin-right: 5px; +} + +.message>* { + margin-left: 5px; +} + +.username { + color: darkgreen; + font-weight: bolder; +} + +.chatMessage { + position: absolute; + background-color: white; + height: 85px; + bottom: 15px; + right: 215px; + left: 360px; +} + +textarea.chatMsg { + height: 100%; + width: 100%; +} + +.chatatchar { + margin-left: 0.2em; + margin-right: 0.2em; + color: silver; +} + +.setupicon { + margin-left: 1em; + cursor: pointer; +} + +.leaveicon { + margin-left: 1em; + cursor: pointer; + color: #d40000; +} + +.selectidentity { + margin: 15px; + font-size: 1.2em; +} + +.setup>.identity { + cursor: pointer; +} + +.setup { + bottom: 15px; +} + +.createDistantChat { + margin-top: 1em; +} + +.no-lobbies { + + .messages, + .chatMessage, + .setup { + left: 165px; + } +} + +/* CHAT ROOM (Single Chat) - Desktop Grid Layout */ +@media (min-width: 900px) { + .node-panel.chat-room { + display: grid !important; + grid-template-columns: 250px 1fr 200px !important; + /* Lobbies, Chat, Users */ + grid-template-rows: auto 1fr auto !important; + /* Header, Messages, Input */ + grid-template-areas: + "lobbies header rightbar" + "lobbies messages rightbar" + "lobbies input rightbar" !important; + padding: 0 !important; + height: 100% !important; + } + + .node-panel.chat-room .lobbyName { + grid-area: header; + padding: 10px; + border-bottom: 1px solid #eee; + margin: 0; + z-index: 10; + background: white; + } + + .node-panel.chat-room .lobbies { + grid-area: lobbies; + position: static !important; + width: auto !important; + height: auto !important; + border-right: 1px solid #ccc; + overflow-y: auto; + display: block !important; + top: auto !important; + bottom: auto !important; + left: auto !important; + } + + .node-panel.chat-room .messages { + grid-area: messages; + position: static !important; + width: auto !important; + height: auto !important; + overflow-y: auto; + padding: 10px; + left: auto !important; + right: auto !important; + top: auto !important; + bottom: auto !important; + margin: 0 !important; + } + + .node-panel.chat-room .rightbar { + grid-area: rightbar; + position: static !important; + width: auto !important; + border-left: 1px solid #ccc; + overflow-y: auto; + display: block !important; + } + + .node-panel.chat-room .chatMessage { + grid-area: input; + position: static !important; + width: auto !important; + height: auto !important; + border-top: 1px solid #eee; + left: auto !important; + right: auto !important; + bottom: auto !important; + flex: 0 0 auto; + padding: 10px !important; + background: white; + z-index: 10; + } +} + +/* Mobile Overrides - Ensure Flex Column */ +@media (max-width: 899px) { + .node-panel.chat-room { + display: flex !important; + flex-direction: column !important; + height: 100% !important; + position: relative !important; + } + + .node-panel.chat-room .lobbyName { + flex: 0 0 auto; + } + + .node-panel.chat-room .messages { + flex: 1 !important; + overflow-y: auto !important; + position: relative !important; + top: 0 !important; + bottom: 0 !important; + left: 0 !important; + right: 0 !important; + width: 100% !important; + height: auto !important; + margin: 0 !important; + } + + .node-panel.chat-room .chatMessage { + flex: 0 0 auto !important; + position: relative !important; + bottom: 0 !important; + left: 0 !important; + right: 0 !important; + width: 100% !important; + height: auto !important; + z-index: 100; + } + + .node-panel.chat-room .rightbar, + .node-panel.chat-room .lobbies { + display: none !important; + position: fixed !important; + top: 60px !important; + bottom: 0 !important; + width: 80% !important; + background: white !important; + z-index: 200 !important; + box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2) !important; + } + + .node-panel.chat-room.show-lobbies .lobbies { + display: block !important; + left: 0 !important; + } + + .node-panel.chat-room.show-users .rightbar { + display: block !important; + right: 0 !important; + } + + .chat-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.4); + z-index: 150; + } + + .show-lobbies .chat-overlay, + .show-users .chat-overlay { + display: block; + } + + /* Mobile Icons in Header */ + .mobile-menu-icons { + display: flex; + gap: 15px; + font-size: 1.2rem; + } + + .mobile-menu-icons i { + cursor: pointer; + padding: 5px; + } +} + +@media (min-width: 900px) { + .mobile-menu-icons { + display: none; + } +} + +/* ===================================================== + CHAT HUB - Two-Pane Layout (matching Network page) + ===================================================== */ + +.chat-hub-container { + display: flex; + height: 100%; + width: 100%; + overflow: hidden; + background-color: #f1f5f9; +} + +.chat-hub-left-pane { + width: 320px; + min-width: 300px; + max-width: 350px; + border-right: 1px solid #cbd5e1; + display: flex; + flex-direction: column; + background: #ffffff; + box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05); +} + +.chat-own-profile-card { + padding: 1.25rem; + border-bottom: 1px solid #e2e8f0; + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); + + .profile-header { + display: flex; + align-items: center; + gap: 1rem; + } + + .profile-info { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; + + .profile-name { + font-weight: 700; + color: #1e293b; + font-size: 1.1rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .profile-status { + font-size: 0.85rem; + color: #10b981; + font-weight: 500; + display: flex; + align-items: center; + gap: 0.35rem; + + &::before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + background-color: #10b981; + border-radius: 50%; + } + } + } +} + +.chat-rooms-list-container { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + + .searchbar-container { + padding: 0.75rem 1rem; + border-bottom: 1px solid #e2e8f0; + + input.searchbar { + width: 100%; + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + background-color: #f8fafc; + outline: none; + transition: all 0.2s; + + &:focus { + background-color: #ffffff; + border-color: #3ba4d7; + box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); + } + } + } + + .rooms-scroll { + flex: 1; + overflow-y: auto; + padding: 0.5rem 0; + } +} + +.rooms-section-title { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem 0.375rem; + font-size: 0.75rem; + font-weight: 700; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.05em; + + i { + font-size: 0.7rem; + color: #94a3b8; + } +} + +.chat-room-list-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + margin: 0.125rem 0.5rem; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background-color: #f1f5f9; + } + + &.selected { + background-color: #e0f2fe; + + .room-name { + color: #0369a1; + font-weight: 600; + } + } + + .room-icon { + flex-shrink: 0; + width: 36px; + height: 36px; + border-radius: 0.5rem; + background: linear-gradient(135deg, #3ba4d7, #0ea5e9); + display: flex; + align-items: center; + justify-content: center; + color: #ffffff; + font-size: 0.85rem; + } + + &.public-room .room-icon { + background: linear-gradient(135deg, #10b981, #059669); + } + + .room-meta { + flex: 1; + min-width: 0; + + .room-name { + font-size: 0.95rem; + color: #334155; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: color 0.2s; + } + + .room-topic { + font-size: 0.8rem; + color: #94a3b8; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .room-badge { + flex-shrink: 0; + min-width: 24px; + height: 24px; + border-radius: 12px; + background-color: #e2e8f0; + color: #475569; + font-size: 0.75rem; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + padding: 0 0.375rem; + } +} + +.chat-hub-right-pane { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: #f8fafc; +} + +.chat-pane-placeholder { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: #94a3b8; + gap: 1rem; + padding: 2rem; + text-align: center; + + i { + font-size: 4rem; + color: #cbd5e1; + } + + p { + font-size: 1.1rem; + max-width: 400px; + } +} + +.chat-hub-tab-content { + flex: 1; + overflow-y: auto; + padding: 1.5rem; +} + +.chat-room-detail-view { + display: flex; + flex-direction: column; + gap: 1.5rem; + + .detail-header { + display: flex; + align-items: flex-start; + gap: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid #e2e8f0; + flex-wrap: wrap; + + .detail-title { + flex: 1; + min-width: 200px; + + h2 { + font-size: 1.75rem; + font-weight: 800; + color: #1e293b; + margin-bottom: 0.25rem; + } + + .detail-subtitle { + font-size: 0.9rem; + color: #64748b; + display: flex; + align-items: center; + gap: 0.5rem; + } + } + + .detail-actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + + button { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + font-size: 0.9rem; + } + } + } + + .detail-section { + background-color: #ffffff; + border-radius: 0.5rem; + border: 1px solid #e2e8f0; + padding: 1.25rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); + + h3 { + font-size: 1.1rem; + font-weight: 700; + color: #334155; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid #f1f5f9; + } + + .info-grid { + display: grid; + grid-template-columns: 130px 1fr; + row-gap: 0.75rem; + font-size: 0.9rem; + + .info-label { + font-weight: 600; + color: #64748b; + } + + .info-value { + color: #1e293b; + word-break: break-all; + } + } + } +} + +.participants-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 0.5rem; +} + +.participant-card { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + background-color: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 0.375rem; + + .participant-name { + font-size: 0.875rem; + color: #334155; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.no-participants { + color: #94a3b8; + font-size: 0.9rem; + font-style: italic; +} + +.detail-actions-footer { + display: flex; + gap: 0.75rem; + + button { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + font-size: 0.9rem; + } +} + +.join-description { + color: #64748b; + font-size: 0.9rem; + margin-bottom: 1rem; +} + +.identities-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 0.75rem; +} + +.identity-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + background-color: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background-color: #e0f2fe; + border-color: #3ba4d7; + } + + .identity-name { + font-size: 0.95rem; + font-weight: 600; + color: #334155; + } + + i { + color: #3ba4d7; + font-size: 0.9rem; + } +} + +.no-rooms { + padding: 1rem; + color: #94a3b8; + text-align: center; + font-style: italic; +} + +/* Chat Hub Responsive - Mobile */ +@media (max-width: 899px) { + .chat-hub-container { + flex-direction: column; + } + + .chat-hub-left-pane { + width: 100%; + min-width: 0; + max-width: none; + max-height: 45%; + border-right: none; + border-bottom: 1px solid #cbd5e1; + } + + .chat-hub-right-pane { + flex: 1; + min-height: 0; + } +} + +/* ===================================================== + CHAT HUB - Right Pane Conversation & Tabs Styling + ===================================================== */ + +.chat-hub-header-bar { + padding: 0.75rem 1.5rem; + background-color: #ffffff; + border-bottom: 1px solid #e2e8f0; + display: flex; + align-items: center; + justify-content: space-between; + height: 65px; + flex-shrink: 0; +} + +.chat-hub-header-bar .chat-header-info { + display: flex; + flex-direction: column; + overflow: hidden; +} + +.chat-hub-header-bar .chat-header-info .chat-header-name { + font-size: 1.15rem; + font-weight: 800; + color: #1e293b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chat-hub-header-bar .chat-header-info .chat-header-topic { + font-size: 0.85rem; + color: #64748b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 0.125rem; +} + +.chat-hub-header-bar .chat-header-actions { + display: flex; + gap: 0.5rem; +} + +.chat-hub-header-bar .chat-header-actions button { + display: flex; + align-items: center; + gap: 0.35rem; + padding: 0.375rem 0.75rem; + font-size: 0.85rem; +} + +.chat-hub-tabs-container { + background-color: #ffffff; + border-bottom: 1px solid #cbd5e1; + padding: 0.5rem 1.5rem 0; +} + +.chat-hub-tabs { + display: flex; + gap: 0.5rem; +} + +.chat-hub-tabs .tab-btn { + padding: 0.625rem 1.25rem; + font-size: 0.95rem; + font-weight: 600; + color: #64748b; + background: transparent; + border: none; + border-radius: 0.375rem 0.375rem 0 0; + border-bottom: 3px solid transparent; + cursor: pointer; + box-shadow: none; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.chat-hub-tabs .tab-btn:hover { + color: #334155; + background-color: #f1f5f9; +} + +.chat-hub-tabs .tab-btn.active { + color: #3ba4d7; + border-bottom-color: #3ba4d7; + background-color: transparent; +} + +.chat-hub-tab-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: #f8fafc; +} + +.chat-hub-conversation-layout { + display: flex; + flex-direction: row; + height: 100%; + width: 100%; + overflow: hidden; +} + +.chat-hub-conversation-main { + display: flex; + flex-direction: column; + flex: 1; + height: 100%; + overflow: hidden; +} + +.chat-hub-rightbar { + width: 200px; + border-left: 1px solid #cbd5e1; + background-color: #ffffff; + display: flex; + flex-direction: column; + flex-shrink: 0; +} + +.chat-hub-rightbar .rightbar-title { + padding: 0.75rem 1rem; + font-size: 0.85rem; + font-weight: 700; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid #e2e8f0; +} + +.chat-hub-rightbar .rightbar-users-list { + flex: 1; + overflow-y: auto; + padding: 0.5rem; +} + +.chat-hub-rightbar .user { + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + color: #334155; + border-radius: 0.375rem; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 0.5rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chat-hub-rightbar .user:hover { + background-color: #f1f5f9; + color: #0f172a; +} + +.chat-hub-rightbar .user .defaultAvatar { + width: 2rem; + height: 2rem; + font-size: 0.9rem; + flex-shrink: 0; +} + +.chat-hub-rightbar .user img.avatar { + width: 2rem; + height: 2rem; + flex-shrink: 0; +} + +@media (max-width: 899px) { + .chat-hub-rightbar { + display: none; + } +} + +.chat-hub-messages { + flex: 1; + overflow-y: auto; + padding: 1.25rem 1.5rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +/* Chat bubble overrides for two-pane layout */ +.chat-hub-messages .message { + display: flex; + flex-direction: column; + max-width: 70%; + padding: 0.625rem 0.875rem; + border-radius: 0.75rem; + font-size: 0.925rem; + line-height: 1.4; + word-break: break-word; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.chat-hub-messages .message.incoming { + align-self: flex-start; + align-items: flex-start; + background-color: #ffffff; + color: #1e293b; + border: 1px solid #e2e8f0; + border-bottom-left-radius: 0.125rem; +} + +.chat-hub-messages .message.outgoing { + align-self: flex-end; + align-items: flex-end; + background-color: #3ba4d7; + color: #ffffff; + border-bottom-right-radius: 0.125rem; +} + +.chat-hub-messages .message .username { + font-size: 0.75rem; + margin-bottom: 0.25rem; + padding: 0 0.125rem; + font-weight: 700; +} + +.chat-hub-messages .message.incoming .username { + color: #0369a1; +} + +.chat-hub-messages .message.outgoing .username { + color: #e0f2fe; +} + +.chat-hub-messages .message .messagetext { + white-space: break-spaces; + margin: 0; +} + +.chat-hub-messages .message .datetime { + font-size: 0.7rem; + margin-top: 0.25rem; + padding: 0 0.125rem; + opacity: 0.8; +} + +.chat-hub-messages .message.incoming .datetime { + color: #64748b; +} + +.chat-hub-messages .message.outgoing .datetime { + color: #f1f5f9; +} + +.chat-hub-input-area { + padding: 1rem 1.5rem; + background-color: #ffffff; + border-top: 1px solid #cbd5e1; + display: flex; + gap: 0.75rem; + align-items: center; + flex-shrink: 0; +} + +.chat-hub-input-area textarea.chat-hub-textarea { + flex: 1; + resize: none; + height: 40px; + padding: 0.5rem 0.75rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + font-size: 0.9rem; + outline: none; + transition: all 0.2s; + background-color: #f8fafc; +} + +.chat-hub-input-area textarea.chat-hub-textarea:focus { + background-color: #ffffff; + border-color: #3ba4d7; + box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); +} + +.chat-hub-input-area button.chat-hub-send-btn { + padding: 0.5rem 1.25rem; + font-size: 0.9rem; + height: 40px; + display: flex; + align-items: center; + gap: 0.5rem; + border-radius: 0.375rem; +} + \ No newline at end of file diff --git a/webui-src/styles.css b/webui-src/styles.css index d7f2c37..acb0af8 100644 --- a/webui-src/styles.css +++ b/webui-src/styles.css @@ -623,3 +623,725 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem font-weight: 600; font-size: 0.9rem; } + +/* ===================================================== + CHAT HUB - Two-Pane Layout + ===================================================== */ + +.chat-hub-container { + display: flex; + height: 100%; + width: 100%; + overflow: hidden; + background-color: #f1f5f9; +} + +.chat-hub-left-pane { + width: 320px; + min-width: 300px; + max-width: 350px; + border-right: 1px solid #cbd5e1; + display: flex; + flex-direction: column; + background: #ffffff; + box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05); +} + +.chat-own-profile-card { + padding: 1.25rem; + border-bottom: 1px solid #e2e8f0; + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); +} + +.chat-own-profile-card .profile-header { + display: flex; + align-items: center; + gap: 1rem; +} + +.chat-own-profile-card .profile-info { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; +} + +.chat-own-profile-card .profile-info .profile-name { + font-weight: 700; + color: #1e293b; + font-size: 1.1rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chat-own-profile-card .profile-info .profile-status { + font-size: 0.85rem; + color: #10b981; + font-weight: 500; + display: flex; + align-items: center; + gap: 0.35rem; +} + +.chat-own-profile-card .profile-info .profile-status::before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + background-color: #10b981; + border-radius: 50%; +} + +.chat-rooms-list-container { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.chat-rooms-list-container .searchbar-container { + padding: 0.75rem 1rem; + border-bottom: 1px solid #e2e8f0; +} + +.chat-rooms-list-container .searchbar-container input.searchbar { + width: 100%; + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + background-color: #f8fafc; + outline: none; + transition: all 0.2s; +} + +.chat-rooms-list-container .searchbar-container input.searchbar:focus { + background-color: #ffffff; + border-color: #3ba4d7; + box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); +} + +.chat-rooms-list-container .rooms-scroll { + flex: 1; + overflow-y: auto; + padding: 0.5rem 0; +} + +.rooms-section-title { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem 0.375rem; + font-size: 0.75rem; + font-weight: 700; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.rooms-section-title i { + font-size: 0.7rem; + color: #94a3b8; +} + +.chat-room-list-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + margin: 0.125rem 0.5rem; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s; +} + +.chat-room-list-item:hover { + background-color: #f1f5f9; +} + +.chat-room-list-item.selected { + background-color: #e0f2fe; +} + +.chat-room-list-item.selected .room-meta .room-name { + color: #0369a1; + font-weight: 600; +} + +.chat-room-list-item .room-icon { + flex-shrink: 0; + width: 36px; + height: 36px; + border-radius: 0.5rem; + background: linear-gradient(135deg, #3ba4d7, #0ea5e9); + display: flex; + align-items: center; + justify-content: center; + color: #ffffff; + font-size: 0.85rem; +} + +.chat-room-list-item.public-room .room-icon { + background: linear-gradient(135deg, #10b981, #059669); +} + +.chat-room-list-item .room-meta { + flex: 1; + min-width: 0; +} + +.chat-room-list-item .room-meta .room-name { + font-size: 0.95rem; + color: #334155; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: color 0.2s; +} + +.chat-room-list-item .room-meta .room-topic { + font-size: 0.8rem; + color: #94a3b8; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chat-room-list-item .room-badge { + flex-shrink: 0; + min-width: 24px; + height: 24px; + border-radius: 12px; + background-color: #e2e8f0; + color: #475569; + font-size: 0.75rem; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + padding: 0 0.375rem; +} + +.chat-hub-right-pane { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: #f8fafc; +} + +.chat-pane-placeholder { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: #94a3b8; + gap: 1rem; + padding: 2rem; + text-align: center; +} + +.chat-pane-placeholder i { + font-size: 4rem; + color: #cbd5e1; +} + +.chat-pane-placeholder p { + font-size: 1.1rem; + max-width: 400px; +} + +.chat-hub-tab-content { + flex: 1; + overflow-y: auto; + padding: 1.5rem; +} + +.chat-room-detail-view { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.chat-room-detail-view .detail-header { + display: flex; + align-items: flex-start; + gap: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid #e2e8f0; + flex-wrap: wrap; +} + +.chat-room-detail-view .detail-header .detail-title { + flex: 1; + min-width: 200px; +} + +.chat-room-detail-view .detail-header .detail-title h2 { + font-size: 1.75rem; + font-weight: 800; + color: #1e293b; + margin-bottom: 0.25rem; +} + +.chat-room-detail-view .detail-header .detail-title .detail-subtitle { + font-size: 0.9rem; + color: #64748b; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.chat-room-detail-view .detail-header .detail-actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +.chat-room-detail-view .detail-header .detail-actions button { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + font-size: 0.9rem; +} + +.chat-room-detail-view .detail-section { + background-color: #ffffff; + border-radius: 0.5rem; + border: 1px solid #e2e8f0; + padding: 1.25rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); +} + +.chat-room-detail-view .detail-section h3 { + font-size: 1.1rem; + font-weight: 700; + color: #334155; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid #f1f5f9; +} + +.chat-room-detail-view .detail-section .info-grid { + display: grid; + grid-template-columns: 130px 1fr; + row-gap: 0.75rem; + font-size: 0.9rem; +} + +.chat-room-detail-view .detail-section .info-grid .info-label { + font-weight: 600; + color: #64748b; +} + +.chat-room-detail-view .detail-section .info-grid .info-value { + color: #1e293b; + word-break: break-all; +} + +.participants-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 0.5rem; +} + +.participant-card { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + background-color: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 0.375rem; +} + +.participant-card .participant-name { + font-size: 0.875rem; + color: #334155; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.no-participants { + color: #94a3b8; + font-size: 0.9rem; + font-style: italic; +} + +.detail-actions-footer { + display: flex; + gap: 0.75rem; +} + +.detail-actions-footer button { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + font-size: 0.9rem; +} + +.join-description { + color: #64748b; + font-size: 0.9rem; + margin-bottom: 1rem; +} + +.identities-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 0.75rem; +} + +.identity-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + background-color: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s; +} + +.identity-card:hover { + background-color: #e0f2fe; + border-color: #3ba4d7; +} + +.identity-card .identity-name { + font-size: 0.95rem; + font-weight: 600; + color: #334155; +} + +.identity-card i { + color: #3ba4d7; + font-size: 0.9rem; +} + +.no-rooms { + padding: 1rem; + color: #94a3b8; + text-align: center; + font-style: italic; +} + +/* Chat Hub Responsive - Mobile */ +@media (max-width: 899px) { + .chat-hub-container { + flex-direction: column; + } + + .chat-hub-left-pane { + width: 100%; + min-width: 0; + max-width: none; + max-height: 45%; + border-right: none; + border-bottom: 1px solid #cbd5e1; + } + + .chat-hub-right-pane { + flex: 1; + min-height: 0; + } +} + +/* ===================================================== + CHAT HUB - Right Pane Conversation & Tabs Styling + ===================================================== */ + +.chat-hub-header-bar { + padding: 0.75rem 1.5rem; + background-color: #ffffff; + border-bottom: 1px solid #e2e8f0; + display: flex; + align-items: center; + justify-content: space-between; + height: 65px; + flex-shrink: 0; +} + +.chat-hub-header-bar .chat-header-info { + display: flex; + flex-direction: column; + overflow: hidden; +} + +.chat-hub-header-bar .chat-header-info .chat-header-name { + font-size: 1.15rem; + font-weight: 800; + color: #1e293b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chat-hub-header-bar .chat-header-info .chat-header-topic { + font-size: 0.85rem; + color: #64748b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 0.125rem; +} + +.chat-hub-header-bar .chat-header-actions { + display: flex; + gap: 0.5rem; +} + +.chat-hub-header-bar .chat-header-actions button { + display: flex; + align-items: center; + gap: 0.35rem; + padding: 0.375rem 0.75rem; + font-size: 0.85rem; +} + +.chat-hub-tabs-container { + background-color: #ffffff; + border-bottom: 1px solid #cbd5e1; + padding: 0.5rem 1.5rem 0; +} + +.chat-hub-tabs { + display: flex; + gap: 0.5rem; +} + +.chat-hub-tabs .tab-btn { + padding: 0.625rem 1.25rem; + font-size: 0.95rem; + font-weight: 600; + color: #64748b; + background: transparent; + border: none; + border-radius: 0.375rem 0.375rem 0 0; + border-bottom: 3px solid transparent; + cursor: pointer; + box-shadow: none; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.chat-hub-tabs .tab-btn:hover { + color: #334155; + background-color: #f1f5f9; +} + +.chat-hub-tabs .tab-btn.active { + color: #3ba4d7; + border-bottom-color: #3ba4d7; + background-color: transparent; +} + +.chat-hub-tab-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: #f8fafc; +} + +.chat-hub-conversation-layout { + display: flex; + flex-direction: row; + height: 100%; + width: 100%; + overflow: hidden; +} + +.chat-hub-conversation-main { + display: flex; + flex-direction: column; + flex: 1; + height: 100%; + overflow: hidden; +} + +.chat-hub-rightbar { + width: 200px; + border-left: 1px solid #cbd5e1; + background-color: #ffffff; + display: flex; + flex-direction: column; + flex-shrink: 0; +} + +.chat-hub-rightbar .rightbar-title { + padding: 0.75rem 1rem; + font-size: 0.85rem; + font-weight: 700; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid #e2e8f0; +} + +.chat-hub-rightbar .rightbar-users-list { + flex: 1; + overflow-y: auto; + padding: 0.5rem; +} + +.chat-hub-rightbar .user { + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + color: #334155; + border-radius: 0.375rem; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 0.5rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chat-hub-rightbar .user:hover { + background-color: #f1f5f9; + color: #0f172a; +} + +.chat-hub-rightbar .user .defaultAvatar { + width: 2rem; + height: 2rem; + font-size: 0.9rem; + flex-shrink: 0; +} + +.chat-hub-rightbar .user img.avatar { + width: 2rem; + height: 2rem; + flex-shrink: 0; +} + +@media (max-width: 899px) { + .chat-hub-rightbar { + display: none; + } +} + +.chat-hub-messages { + flex: 1; + overflow-y: auto; + padding: 1.25rem 1.5rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +/* Chat bubble overrides for two-pane layout */ +.chat-hub-messages .message { + display: flex; + flex-direction: column; + max-width: 70%; + padding: 0.625rem 0.875rem; + border-radius: 0.75rem; + font-size: 0.925rem; + line-height: 1.4; + word-break: break-word; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.chat-hub-messages .message.incoming { + align-self: flex-start; + align-items: flex-start; + background-color: #ffffff; + color: #1e293b; + border: 1px solid #e2e8f0; + border-bottom-left-radius: 0.125rem; +} + +.chat-hub-messages .message.outgoing { + align-self: flex-end; + align-items: flex-end; + background-color: #3ba4d7; + color: #ffffff; + border-bottom-right-radius: 0.125rem; +} + +.chat-hub-messages .message .username { + font-size: 0.75rem; + margin-bottom: 0.25rem; + padding: 0 0.125rem; + font-weight: 700; +} + +.chat-hub-messages .message.incoming .username { + color: #0369a1; +} + +.chat-hub-messages .message.outgoing .username { + color: #e0f2fe; +} + +.chat-hub-messages .message .messagetext { + white-space: break-spaces; + margin: 0; +} + +.chat-hub-messages .message .datetime { + font-size: 0.7rem; + margin-top: 0.25rem; + padding: 0 0.125rem; + opacity: 0.8; +} + +.chat-hub-messages .message.incoming .datetime { + color: #64748b; +} + +.chat-hub-messages .message.outgoing .datetime { + color: #f1f5f9; +} + +.chat-hub-input-area { + padding: 1rem 1.5rem; + background-color: #ffffff; + border-top: 1px solid #cbd5e1; + display: flex; + gap: 0.75rem; + align-items: center; + flex-shrink: 0; +} + +.chat-hub-input-area textarea.chat-hub-textarea { + flex: 1; + resize: none; + height: 40px; + padding: 0.5rem 0.75rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + font-size: 0.9rem; + outline: none; + transition: all 0.2s; + background-color: #f8fafc; +} + +.chat-hub-input-area textarea.chat-hub-textarea:focus { + background-color: #ffffff; + border-color: #3ba4d7; + box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); +} + +.chat-hub-input-area button.chat-hub-send-btn { + padding: 0.5rem 1.25rem; + font-size: 0.9rem; + height: 40px; + display: flex; + align-items: center; + gap: 0.5rem; + border-radius: 0.375rem; +} + + From 4742a87040c5110cfa4dc003d0336323ae949066 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Mon, 6 Jul 2026 15:46:55 +0200 Subject: [PATCH 04/40] Added identicon display --- webui-src/app/chat/chat.js | 2 +- webui-src/app/jdenticon.js | 102 ++++++++++++++++++++++++++ webui-src/app/mail/mail_util.js | 1 + webui-src/app/main.js | 2 +- webui-src/app/network/network.js | 35 ++++----- webui-src/app/network/network_data.js | 67 +++++++++++------ webui-src/app/people/people.js | 34 ++++++--- webui-src/app/people/people_ownids.js | 1 + webui-src/app/people/people_util.js | 64 +++++++++++++--- 9 files changed, 239 insertions(+), 69 deletions(-) create mode 100644 webui-src/app/jdenticon.js diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index 987dbe6..6f6f57a 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -706,7 +706,7 @@ const ChatConversationView = () => { const firstLetter = (name || '?').slice(0, 1).toUpperCase(); return m('.user', [ - m(peopleUtil.UserAvatar, { avatar, firstLetter }), + m(peopleUtil.UserAvatar, { avatar, firstLetter, identityId: gxsId }), m('span.user-name', name), ]); })) diff --git a/webui-src/app/jdenticon.js b/webui-src/app/jdenticon.js new file mode 100644 index 0000000..b8b21eb --- /dev/null +++ b/webui-src/app/jdenticon.js @@ -0,0 +1,102 @@ +const CORNER_SPRITES = { + 0: [[0.5, 1.0], [1.0, 0.0], [1.0, 1.0]], + 1: [[0.5, 0.0], [1.0, 0.0], [0.5, 1.0], [0.0, 1.0]], + 2: [[0.5, 0], [1, 0], [1, 1], [0.5, 1], [1, 0.5]], + 3: [[0, 0.5], [0.5, 0], [1, 0.5], [0.5, 1], [0.5, 0.5]], + 4: [[0, 0.5], [1, 0], [1, 1], [0, 1], [1, 0.5]], + 5: [[1, 0], [1, 1], [0.5, 1], [1, 0.5], [0.5, 0.5]], + 6: [[0, 0], [1, 0], [1, 0.5], [0, 0], [0.5, 1], [0, 1]], + 7: [[0, 0], [0.5, 0], [1, 0.5], [0.5, 1], [0, 1], [0.5, 0.5]], + 8: [[0.5, 0], [0.5, 0.5], [1, 0.5], [1, 1], [0.5, 1], [0.5, 0.5], [0, 0.5]], + 9: [[0, 0], [1, 0], [0.5, 0.5], [1, 0.5], [0.5, 1], [0.5, 0.5], [0, 1]], + 10: [[0, 0.5], [0.5, 1], [1, 0.5], [0.5, 0], [1, 0], [1, 1], [0, 1]], + 11: [[0.5, 0], [1, 0], [1, 1], [0.5, 1], [1, 0.75], [0.5, 0.5], [1, 0.25]], + 12: [[0, 0.5], [0.5, 0], [0.5, 0.5], [1, 0], [1, 0.5], [0.5, 1], [0.5, 0.5], [0, 1]], + 13: [[0, 0], [1, 0], [1, 1], [0, 1], [1, 0.5], [0.5, 0.25], [0.5, 0.75], [0, 0.5], [0.5, 0.25]], + 14: [[0, 0.5], [0.5, 0.5], [0.5, 0], [1, 0], [0.5, 0.5], [1, 0.5], [0.5, 1], [0.5, 0.5], [0, 1]], + 15: [[0, 0], [1, 0], [0.5, 0.5], [0.5, 0], [0, 0.5], [1, 0.5], [0.5, 1], [0.5, 0.5], [0, 1]] +}; + +const CENTER_SPRITES = { + 0: [], + 1: [[0, 0], [1, 0], [1, 1], [0, 1]], + 2: [[0.5, 0], [1, 0.5], [0.5, 1], [0, 0.5]], + 3: [[0, 0], [1, 0], [1, 1], [0, 1], [0, 0.5], [0.5, 1], [1, 0.5], [0.5, 0], [0, 0.5]], + 4: [[0.25, 0], [0.75, 0], [0.5, 0.5], [1, 0.25], [1, 0.75], [0.5, 0.5], [0.75, 1], [0.25, 1], [0.5, 0.5], [0, 0.75], [0, 0.25], [0.5, 0.5]], + 5: [[0, 0], [0.5, 0.25], [1, 0], [0.75, 0.5], [1, 1], [0.5, 0.75], [0, 1], [0.25, 0.5]], + 6: [[0.33, 0.33], [0.67, 0.33], [0.67, 0.67], [0.33, 0.67]], + 7: [[0, 0], [0.33, 0], [0.33, 0.33], [0.66, 0.33], [0.67, 0], [1, 0], [1, 0.33], [0.67, 0.33], [0.67, 0.67], [1, 0.67], [1, 1], [0.67, 1], [0.67, 0.67], [0.33, 0.67], [0.33, 1], [0, 1], [0, 0.67], [0.33, 0.67], [0.33, 0.33], [0, 0.33]] +}; + +function getSpritePoints(shapePoints, size) { + return shapePoints.map(([rx, ry]) => `${(rx - 0.5) * size},${(ry - 0.5) * size}`).join(' '); +} + +function renderPolygon(shapePoints, x, y, angle, shapeAngle, size, color) { + if (!shapePoints || shapePoints.length === 0) return ''; + const halfSize = size / 2; + const pointsStr = getSpritePoints(shapePoints, size); + return ``; +} + +function toSvg(hash, width) { + if (!hash || hash.length < 18) { + hash = "00000000000000000000000000000000"; + } + + const csh = parseInt(hash.substr(0, 1), 16); + const ssh = parseInt(hash.substr(1, 1), 16); + const xsh = parseInt(hash.substr(2, 1), 16) & 7; + + // We rotate shape by default (rotate = true) + const cro = 90 * (parseInt(hash.substr(3, 1), 16) & 3); + const sro = 90 * (parseInt(hash.substr(4, 1), 16) & 3); + const xbg = parseInt(hash.substr(5, 1), 16) % 2; + + const cfr = parseInt(hash.substr(6, 2), 16); + const cfg = parseInt(hash.substr(8, 2), 16); + const cfb = parseInt(hash.substr(10, 2), 16); + + const sfr = parseInt(hash.substr(12, 2), 16); + const sfg = parseInt(hash.substr(14, 2), 16); + const sfb = parseInt(hash.substr(16, 2), 16); + + const fillCorner = `rgb(${cfr}, ${cfg}, ${cfb})`; + const fillSide = `rgb(${sfr}, ${sfg}, ${sfb})`; + + let fillCenter; + if (xbg > 0 && (Math.abs(cfr - sfr) > 127 || Math.abs(cfg - sfg) > 127 || Math.abs(cfb - sfb) > 127)) { + fillCenter = fillSide; + } else { + fillCenter = fillCorner; + } + + const size = width / 3; + const totalsize = width; + + let svgContent = ``; + + // Draw corners + const cornerPoints = CORNER_SPRITES[csh] || CORNER_SPRITES[15]; + svgContent += renderPolygon(cornerPoints, 0, 0, 0, cro, size, fillCorner); + svgContent += renderPolygon(cornerPoints, totalsize, 0, 90, cro, size, fillCorner); + svgContent += renderPolygon(cornerPoints, totalsize, totalsize, 180, cro, size, fillCorner); + svgContent += renderPolygon(cornerPoints, 0, totalsize, 270, cro, size, fillCorner); + + // Draw sides + const sidePoints = CORNER_SPRITES[ssh] || CORNER_SPRITES[15]; + svgContent += renderPolygon(sidePoints, 0, size, 0, sro, size, fillSide); + svgContent += renderPolygon(sidePoints, 2 * size, 0, 90, sro, size, fillSide); + svgContent += renderPolygon(sidePoints, 3 * size, 2 * size, 180, sro, size, fillSide); + svgContent += renderPolygon(sidePoints, size, 3 * size, 270, sro, size, fillSide); + + // Draw center + const centerPoints = CENTER_SPRITES[xsh] !== undefined ? CENTER_SPRITES[xsh] : CORNER_SPRITES[15]; + svgContent += renderPolygon(centerPoints, size, size, 0, 0, size, fillCenter); + + return `${svgContent}`; +} + +module.exports = { + toSvg +}; \ No newline at end of file diff --git a/webui-src/app/mail/mail_util.js b/webui-src/app/mail/mail_util.js index ca5511f..1242de4 100644 --- a/webui-src/app/mail/mail_util.js +++ b/webui-src/app/mail/mail_util.js @@ -256,6 +256,7 @@ const MessageView = () => { firstLetter: rs.userList.userMap[MailData.sender._addr_string] ? rs.userList.userMap[MailData.sender._addr_string].slice(0, 1).toUpperCase() : '', + identityId: MailData.sender._addr_string, }), m('.msg-details__info', [ MailData.sender && diff --git a/webui-src/app/main.js b/webui-src/app/main.js index 2e61068..0f034bc 100644 --- a/webui-src/app/main.js +++ b/webui-src/app/main.js @@ -117,7 +117,7 @@ const Layout = () => { links: { home: '/home', network: '/network', - people: '/people/OwnIdentity', + people: '/people/MyContacts', chat: '/chat', mail: '/mail/inbox', files: '/files/files', diff --git a/webui-src/app/network/network.js b/webui-src/app/network/network.js index 1d9be87..132d08c 100644 --- a/webui-src/app/network/network.js +++ b/webui-src/app/network/network.js @@ -12,6 +12,7 @@ const State = { ssl_id: '', gpg_id: '', customState: '', + avatar: '', }, ownGxsIds: [], selectedOwnGxsId: '', @@ -51,6 +52,14 @@ function loadOwnProfile() { m.redraw(); } }); + + // Fetch own SSL avatar using our own Location SSL ID + 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(); } @@ -203,25 +212,10 @@ const ConfirmRemove = () => { }; }; -// Helper: get avatar safely for UserAvatar (must pass undefined, not null) -function getSafeAvatar(details) { - if ( - details && - details.mAvatar && - details.mAvatar.mData && - details.mAvatar.mData.base64 !== '' - ) { - return details.mAvatar; - } - return undefined; -} - const OwnProfileCard = () => { return { view: () => { - const ownGxsId = State.ownProfile.gpg_id ? State.gpgToGxsIdMap[State.ownProfile.gpg_id.toLowerCase()] : null; - const ownDetails = ownGxsId ? State.gxsIdToDetailsMap[ownGxsId] : null; - const avatar = getSafeAvatar(ownDetails); + const avatar = State.ownProfile.avatar ? { mData: { base64: State.ownProfile.avatar } } : undefined; const firstLetter = (State.ownProfile.name || 'U').slice(0, 1).toUpperCase(); return m('.own-profile-card', [ @@ -271,9 +265,7 @@ const FriendsList = () => { : filteredFriends .sort((a, b) => (a[1].isOnline === b[1].isOnline ? 0 : a[1].isOnline ? -1 : 1)) .map(([gpgId, friend]) => { - const friendGxsId = State.gpgToGxsIdMap[gpgId.toLowerCase()]; - const friendDetails = friendGxsId ? State.gxsIdToDetailsMap[friendGxsId] : null; - const avatar = getSafeAvatar(friendDetails); + const avatar = friend.avatar ? { mData: { base64: friend.avatar } } : undefined; const firstLetter = (friend.name || '?').slice(0, 1).toUpperCase(); const isSelected = State.selectedFriendGpgId === gpgId; @@ -330,6 +322,11 @@ const DetailsTab = () => { return m('.network-detail-view', [ m('.detail-header', [ + m('.friend-avatar', m(peopleUtil.UserAvatar, { + avatar: friend.avatar ? { mData: { base64: friend.avatar } } : undefined, + firstLetter: (friend.name || '?').slice(0, 1).toUpperCase(), + size: 128, + })), m('.detail-title', [ m('h2', friend.name), m('.detail-subtitle', [ diff --git a/webui-src/app/network/network_data.js b/webui-src/app/network/network_data.js index a626ed4..427cbf3 100644 --- a/webui-src/app/network/network_data.js +++ b/webui-src/app/network/network_data.js @@ -46,31 +46,50 @@ Data.refreshGpgDetails = async function () { ) .catch(() => {}) .then(() => { - const gpgId = (data.gpg_id || '').toLowerCase(); - const loc = { - name: data.location, - id: data.id, - lastSeen: data.lastConnect, - isOnline, - gpg_id: gpgId, - customState, - }; + let avatar = ''; + return rs + .rsJsonApiRequest( + '/rsChats/getAvatar', + { pid: data.id }, + (avatarData) => { + if (avatarData && avatarData.retval && avatarData.avatar_base64_string) { + avatar = avatarData.avatar_base64_string; + } + } + ) + .catch(() => {}) + .then(() => { + const gpgId = (data.gpg_id || '').toLowerCase(); + const loc = { + name: data.location, + id: data.id, + lastSeen: data.lastConnect, + isOnline, + gpg_id: gpgId, + customState, + avatar, + }; - if (details[gpgId] === undefined) { - details[gpgId] = { - name: data.name, - isSearched: true, - isOnline, - locations: [loc], - customState, - }; - } else { - details[gpgId].locations.push(loc); - if (!details[gpgId].customState || (isOnline && customState)) { - details[gpgId].customState = customState; - } - } - details[gpgId].isOnline = details[gpgId].isOnline || isOnline; + if (details[gpgId] === undefined) { + details[gpgId] = { + name: data.name, + isSearched: true, + isOnline, + locations: [loc], + customState, + avatar: avatar || '', + }; + } else { + details[gpgId].locations.push(loc); + if (avatar) { + details[gpgId].avatar = avatar; + } + if (!details[gpgId].customState || (isOnline && customState)) { + details[gpgId].customState = customState; + } + } + details[gpgId].isOnline = details[gpgId].isOnline || isOnline; + }); }); }); }) diff --git a/webui-src/app/people/people.js b/webui-src/app/people/people.js index d270465..bfa392a 100644 --- a/webui-src/app/people/people.js +++ b/webui-src/app/people/people.js @@ -11,7 +11,7 @@ const { CreateIdentity, EditIdentity, DeleteIdentity } = ownIdsLayout; const State = { searchString: '', selectedId: null, // GXS ID of the selected identity - activeFilter: 'all', // 'all' | 'contacts' | 'own' + activeFilter: 'contacts', // 'all' | 'contacts' | 'own' gxsIdToDetailsMap: {}, ownGxsIds: [], gpgToGxsIdMap: {}, @@ -210,7 +210,13 @@ const DetailsTab = () => { return m('.network-detail-view', [ m('.detail-header', [ - m('.friend-avatar', m(peopleUtil.UserAvatar, { avatar: getSafeAvatar(details), firstLetter: (name || '?').slice(0, 1).toUpperCase() })), + m('.friend-avatar', m(peopleUtil.UserAvatar, { + avatar: getSafeAvatar(details), + firstLetter: (name || '?').slice(0, 1).toUpperCase(), + identityId: State.selectedId, + size: 128, + isSquare: true, + })), m('.detail-title', [ m('h2', name), m('.detail-subtitle', [ @@ -491,15 +497,6 @@ const PeopleLayout = () => { m('.people-left-pane', [ // Filter Tabs Group m('.people-filter-group', [ - m( - 'button.filter-btn' + (State.activeFilter === 'all' ? '.active' : ''), - { - onclick: () => { - m.route.set('/people/All'); - }, - }, - 'All' - ), m( 'button.filter-btn' + (State.activeFilter === 'contacts' ? '.active' : ''), { @@ -518,6 +515,15 @@ const PeopleLayout = () => { }, '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") @@ -583,7 +589,11 @@ const PeopleLayout = () => { }, }, [ - m('.friend-avatar', m(peopleUtil.UserAvatar, { avatar: itemAvatar, firstLetter: itemFirstLetter })), + m('.friend-avatar', m(peopleUtil.UserAvatar, { + avatar: itemAvatar, + firstLetter: itemFirstLetter, + identityId: gxsId, + })), m('.friend-meta', [ m('.friend-name', displayName), m( diff --git a/webui-src/app/people/people_ownids.js b/webui-src/app/people/people_ownids.js index cb8590f..91eed52 100644 --- a/webui-src/app/people/people_ownids.js +++ b/webui-src/app/people/people_ownids.js @@ -276,6 +276,7 @@ const Identity = () => { m(peopleUtil.UserAvatar, { avatar: details.mAvatar, firstLetter: details.mNickname.slice(0, 1).toUpperCase(), + identityId: details.mId, }), m('.details', [ m('p', 'ID:'), diff --git a/webui-src/app/people/people_util.js b/webui-src/app/people/people_util.js index 80c4322..c1da6e2 100644 --- a/webui-src/app/people/people_util.js +++ b/webui-src/app/people/people_util.js @@ -1,5 +1,6 @@ const rs = require('rswebui'); const m = require('mithril'); +const jdenticon = require('jdenticon'); function checksudo(id) { return id === '0000000000000000'; @@ -8,20 +9,58 @@ function checksudo(id) { const UserAvatar = () => ({ view: (v) => { const imageURI = v.attrs.avatar; - return imageURI === undefined || imageURI.mData.base64 === '' - ? m( - 'div.defaultAvatar', - { - // image isn't getting loaded - // ? m('img.defaultAvatar', { - // src: '../data/user.png' - // }) - }, - m('p', v.attrs.firstLetter) - ) - : m('img.avatar', { + const identityId = v.attrs.identityId || v.attrs.id; + const rawSize = v.attrs.size || 48; + const sizeStr = typeof rawSize === 'number' ? `${rawSize}px` : rawSize; + const pxSize = typeof rawSize === 'number' ? rawSize : parseInt(rawSize) || 48; + const isSquare = !!v.attrs.isSquare; + + if (imageURI && imageURI.mData && imageURI.mData.base64 !== '') { + return m('img.avatar', { src: 'data:image/png;base64,' + imageURI.mData.base64, + style: { + width: sizeStr, + height: sizeStr, + borderRadius: isSquare ? '0' : '', + } }); + } + + if (identityId && identityId !== '0000000000000000') { + const svgString = jdenticon.toSvg(identityId, pxSize); + return m('div.jdenticon-avatar', { + style: { + display: 'inline-block', + width: sizeStr, + height: sizeStr, + borderRadius: isSquare ? '0' : '50%', + overflow: 'hidden', + verticalAlign: 'middle', + marginRight: '0.3em', + }, + oncreate: (vnode) => { + const svg = vnode.dom.querySelector('svg'); + if (svg) { + svg.style.width = '100%'; + svg.style.height = '100%'; + svg.style.display = 'block'; + } + } + }, m.trust(svgString)); + } + + return m( + 'div.defaultAvatar', + { + style: { + width: sizeStr, + height: sizeStr, + borderRadius: isSquare ? '0' : '50%', + fontSize: `calc(${sizeStr} * 0.4)`, + } + }, + m('p', v.attrs.firstLetter) + ); }, }); @@ -120,6 +159,7 @@ const regularcontactInfo = () => { m(UserAvatar, { avatar: details.mAvatar, firstLetter: details.mNickname.slice(0, 1).toUpperCase(), + identityId: details.mId || v.attrs.id.mGroupId, }), m('.details', [ m('p', 'ID:'), From f6a9dbef413d7dbbcd4255474ab1670cb6dec711 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Mon, 6 Jul 2026 20:50:20 +0200 Subject: [PATCH 05/40] removed bubble chat from rooms --- webui-src/app/chat/chat.js | 35 +++++++++++++++++++-- webui-src/app/scss/pages/_chat.scss | 49 +++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index 6f6f57a..578fd3c 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -56,6 +56,16 @@ function sortLobbies(lobbies) { return []; // return empty array instead of undefined } +function getNicknameColor(id, name) { + const hashString = id && id !== '00000000000000000000000000000000' ? id : (name || ''); + let hash = 0; + for (let i = 0; i < hashString.length; i++) { + hash = hashString.charCodeAt(i) + ((hash << 5) - hash); + } + const hue = Math.abs(hash) % 360; + return `hsl(${hue}, 75%, 35%)`; +} + // ***************************** models *********************************** const MobileState = { @@ -191,6 +201,20 @@ const Message = () => { const text = (msg.msg || msg.message || '') .replaceAll('
', '\n') .replace(new RegExp('|<[^>]*>', 'gm'), ''); + + const chatType = ChatLobbyModel.currentLobby && ChatLobbyModel.currentLobby.chatType; + const isRoom = chatType === 3; + + if (isRoom) { + const nickColor = getNicknameColor(gxsId, username); + return m( + '.message.compact', + m('span.datetime', datetime), + m('span.username', { style: { color: nickColor } }, username + ':'), + m('span.messagetext', text) + ); + } + return m( '.message' + (msg.incoming ? '.incoming' : '.outgoing'), m('span.datetime', datetime), @@ -637,10 +661,12 @@ const ChatConversationView = () => { scrollChatToBottom(); }, view: () => { + const chatType = ChatLobbyModel.currentLobby && ChatLobbyModel.currentLobby.chatType; + const isRoom = chatType === 3; return m('.chat-hub-conversation-layout', [ m('.chat-hub-conversation-main', [ m( - '.chat-hub-messages', + '.chat-hub-messages' + (isRoom ? '.compact-container' : ''), { oncreate: () => scrollChatToBottom(), onupdate: () => scrollChatToBottom(), @@ -1036,6 +1062,7 @@ const LayoutSingle = () => { view: (vnode) => { const chatType = ChatLobbyModel.currentLobby.chatType; const isPrivate = chatType === 1 || chatType === 2; + const isRoom = chatType === 3; return m( '.node-panel.chat-panel.chat-room', { @@ -1046,7 +1073,11 @@ const LayoutSingle = () => { }, [ m('.chat-overlay', { onclick: () => MobileState.closeAll() }), - m('.messages', { onclick: () => MobileState.closeAll() }, ChatLobbyModel.messages), + m( + '.messages' + (isRoom ? '.compact-container' : ''), + { onclick: () => MobileState.closeAll() }, + ChatLobbyModel.messages + ), m( '.chatMessage', {}, diff --git a/webui-src/app/scss/pages/_chat.scss b/webui-src/app/scss/pages/_chat.scss index 9f7958e..f499cd3 100644 --- a/webui-src/app/scss/pages/_chat.scss +++ b/webui-src/app/scss/pages/_chat.scss @@ -1064,4 +1064,53 @@ textarea.chatMsg { gap: 0.5rem; border-radius: 0.375rem; } + +/* Compact Room Chat Style (No bubbles, unique nickname colors) */ +.chat-hub-messages.compact-container, +.messages.compact-container { + gap: 0.25rem !important; + padding: 0.75rem 1rem !important; + background-color: #ffffff !important; + display: flex !important; + flex-direction: column !important; + + .message.compact { + display: block !important; + max-width: 100% !important; + padding: 0.15rem 0 !important; + border-radius: 0 !important; + background-color: transparent !important; + border: none !important; + box-shadow: none !important; + align-self: flex-start !important; + font-size: 0.9rem !important; + line-height: 1.4 !important; + margin: 0 !important; + + .datetime { + color: #808080 !important; + margin-right: 0.5rem !important; + font-size: 0.8rem !important; + font-family: monospace !important; + opacity: 1 !important; + display: inline-block !important; + } + + .username { + font-weight: bold !important; + margin-right: 0.35rem !important; + font-size: 0.9rem !important; + display: inline-block !important; + } + + .messagetext { + color: #1e293b !important; + white-space: pre-wrap !important; + word-break: break-word !important; + display: inline !important; + margin: 0 !important; + } + } +} + \ No newline at end of file From 96eb83096812d7264f481342b8c40a28b0e22b27 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Mon, 6 Jul 2026 22:04:22 +0200 Subject: [PATCH 06/40] Fixed home shortid display Added to calculate background color when there is no Avatar available --- webui-src/app/home.js | 7 +++ webui-src/app/network/network.js | 5 +- webui-src/app/people/people_util.js | 25 +++++++++- webui-src/app/scss/pages/_chat.scss | 36 +++++++++----- webui-src/app/scss/pages/_home.scss | 4 +- webui-src/styles.css | 74 +++++++++++++++++++++++++++++ 6 files changed, 133 insertions(+), 18 deletions(-) diff --git a/webui-src/app/home.js b/webui-src/app/home.js index bb9e932..461aaff 100644 --- a/webui-src/app/home.js +++ b/webui-src/app/home.js @@ -72,6 +72,11 @@ const ConfirmCopied = () => { }; const retroshareId = () => { + function autoResize(el) { + if (!el) return; + el.style.height = 'auto'; + el.style.height = el.scrollHeight + 'px'; + } return { view(v) { return m('.retroshareID', [ @@ -83,6 +88,8 @@ const retroshareId = () => { onclick: () => { document.getElementById('retroId').select(); }, + oncreate: (vnode) => autoResize(vnode.dom), + onupdate: (vnode) => autoResize(vnode.dom), }, v.attrs.ownCert ), diff --git a/webui-src/app/network/network.js b/webui-src/app/network/network.js index 132d08c..7ab637e 100644 --- a/webui-src/app/network/network.js +++ b/webui-src/app/network/network.js @@ -220,7 +220,7 @@ const OwnProfileCard = () => { return m('.own-profile-card', [ m('.profile-header', [ - m(peopleUtil.UserAvatar, { avatar, firstLetter }), + m(peopleUtil.UserAvatar, { avatar, firstLetter, seed: State.ownProfile.name }), m('.profile-info', [ m('.profile-name', State.ownProfile.name || 'Loading...'), m('.profile-status', 'Online'), @@ -284,7 +284,7 @@ const FriendsList = () => { }, }, [ - m('.friend-avatar', m(peopleUtil.UserAvatar, { avatar, firstLetter })), + m('.friend-avatar', m(peopleUtil.UserAvatar, { avatar, firstLetter, seed: gpgId })), m('.friend-meta', [ m('.friend-name', friend.name), m( @@ -326,6 +326,7 @@ const DetailsTab = () => { avatar: friend.avatar ? { mData: { base64: friend.avatar } } : undefined, firstLetter: (friend.name || '?').slice(0, 1).toUpperCase(), size: 128, + seed: gpgId, })), m('.detail-title', [ m('h2', friend.name), diff --git a/webui-src/app/people/people_util.js b/webui-src/app/people/people_util.js index c1da6e2..95bd48c 100644 --- a/webui-src/app/people/people_util.js +++ b/webui-src/app/people/people_util.js @@ -6,6 +6,17 @@ function checksudo(id) { return id === '0000000000000000'; } +function getAvatarColor(seed) { + let hash = 0; + if (seed) { + for (let i = 0; i < seed.length; i++) { + hash = seed.charCodeAt(i) + ((hash << 5) - hash); + } + } + const hue = Math.abs(hash) % 360; + return `hsl(${hue}, 60%, 60%)`; +} + const UserAvatar = () => ({ view: (v) => { const imageURI = v.attrs.avatar; @@ -49,6 +60,9 @@ const UserAvatar = () => ({ }, m.trust(svgString)); } + const seed = v.attrs.seed || v.attrs.firstLetter || ''; + const backgroundColor = getAvatarColor(seed); + return m( 'div.defaultAvatar', { @@ -56,10 +70,17 @@ const UserAvatar = () => ({ width: sizeStr, height: sizeStr, borderRadius: isSquare ? '0' : '50%', - fontSize: `calc(${sizeStr} * 0.4)`, + backgroundColor: backgroundColor, } }, - m('p', v.attrs.firstLetter) + m('p', { + style: { + color: '#ffffff', + fontWeight: '900', + margin: '0', + fontSize: `calc(${sizeStr} * 0.55)`, + } + }, v.attrs.firstLetter || '?') ); }, }); diff --git a/webui-src/app/scss/pages/_chat.scss b/webui-src/app/scss/pages/_chat.scss index f499cd3..e70fe09 100644 --- a/webui-src/app/scss/pages/_chat.scss +++ b/webui-src/app/scss/pages/_chat.scss @@ -1065,10 +1065,10 @@ textarea.chatMsg { border-radius: 0.375rem; } -/* Compact Room Chat Style (No bubbles, unique nickname colors) */ +/* Compact Room Chat Style (No bubbles, unique nickname colors, IRC-style) */ .chat-hub-messages.compact-container, .messages.compact-container { - gap: 0.25rem !important; + gap: 0 !important; padding: 0.75rem 1rem !important; background-color: #ffffff !important; display: flex !important; @@ -1077,35 +1077,44 @@ textarea.chatMsg { .message.compact { display: block !important; max-width: 100% !important; - padding: 0.15rem 0 !important; + padding: 0.1rem 0 !important; border-radius: 0 !important; background-color: transparent !important; border: none !important; box-shadow: none !important; align-self: flex-start !important; - font-size: 0.9rem !important; - line-height: 1.4 !important; + font-size: 0.875rem !important; + line-height: 1.45 !important; margin: 0 !important; + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + + &:hover { + background-color: #f8fafc !important; + overflow: visible !important; + white-space: normal !important; + } .datetime { - color: #808080 !important; - margin-right: 0.5rem !important; - font-size: 0.8rem !important; + color: #a0a0a0 !important; + margin-right: 0.4rem !important; + font-size: 0.78rem !important; font-family: monospace !important; opacity: 1 !important; - display: inline-block !important; + display: inline !important; } .username { font-weight: bold !important; - margin-right: 0.35rem !important; - font-size: 0.9rem !important; - display: inline-block !important; + margin-right: 0.2rem !important; + font-size: 0.875rem !important; + display: inline !important; } .messagetext { color: #1e293b !important; - white-space: pre-wrap !important; + white-space: normal !important; word-break: break-word !important; display: inline !important; margin: 0 !important; @@ -1113,4 +1122,5 @@ textarea.chatMsg { } } + \ No newline at end of file diff --git a/webui-src/app/scss/pages/_home.scss b/webui-src/app/scss/pages/_home.scss index 7311777..5090d03 100644 --- a/webui-src/app/scss/pages/_home.scss +++ b/webui-src/app/scss/pages/_home.scss @@ -67,12 +67,14 @@ & .textArea { padding: 0; width: 100%; - min-height: 75px; + height: auto; font-size: 1rem; font-family: monospace; background: transparent; border: none; resize: none; + overflow: hidden; + field-sizing: content; } & i { diff --git a/webui-src/styles.css b/webui-src/styles.css index acb0af8..8ecb475 100644 --- a/webui-src/styles.css +++ b/webui-src/styles.css @@ -1344,4 +1344,78 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem border-radius: 0.375rem; } +/* Compact Room Chat Style (No bubbles, IRC-style single line per message) */ +.chat-hub-messages.compact-container, +.messages.compact-container { + gap: 0 !important; + padding: 0.75rem 1rem !important; + background-color: #ffffff !important; + display: flex !important; + flex-direction: column !important; +} +.chat-hub-messages.compact-container .message.compact, +.messages.compact-container .message.compact { + display: block !important; + max-width: 100% !important; + padding: 0.1rem 0 !important; + border-radius: 0 !important; + background-color: transparent !important; + border: none !important; + box-shadow: none !important; + align-self: flex-start !important; + font-size: 0.875rem !important; + line-height: 1.45 !important; + margin: 0 !important; + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + width: 100% !important; +} + +.chat-hub-messages.compact-container .message.compact:hover, +.messages.compact-container .message.compact:hover { + background-color: #f8fafc !important; + overflow: visible !important; + white-space: normal !important; +} + +.chat-hub-messages.compact-container .message.compact .datetime, +.messages.compact-container .message.compact .datetime { + color: #a0a0a0 !important; + margin-right: 0.4rem !important; + font-size: 0.78rem !important; + font-family: monospace !important; + opacity: 1 !important; + display: inline !important; + margin-top: 0 !important; + margin-bottom: 0 !important; + padding: 0 !important; +} + +.chat-hub-messages.compact-container .message.compact .username, +.messages.compact-container .message.compact .username { + font-weight: bold !important; + margin-right: 0.2rem !important; + margin-bottom: 0 !important; + font-size: 0.875rem !important; + display: inline !important; + padding: 0 !important; +} + +.chat-hub-messages.compact-container .message.compact .messagetext, +.messages.compact-container .message.compact .messagetext { + color: #1e293b !important; + white-space: normal !important; + word-break: break-word !important; + display: inline !important; + margin: 0 !important; +} + +/* Fix RetroShare ID textarea - auto-size to content, no scrollbar */ +.homepage .certificate__content .retroshareID .textArea { + min-height: unset !important; + height: auto !important; + overflow: hidden !important; + field-sizing: content !important; +} From 911dc01bf9de3513c09f90c139b343e3cbfeaf5a Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Wed, 8 Jul 2026 19:34:44 +0200 Subject: [PATCH 07/40] Improved loading people and network list Add Chat improvements Added Send Chat & Mail for Parcipants list Added tooltip functionality Improved Identiy details display Added show distant chat status --- webui-src/app/chat/chat.js | 611 +++++++++++++++++++++++++++++-- webui-src/app/network/network.js | 37 +- webui-src/app/people/people.js | 529 ++++++++++++++++++++++++-- webui-src/styles.css | 246 +++++++++++++ 4 files changed, 1352 insertions(+), 71 deletions(-) diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index 578fd3c..3f1da6a 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -1,9 +1,18 @@ const m = require('mithril'); const rs = require('rswebui'); const peopleUtil = require('people/people_util'); +const people = require('people/people'); // **************** utility functions ******************** +function get64Num(val) { + if (!val) return 0; + if (typeof val === 'object') { + return val.xint64 || parseInt(val.xstr64) || 0; + } + return Number(val) || 0; +} + function loadLobbyDetails(id, apply) { rs.rsJsonApiRequest( '/rsChats/getChatLobbyInfo', @@ -176,6 +185,33 @@ const Message = () => { view: (vnode) => { const msg = vnode.attrs; const datetime = new Date(msg.sendTime * 1000).toLocaleTimeString(); + 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( + '.message.incoming', + [ + m('span.datetime', datetime), + m('span.username', 'Chat status'), + m('.messagetext', { + style: { + backgroundColor: bgColor, + border: `1px ${borderStyle} ${borderColor}`, + color: textColor, + padding: '0.5rem 0.75rem', + borderRadius: '0.375rem', + display: 'inline-block', + marginTop: '0.25rem', + } + }, text) + ] + ); + } // Handle both HistoryMsg (peerId) and ChatMessage (lobby_peer_gxs_id) const rawGxsId = msg.lobby_peer_gxs_id || msg.peerId; let gxsId = rs.idToHex(rawGxsId); @@ -225,6 +261,24 @@ const Message = () => { }; }; +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.'; + } +} + const ChatLobbyModel = { currentLobby: { lobby_name: '...', @@ -235,6 +289,57 @@ const ChatLobbyModel = { users: [], messageKeys: new Set(), lastLobbyId: null, + distantChatStatus: null, + statusPollInterval: null, + + pollDistantChatStatus() { + if (!this.currentLobby || this.currentLobby.chatType !== 2) return; + rs.rsJsonApiRequest( + '/rsChats/getDistantChatStatus', + { + pid: this.currentLobby.lobby_id, + }, + (detail, success) => { + if (success && detail.retval) { + const oldStatus = this.distantChatStatus ? this.distantChatStatus.status : null; + this.distantChatStatus = detail.info; + + if (oldStatus !== null && oldStatus !== detail.info.status) { + if (detail.info.status === 2) { + this.addMessages([{ + chat_id: this.chatId(), + isSystem: true, + msg: 'Tunnel is secured. You can talk!', + sendTime: Math.floor(Date.now() / 1000) + }]); + } else if (detail.info.status === 3) { + this.addMessages([{ + chat_id: this.chatId(), + isSystem: true, + msg: 'Your partner closed the conversation.', + sendTime: Math.floor(Date.now() / 1000) + }]); + } + } + m.redraw(); + } + } + ); + }, + + startStatusPolling() { + this.stopStatusPolling(); + this.pollDistantChatStatus(); + this.statusPollInterval = setInterval(() => this.pollDistantChatStatus(), 3000); + }, + + stopStatusPolling() { + if (this.statusPollInterval) { + clearInterval(this.statusPollInterval); + this.statusPollInterval = null; + } + this.distantChatStatus = null; + }, // Helper to generate a unique key for deduplication getMessageKey(msg) { @@ -370,6 +475,7 @@ const ChatLobbyModel = { return cid; }, loadLobby(currentlobbyid) { + this.stopStatusPolling(); this.lastLobbyId = currentlobbyid; const finishLoad = (detail) => { @@ -425,18 +531,23 @@ const ChatLobbyModel = { if (Array.isArray(detail.gxs_ids)) { list = detail.gxs_ids.map((u) => { const key = u.key; - return { key, name: rs.userList.username(key) }; + return { key, name: rs.userList.username(key) || key, lastAct: get64Num(u.value) }; }); } else if (typeof detail.gxs_ids === 'object') { list = Object.keys(detail.gxs_ids).map((key) => { - return { key, name: rs.userList.username(key) }; + return { key, name: rs.userList.username(key) || key, lastAct: get64Num(detail.gxs_ids[key]) }; }); } list.sort((a, b) => a.name.localeCompare(b.name)); this.users = list; } else { - this.users = [{ key: detail.gxs_id || '', name: detail.lobby_name }]; + this.users = [{ key: detail.gxs_id || '', name: detail.lobby_name, lastAct: Math.floor(Date.now() / 1000) }]; } + + if (detail.chatType === 2) { + this.startStatusPolling(); + } + m.redraw(); }; @@ -532,6 +643,20 @@ const ChatHubState = { searchString: '', ownProfile: { name: 'Loading...' }, gxsDetails: {}, + hoveredUser: null, + mutedUsers: new Set(), + activeMenu: null, + showAttachModal: false, + attachPath: '', + isHashing: false, + hashingError: '', + showCreateRoomModal: false, + newRoomName: '', + newRoomTopic: '', + newRoomIdentity: '', + newRoomPublic: true, + ownGxsIdentities: [], + createRoomError: '', }; function loadOwnChatProfile() { @@ -619,27 +744,65 @@ const ChatRoomHeader = () => { view: (vnode) => { const room = vnode.attrs.room; const lobbyHexId = rs.idToHex(room.lobby_id); + const isDistant = room.chatType === 2; return m('.chat-hub-header-bar', [ m('.chat-header-info', [ - m('.chat-header-name', room.lobby_name || ''), + m('.chat-header-name-container', { style: 'display: flex; align-items: center; gap: 0.5rem;' }, [ + m('.chat-header-name', room.lobby_name || ''), + isDistant && m('i.fas.fa-circle', { + style: { + color: getStatusColor(ChatLobbyModel.distantChatStatus ? ChatLobbyModel.distantChatStatus.status : 0), + fontSize: '0.85rem', + transition: 'color 0.3s ease', + }, + title: getStatusTooltip(ChatLobbyModel.distantChatStatus ? ChatLobbyModel.distantChatStatus.status : 0), + }) + ]), m('.chat-header-topic', room.lobby_topic || 'No topic'), ]), m('.chat-header-actions', [ - m( - 'button.red', - { - title: 'Leave Room', - onclick: () => { - ChatLobbyModel.unsubscribeChatLobby(lobbyHexId, () => { - ChatHubState.selectedRoom = null; - ChatHubState.selectedRoomId = null; - ChatHubState.selectedRoomType = null; - m.route.set('/chat'); - }); - }, - }, - [m('i.fas.fa-sign-out-alt'), ' Leave'] - ), + 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('i.fas.fa-sign-out-alt'), ' Leave Chat'] + ) + : m( + 'button.red', + { + title: 'Leave Room', + onclick: () => { + ChatLobbyModel.unsubscribeChatLobby(lobbyHexId, () => { + ChatHubState.selectedRoom = null; + ChatHubState.selectedRoomId = null; + ChatHubState.selectedRoomType = null; + m.route.set('/chat'); + }); + }, + }, + [m('i.fas.fa-sign-out-alt'), ' Leave'] + ), ]), ]); }, @@ -655,6 +818,31 @@ function scrollChatToBottom() { }, 50); } +function pollHashStatus(localpath) { + rs.rsJsonApiRequest('/rsFiles/ExtraFileStatus', { localpath }, (data) => { + if (data && data.retval && data.info && data.info.hash && data.info.hash !== '0000000000000000000000000000000000000000') { + const info = data.info; + const sizeNum = info.size.xint64 || parseInt(info.size.xstr64) || info.size; + const fileLink = `${info.name} (${rs.formatBytes(sizeNum)})`; + + const textarea = document.querySelector('.chat-hub-textarea'); + if (textarea) { + const val = textarea.value; + textarea.value = val ? val + '\n' + fileLink : fileLink; + } + + ChatHubState.showAttachModal = false; + ChatHubState.isHashing = false; + ChatHubState.attachPath = ''; + m.redraw(); + } else { + if (ChatHubState.isHashing) { + setTimeout(() => pollHashStatus(localpath), 1000); + } + } + }); +} + const ChatConversationView = () => { return { oninit: () => { @@ -663,6 +851,8 @@ const ChatConversationView = () => { view: () => { const chatType = ChatLobbyModel.currentLobby && ChatLobbyModel.currentLobby.chatType; const isRoom = chatType === 3; + const isDistant = chatType === 2; + const canTalk = !isDistant || (ChatLobbyModel.distantChatStatus && ChatLobbyModel.distantChatStatus.status === 2); return m('.chat-hub-conversation-layout', [ m('.chat-hub-conversation-main', [ m( @@ -677,10 +867,12 @@ const ChatConversationView = () => { '.chat-hub-input-area', [ m('textarea.chat-hub-textarea', { - placeholder: 'Type a message... Press Enter to send', + placeholder: canTalk ? 'Type a message... Press Enter to send' : 'Waiting for tunnel to be secured...', + disabled: !canTalk, enterkeyhint: 'send', onkeydown: (e) => { if ((e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey) { + if (!canTalk) return false; const msg = e.target.value; if (msg.trim() === '') return false; e.target.value = ' sending ... '; @@ -692,10 +884,24 @@ const ChatConversationView = () => { } }, }), + m( + 'button.chat-hub-attach-btn', + { + disabled: !canTalk, + style: !canTalk ? 'opacity: 0.5; cursor: not-allowed; margin-right: 0.5rem;' : 'margin-right: 0.5rem;', + onclick: () => { + ChatHubState.showAttachModal = true; + } + }, + m('i.fas.fa-paperclip') + ), m( 'button.chat-hub-send-btn', { + disabled: !canTalk, + style: !canTalk ? 'opacity: 0.5; cursor: not-allowed;' : '', onclick: (e) => { + if (!canTalk) return; const textarea = e.target.closest('.chat-hub-input-area').querySelector('textarea'); const msg = textarea.value; if (msg.trim() === '') return; @@ -710,6 +916,55 @@ const ChatConversationView = () => { ), ] ), + ChatHubState.showAttachModal && m('.attach-modal-overlay', [ + m('.attach-modal', [ + m('h4', 'Attach File to Chat'), + m('p', 'Enter the absolute path of the file on your local system:'), + m('input[type=text][placeholder=e.g. C:\\Downloads\\file.zip]', { + value: ChatHubState.attachPath, + oninput: (e) => { ChatHubState.attachPath = e.target.value; }, + disabled: ChatHubState.isHashing, + }), + ChatHubState.isHashing && m('.hashing-spinner', [ + m('i.fas.fa-spinner.fa-spin'), + m('span', ' Hashing file... Please wait.') + ]), + ChatHubState.hashingError && m('p.error-text', ChatHubState.hashingError), + m('.modal-buttons', [ + m('button.btn.blue', { + disabled: ChatHubState.isHashing || !ChatHubState.attachPath.trim(), + onclick: () => { + const path = ChatHubState.attachPath.trim(); + ChatHubState.isHashing = true; + ChatHubState.hashingError = ''; + m.redraw(); + + rs.rsJsonApiRequest('/rsFiles/ExtraFileHash', { + localpath: path, + period: 86400 * 7, + flags: 0 + }, (data, success) => { + if (success && data.retval) { + pollHashStatus(path); + } else { + ChatHubState.isHashing = false; + ChatHubState.hashingError = 'Failed to initiate file hashing. Check path.'; + m.redraw(); + } + }); + } + }, 'Attach'), + m('button.btn.red', { + disabled: ChatHubState.isHashing, + onclick: () => { + ChatHubState.showAttachModal = false; + ChatHubState.attachPath = ''; + ChatHubState.hashingError = ''; + } + }, 'Cancel') + ]) + ]) + ]), ]), m('.chat-hub-rightbar', [ m('.rightbar-title', 'Participants'), @@ -728,14 +983,186 @@ const ChatConversationView = () => { }); } - const avatar = getSafeAvatar(ChatHubState.gxsDetails[gxsId]); + const details = ChatHubState.gxsDetails[gxsId]; + const avatar = getSafeAvatar(details); const firstLetter = (name || '?').slice(0, 1).toUpperCase(); - return m('.user', [ - m(peopleUtil.UserAvatar, { avatar, firstLetter, identityId: gxsId }), + // Calculate status color and tooltip + const now = Math.floor(Date.now() / 1000); + const tLastAct = user.lastAct || 0; + const isOwn = gxsId === rs.idToHex(ChatLobbyModel.currentLobby.gxs_id || ''); + const isMuted = ChatHubState.mutedUsers && ChatHubState.mutedUsers.has(gxsId); + + let statusColor = '#22c55e'; // active (green) + let statusTooltip = 'Active'; + + if (isMuted) { + statusColor = '#ef4444'; // muted (red) + statusTooltip = 'Muted'; + } else if (isOwn) { + statusColor = '#3ba4d7'; // own identity (blue) + statusTooltip = 'You'; + } else if (tLastAct + 600 < now) { + statusColor = '#cbd5e1'; // inactive > 10 mins (grey) + statusTooltip = 'Inactive'; + } else if (tLastAct + 300 < now) { + statusColor = '#eab308'; // away > 5 mins (yellow) + statusTooltip = 'Away'; + } + + return m('.user', { + onmouseenter: (e) => { + if (ChatHubState.activeMenu) return; // skip tooltip if menu is open + const rect = e.currentTarget.getBoundingClientRect(); + const rightbar = document.querySelector('.chat-hub-rightbar'); + if (rightbar) { + const parentRect = rightbar.getBoundingClientRect(); + const top = rect.top - parentRect.top + rect.height / 2; + ChatHubState.hoveredUser = { gxsId, name, top }; + } + }, + onmouseleave: () => { + ChatHubState.hoveredUser = null; + }, + onclick: (e) => { + e.preventDefault(); + e.stopPropagation(); + ChatHubState.hoveredUser = null; // hide tooltip + + const rect = e.currentTarget.getBoundingClientRect(); + const rightbar = document.querySelector('.chat-hub-rightbar'); + if (rightbar) { + const parentRect = rightbar.getBoundingClientRect(); + const top = rect.bottom - parentRect.top; + if (ChatHubState.activeMenu && ChatHubState.activeMenu.gxsId === gxsId) { + ChatHubState.activeMenu = null; + } else { + ChatHubState.activeMenu = { gxsId, name, top }; + } + m.redraw(); + } + }, + oncontextmenu: (e) => { + e.preventDefault(); + e.stopPropagation(); + ChatHubState.hoveredUser = null; // hide tooltip + + const rect = e.currentTarget.getBoundingClientRect(); + const rightbar = document.querySelector('.chat-hub-rightbar'); + if (rightbar) { + const parentRect = rightbar.getBoundingClientRect(); + const top = rect.bottom - parentRect.top; + ChatHubState.activeMenu = { gxsId, name, top }; + m.redraw(); + } + } + }, [ + m(peopleUtil.UserAvatar, { avatar, firstLetter, identityId: gxsId, size: 32 }), m('span.user-name', name), + statusColor !== '#22c55e' && m('i.fas.fa-circle', { + style: { + color: statusColor, + fontSize: '0.65rem', + marginLeft: 'auto', + flexShrink: 0, + transition: 'color 0.3s ease', + }, + title: statusTooltip + }) ]); - })) + })), + ChatHubState.hoveredUser && (() => { + const hUser = ChatHubState.hoveredUser; + const details = ChatHubState.gxsDetails[hUser.gxsId]; + if (!details) return null; + + const avatar = getSafeAvatar(details); + const firstLetter = (hUser.name || '?').slice(0, 1).toUpperCase(); + const votes = details.mReputation + ? (details.mReputation.mFriendsPositiveVotes - details.mReputation.mFriendsNegativeVotes) + : 0; + + return m('.user-tooltip', { + style: { + top: `${hUser.top}px`, + } + }, [ + m('.tooltip-avatar', m(peopleUtil.UserAvatar, { avatar, firstLetter, identityId: hUser.gxsId, size: 64 })), + m('.tooltip-details', [ + m('.tooltip-row', [m('span.tooltip-label', 'Identity name: '), m('span.tooltip-value', hUser.name)]), + m('.tooltip-row', [m('span.tooltip-label', 'Identity Id: '), m('span.tooltip-value.tooltip-id', hUser.gxsId)]), + details.mPgpId && details.mPgpId !== '0000000000000000' && m('.tooltip-row', [ + m('span.tooltip-label', 'Node: '), + m('span.tooltip-value', `${rs.userList.username(details.mPgpId) || hUser.name} [${details.mPgpId}]`) + ]), + m('.tooltip-row', [ + m('span.tooltip-label', 'Votes: '), + m('span.tooltip-value', { + style: { + color: votes >= 0 ? '#22c55e' : '#ef4444', + fontWeight: 'bold' + } + }, (votes >= 0 ? '+' : '') + votes) + ]) + ]) + ]); + })(), + ChatHubState.activeMenu && (() => { + const menu = ChatHubState.activeMenu; + const isOwn = menu.gxsId === rs.idToHex(ChatLobbyModel.currentLobby.gxs_id || ''); + const isMuted = ChatHubState.mutedUsers && ChatHubState.mutedUsers.has(menu.gxsId); + + return m('.rightbar-context-menu', { + style: { + top: `${menu.top}px`, + }, + onclick: (e) => { + e.stopPropagation(); + } + }, [ + !isOwn && m('.menu-item', { + onclick: () => { + if (isMuted) { + ChatHubState.mutedUsers.delete(menu.gxsId); + } else { + ChatHubState.mutedUsers.add(menu.gxsId); + } + ChatHubState.activeMenu = null; + m.redraw(); + } + }, [ + m('i.fas.fa-volume-mute', { style: 'color: #ef4444; margin-right: 0.5rem;' }), + isMuted ? 'Unmute participant' : 'Mute participant' + ]), + !isOwn && m('.menu-item', { + onclick: () => { + ChatHubState.activeMenu = null; + people.setSelectedId(menu.gxsId, 'chat'); + } + }, [ + m('i.fas.fa-comments', { style: 'color: #3b82f6; margin-right: 0.5rem;' }), + 'Start private chat' + ]), + !isOwn && m('.menu-item', { + onclick: () => { + ChatHubState.activeMenu = null; + people.setSelectedId(menu.gxsId, 'details', true); + } + }, [ + m('i.fas.fa-envelope', { style: 'color: #10b981; margin-right: 0.5rem;' }), + 'Send Message' + ]), + m('.menu-item', { + onclick: () => { + ChatHubState.activeMenu = null; + people.setSelectedId(menu.gxsId, 'details'); + } + }, [ + m('i.fas.fa-user', { style: 'color: #8b5cf6; margin-right: 0.5rem;' }), + 'Show author in people tab' + ]) + ]); + })() ]) ]); }, @@ -846,6 +1273,12 @@ const ChatRoomJoinView = () => { }; const Layout = { + dismissMenu: () => { + if (ChatHubState.activeMenu) { + ChatHubState.activeMenu = null; + m.redraw(); + } + }, oninit: () => { ChatHubState.activeTab = 'chat'; const lobbyId = m.route.param('lobby'); @@ -853,6 +1286,26 @@ const Layout = { ChatHubState.selectedRoomId = lobbyId; ChatLobbyModel.loadLobby(lobbyId); } + window.addEventListener('click', Layout.dismissMenu); + + // Load own identities for room creation + peopleUtil.ownIds((ids) => { + ChatHubState.ownGxsIdentities = ids || []; + if (ChatHubState.ownGxsIdentities.length > 0) { + ChatHubState.newRoomIdentity = ChatHubState.ownGxsIdentities[0]; + } + ChatHubState.ownGxsIdentities.forEach((id) => { + if (ChatHubState.gxsDetails[id] === undefined) { + rs.rsJsonApiRequest('/rsIdentity/getIdDetails', { id }, (data) => { + if (data && data.details) { + ChatHubState.gxsDetails[id] = data.details; + m.redraw(); + } + }); + } + }); + m.redraw(); + }); }, onupdate: () => { const lobbyId = m.route.param('lobby'); @@ -861,6 +1314,10 @@ const Layout = { ChatLobbyModel.loadLobby(lobbyId); } }, + onremove: () => { + ChatLobbyModel.stopStatusPolling(); + window.removeEventListener('click', Layout.dismissMenu); + }, view: () => { const search = ChatHubState.searchString.toLowerCase(); @@ -889,6 +1346,12 @@ const Layout = { ); if (selectedRoom) { selectedRoomType = 'public'; + } else if ( + ChatLobbyModel.currentLobby && + rs.idToHex(ChatLobbyModel.currentLobby.lobby_id || '') === lobbyId + ) { + selectedRoom = ChatLobbyModel.currentLobby; + selectedRoomType = 'subscribed'; } } } @@ -911,6 +1374,14 @@ const Layout = { m('.profile-name', 'Chat rooms'), ]), ]), + m('button.chat-create-lobby-btn', { + onclick: () => { + ChatHubState.showCreateRoomModal = true; + } + }, [ + m('i.fas.fa-plus'), + ' Create' + ]) ]), m('.chat-rooms-list-container', [ @@ -993,6 +1464,98 @@ const Layout = { m('p.no-rooms', 'No chat rooms found'), ]), ]), + ChatHubState.showCreateRoomModal && m('.attach-modal-overlay', [ + m('.attach-modal', [ + m('h4', 'Create New Chat Room'), + + m('.form-field', { style: 'display: flex; flex-direction: column; gap: 0.25rem;' }, [ + m('label', { style: 'font-weight: bold; font-size: 0.9rem; color: #475569;' }, 'Room Name:'), + m('input[type=text]', { + value: ChatHubState.newRoomName, + oninput: (e) => { ChatHubState.newRoomName = e.target.value; }, + placeholder: 'Enter room name', + style: 'padding: 0.5rem; border: 1px solid #cbd5e1; border-radius: 0.25rem; font-size: 0.9rem;' + }) + ]), + + m('.form-field', { style: 'display: flex; flex-direction: column; gap: 0.25rem; margin-top: 0.5rem;' }, [ + m('label', { style: 'font-weight: bold; font-size: 0.9rem; color: #475569;' }, 'Topic:'), + m('input[type=text]', { + value: ChatHubState.newRoomTopic, + oninput: (e) => { ChatHubState.newRoomTopic = e.target.value; }, + placeholder: 'Enter room topic', + style: 'padding: 0.5rem; border: 1px solid #cbd5e1; border-radius: 0.25rem; font-size: 0.9rem;' + }) + ]), + + m('.form-field', { style: 'display: flex; flex-direction: column; gap: 0.25rem; margin-top: 0.5rem;' }, [ + m('label', { style: 'font-weight: bold; font-size: 0.9rem; color: #475569;' }, 'Admin Identity:'), + m('select', { + value: ChatHubState.newRoomIdentity, + onchange: (e) => { ChatHubState.newRoomIdentity = e.target.value; }, + style: 'padding: 0.5rem; border: 1px solid #cbd5e1; border-radius: 0.25rem; font-size: 0.9rem; background-color: #ffffff;' + }, [ + ChatHubState.ownGxsIdentities && ChatHubState.ownGxsIdentities.map(id => { + const details = ChatHubState.gxsDetails[id]; + const name = details ? (details.mNickname || details.mGroupName) : id; + return m('option', { value: id }, name); + }) + ]) + ]), + + m('.form-field', { style: 'display: flex; gap: 0.5rem; align-items: center; margin-top: 0.75rem;' }, [ + m('input[type=checkbox]', { + checked: ChatHubState.newRoomPublic, + onclick: (e) => { ChatHubState.newRoomPublic = e.target.checked; } + }), + m('label', { style: 'font-size: 0.9rem; color: #475569;' }, 'Public Room') + ]), + + ChatHubState.createRoomError && m('p.error-text', { style: 'color: #ef4444; font-size: 0.85rem; margin: 0.5rem 0 0 0;' }, ChatHubState.createRoomError), + + m('.modal-buttons', { style: 'display: flex; justify-content: flex-end; gap: 0.75rem; margin-top: 1rem;' }, [ + m('button.btn.blue', { + disabled: !ChatHubState.newRoomName.trim() || !ChatHubState.newRoomIdentity, + onclick: () => { + const name = ChatHubState.newRoomName.trim(); + const topic = ChatHubState.newRoomTopic.trim(); + const identity = ChatHubState.newRoomIdentity; + const isPublic = ChatHubState.newRoomPublic; + const flags = isPublic ? 4 : 0; // RS_CHAT_LOBBY_FLAGS_PUBLIC + + rs.rsJsonApiRequest('/rsChats/createChatLobby', { + lobby_name: name, + lobby_identity: identity, + lobby_topic: topic, + invited_friends: [], + lobby_privacy_type: flags + }, (data, success) => { + if (success) { + ChatHubState.showCreateRoomModal = false; + ChatHubState.newRoomName = ''; + ChatHubState.newRoomTopic = ''; + ChatHubState.createRoomError = ''; + // Refresh rooms list + ChatRoomsModel.loadRooms(); + m.redraw(); + } else { + ChatHubState.createRoomError = 'Failed to create room. Check parameters.'; + m.redraw(); + } + }); + } + }, 'Create'), + m('button.btn.red', { + onclick: () => { + ChatHubState.showCreateRoomModal = false; + ChatHubState.newRoomName = ''; + ChatHubState.newRoomTopic = ''; + ChatHubState.createRoomError = ''; + } + }, 'Cancel') + ]) + ]) + ]), ]), m('.chat-hub-right-pane', [ diff --git a/webui-src/app/network/network.js b/webui-src/app/network/network.js index 7ab637e..b6e9334 100644 --- a/webui-src/app/network/network.js +++ b/webui-src/app/network/network.js @@ -22,6 +22,7 @@ const State = { searchString: '', gpgToGxsIdMap: {}, gxsIdToDetailsMap: {}, + gxsIdentities: [], currentChatPeerId: null, chatMessages: [], chatInputMsg: '', @@ -94,23 +95,29 @@ function loadSelectedOwnGxsDetails() { ); } +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(); + } + }); + } +} + // Build map GPG ID -> GXS ID for all known identities function loadGxsIdentities() { rs.rsJsonApiRequest('/rsIdentity/getIdentitiesSummaries', {}, (data) => { if (data && data.ids) { - data.ids.forEach((user) => { - const gxsId = user.mGroupId; - 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(); - } - }); - }); + State.gxsIdentities = data.ids.map(u => u.mGroupId); + m.redraw(); } }); } @@ -572,6 +579,10 @@ const NetworkLayout = () => { ? State.gpgToGxsIdMap[State.selectedFriendGpgId.toLowerCase()] : null; + if (State.selectedFriendGpgId && !selectedGxsId && State.gxsIdentities) { + State.gxsIdentities.forEach(gxsId => fetchIdDetails(gxsId)); + } + return m('.network-container', [ m('.network-left-pane', [m(OwnProfileCard), m(FriendsList)]), m('.network-right-pane', [ diff --git a/webui-src/app/people/people.js b/webui-src/app/people/people.js index bfa392a..bf54a62 100644 --- a/webui-src/app/people/people.js +++ b/webui-src/app/people/people.js @@ -21,25 +21,34 @@ const State = { chatPid: null, chatMessages: [], chatInputMsg: '', + distantChatStatus: null, + statusPollInterval: null, + chatDisconnected: false, + activeMenu: null, }; +// Build map GPG ID -> GXS ID for all known identities +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(); + } + }); + } +} // Build map GPG ID -> GXS ID for all known identities function loadGxsIdentities() { rs.rsJsonApiRequest('/rsIdentity/getIdentitiesSummaries', {}, (data) => { if (data && data.ids) { - data.ids.forEach((user) => { - const gxsId = user.mGroupId; - 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(); - } - }); - }); + m.redraw(); } }); } @@ -57,6 +66,81 @@ function loadOwnGxsIds() { }); } +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}`; + } +} + // Helpers function getSafeAvatar(details) { return details && details.mAvatar ? details.mAvatar : undefined; @@ -73,6 +157,7 @@ function getOnlineSslId(gpgId) { } function isIdentityOnline(gxsId) { + fetchIdDetails(gxsId); const details = State.gxsIdToDetailsMap[gxsId]; if (details && details.mPgpId && details.mPgpId !== '0000000000000000') { const friend = Data.gpgDetails[details.mPgpId.toLowerCase()]; @@ -99,11 +184,89 @@ function syncFilter(tab) { } } +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( @@ -116,7 +279,10 @@ function initializeDistantChat() { (res) => { if (res && res.pid) { State.chatPid = rs.idToHex(res.pid); + State.distantChatStatus = null; loadChatMessages(); + pollDistantChatStatus(); + startStatusPolling(); } } ); @@ -199,6 +365,7 @@ function sendDistantChatMessage() { const DetailsTab = () => { return { view: () => { + fetchIdDetails(State.selectedId); const details = State.selectedId ? State.gxsIdToDetailsMap[State.selectedId] : null; if (!details) return null; @@ -210,13 +377,58 @@ const DetailsTab = () => { return m('.network-detail-view', [ m('.detail-header', [ - m('.friend-avatar', m(peopleUtil.UserAvatar, { - avatar: getSafeAvatar(details), - firstLetter: (name || '?').slice(0, 1).toUpperCase(), - identityId: State.selectedId, - size: 128, - isSquare: true, - })), + 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', [ @@ -317,8 +529,48 @@ const DetailsTab = () => { ? 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)), + ]); + }); + })() + ]) + ]), ]); }, }; @@ -327,6 +579,7 @@ const DetailsTab = () => { const ChatTab = () => { return { view: () => { + fetchIdDetails(State.selectedId); const details = State.selectedId ? State.gxsIdToDetailsMap[State.selectedId] : null; if (!details) return null; @@ -340,6 +593,18 @@ const ChatTab = () => { ]); } + 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'), @@ -348,22 +613,62 @@ const ChatTab = () => { ]); } + 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('span', { style: 'color: #64748b; font-weight: 500;' }, 'Distant Chat Tunnel'), - 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(); + 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', }, - }, State.ownGxsIds.map(id => m('option', { value: id }, rs.userList.username(id)))), + 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' + ]) + ]) ]), // Messages area @@ -375,6 +680,26 @@ const ChatTab = () => { 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); @@ -388,7 +713,9 @@ const ChatTab = () => { // Input area m('.chat-input-area', [ - m('textarea.chat-textarea[placeholder=Type your encrypted message here...]', { + 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; @@ -396,14 +723,18 @@ const ChatTab = () => { onkeydown: (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); - sendDistantChatMessage(); + if (canTalk) sendDistantChatMessage(); } }, }), m( 'button.send-btn.blue', { - onclick: () => sendDistantChatMessage(), + disabled: !canTalk, + style: !canTalk ? 'opacity: 0.5; cursor: not-allowed;' : '', + onclick: () => { + if (canTalk) sendDistantChatMessage(); + }, }, [m('i.fas.fa-paper-plane'), ' Send'] ), @@ -414,12 +745,20 @@ const ChatTab = () => { }; const PeopleLayout = () => { + const dismissMenu = () => { + if (State.activeMenu) { + State.activeMenu = null; + m.redraw(); + } + }; + return { oninit: (vnode) => { syncFilter(vnode.attrs.tab); Data.refreshGpgDetails().then(() => m.redraw()); loadGxsIdentities(); loadOwnGxsIds(); + window.addEventListener('click', dismissMenu); // Register for chatEvents to receive live incoming messages rs.events[15].notify = (chatMessage) => { @@ -448,6 +787,8 @@ const PeopleLayout = () => { if (rs.events[15]) { rs.events[15].notify = () => {}; } + stopStatusPolling(); + window.removeEventListener('click', dismissMenu); }, onupdate: (vnode) => { syncFilter(vnode.attrs.tab); @@ -489,6 +830,7 @@ const PeopleLayout = () => { }); // 3. Selected details details info + fetchIdDetails(State.selectedId); const details = State.selectedId ? State.gxsIdToDetailsMap[State.selectedId] : null; const name = details ? details.mNickname || details.mGroupName || 'Unknown' : ''; @@ -563,6 +905,7 @@ const PeopleLayout = () => { displayName = item.mGroupName || 'Unknown'; } + fetchIdDetails(gxsId); const itemDetails = State.gxsIdToDetailsMap[gxsId]; const itemAvatar = getSafeAvatar(itemDetails); const itemFirstLetter = (displayName || '?').slice(0, 1).toUpperCase(); @@ -576,17 +919,47 @@ const PeopleLayout = () => { '.friend-list-item', { class: isSelected ? 'selected' : '', - onclick: () => { + 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, { @@ -609,6 +982,68 @@ const PeopleLayout = () => { ); }), ]), + 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' + ]) + ]); + })() ]), ]), @@ -622,6 +1057,7 @@ const PeopleLayout = () => { { onclick: () => { State.activeTab = 'details'; + stopStatusPolling(); }, }, 'Profile Details' @@ -680,4 +1116,29 @@ const PeopleLayout = () => { }; }; +PeopleLayout.setSelectedId = (id, activeTab = 'details', showCompose = false) => { + const isOwn = State.ownGxsIds.includes(id); + const entry = rs.userList.userMap[id]; + const isContact = entry && entry.isContact; + + let filter = 'all'; + let route = '/people/All'; + if (isOwn) { + filter = 'own'; + route = '/people/OwnIdentity'; + } else if (isContact) { + filter = 'contacts'; + route = '/people/MyContacts'; + } + + State.activeFilter = filter; + State.selectedId = id; + State.activeTab = activeTab; + if (showCompose) { + State.showMailCompose = true; + } + + m.route.set(route); +}; + module.exports = PeopleLayout; diff --git a/webui-src/styles.css b/webui-src/styles.css index 8ecb475..8137f9e 100644 --- a/webui-src/styles.css +++ b/webui-src/styles.css @@ -112,6 +112,36 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem display: flex; flex-direction: column; overflow: hidden; + position: relative; +} + +.friends-list-container .people-context-menu { + position: absolute; + left: 2rem; + width: 210px; + background-color: #ffffff; + border: 1px solid #e2e8f0; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); + border-radius: 0.375rem; + z-index: 1010; + padding: 0.25rem 0; + display: flex; + flex-direction: column; +} + +.friends-list-container .people-context-menu .menu-item { + padding: 0.5rem 1rem; + font-size: 0.85rem; + color: #334155; + cursor: pointer; + display: flex; + align-items: center; + transition: background-color 0.2s; +} + +.friends-list-container .people-context-menu .menu-item:hover { + background-color: #f1f5f9; + color: #0f172a; } .friends-list-container .searchbar-container { @@ -651,6 +681,35 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem padding: 1.25rem; border-bottom: 1px solid #e2e8f0; background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); + position: relative; +} + +.chat-create-lobby-btn { + position: absolute; + bottom: 0.5rem; + right: 1.25rem; + background-color: #0084ff; + color: #ffffff; + border: none; + border-radius: 0.375rem; + padding: 0.35rem 0.75rem; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + box-shadow: 0 4px 6px -1px rgba(0, 132, 255, 0.2), 0 2px 4px -1px rgba(0, 132, 255, 0.1); + transition: background-color 0.2s, transform 0.2s; + display: flex; + align-items: center; + gap: 0.25rem; +} + +.chat-create-lobby-btn:hover { + background-color: #0073e6; + transform: translateY(-1px); +} + +.chat-create-lobby-btn:active { + transform: translateY(0); } .chat-own-profile-card .profile-header { @@ -1175,6 +1234,7 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem display: flex; flex-direction: column; flex-shrink: 0; + position: relative; } .chat-hub-rightbar .rightbar-title { @@ -1202,9 +1262,14 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem display: flex; align-items: center; gap: 0.5rem; + position: relative; +} + +.chat-hub-rightbar .user .user-name { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + flex: 1; } .chat-hub-rightbar .user:hover { @@ -1212,6 +1277,82 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem color: #0f172a; } +.chat-hub-rightbar .user-tooltip { + position: absolute; + left: -275px; + transform: translateY(-50%); + width: 260px; + background-color: #ffffe1; + border: 1px solid #7f7f7f; + box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.25); + padding: 0.5rem; + border-radius: 0.25rem; + z-index: 1000; + white-space: normal; + display: flex; + gap: 0.5rem; + align-items: flex-start; +} + +.chat-hub-rightbar .user-tooltip .tooltip-avatar { + flex-shrink: 0; +} + +.chat-hub-rightbar .user-tooltip .tooltip-details { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 0.8rem; + color: #000000; + text-align: left; +} + +.chat-hub-rightbar .user-tooltip .tooltip-row { + line-height: 1.2; +} + +.chat-hub-rightbar .user-tooltip .tooltip-label { + font-weight: bold; +} + +.chat-hub-rightbar .user-tooltip .tooltip-value { + font-weight: normal; + word-break: break-all; +} + +.chat-hub-rightbar .user-tooltip .tooltip-value.tooltip-id { + font-family: monospace; +} + +.chat-hub-rightbar .rightbar-context-menu { + position: absolute; + right: 1rem; + width: 210px; + background-color: #ffffff; + border: 1px solid #e2e8f0; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); + border-radius: 0.375rem; + z-index: 1010; + padding: 0.25rem 0; + display: flex; + flex-direction: column; +} + +.chat-hub-rightbar .rightbar-context-menu .menu-item { + padding: 0.5rem 1rem; + font-size: 0.85rem; + color: #334155; + cursor: pointer; + display: flex; + align-items: center; + transition: background-color 0.2s; +} + +.chat-hub-rightbar .rightbar-context-menu .menu-item:hover { + background-color: #f1f5f9; + color: #0f172a; +} + .chat-hub-rightbar .user .defaultAvatar { width: 2rem; height: 2rem; @@ -1419,3 +1560,108 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem overflow: hidden !important; field-sizing: content !important; } + +/* Attach file button and modal popup */ +.chat-hub-attach-btn { + background-color: transparent; + border: none; + font-size: 1.25rem; + color: #64748b; + cursor: pointer; + padding: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + transition: color 0.2s, transform 0.2s; +} + +.chat-hub-attach-btn:hover { + color: #3b82f6; + transform: scale(1.05); +} + +.attach-modal-overlay { + 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; +} + +.attach-modal { + background-color: #ffffff; + border-radius: 0.5rem; + width: 450px; + max-width: 90%; + padding: 1.5rem; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + gap: 1rem; +} + +.attach-modal h4 { + margin: 0; + font-size: 1.2rem; + color: #0f172a; +} + +.attach-modal p { + margin: 0; + font-size: 0.9rem; + color: #475569; +} + +.attach-modal input[type="text"] { + width: 100%; + padding: 0.75rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + font-size: 0.9rem; + outline: none; + transition: border-color 0.2s; +} + +.attach-modal input[type="text"]:focus { + border-color: #3b82f6; +} + +.attach-modal .hashing-spinner { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9rem; + color: #3b82f6; +} + +.attach-modal .error-text { + color: #ef4444; + font-size: 0.85rem; + margin: 0; +} + +.attach-modal .modal-buttons { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + margin-top: 0.5rem; +} + +.attach-modal .modal-buttons button { + padding: 0.5rem 1rem; + font-size: 0.9rem; + border-radius: 0.25rem; + border: none; + cursor: pointer; + transition: opacity 0.2s; +} + +.attach-modal .modal-buttons button:hover { + opacity: 0.9; +} From 845566da21563e22b2e270a30878b98ea18c2ead Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Thu, 9 Jul 2026 13:25:57 +0200 Subject: [PATCH 08/40] Fixed to update subscribed list list when chat rooms is created --- webui-src/app/chat/chat.js | 68 ++++++++++++++++++++++++++++++-------- 1 file changed, 55 insertions(+), 13 deletions(-) diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index 3f1da6a..f57a105 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -526,8 +526,8 @@ const ChatLobbyModel = { }; // Lookup for chat-user names + let list = []; if (detail.gxs_ids) { - let list = []; if (Array.isArray(detail.gxs_ids)) { list = detail.gxs_ids.map((u) => { const key = u.key; @@ -538,12 +538,27 @@ const ChatLobbyModel = { return { key, name: rs.userList.username(key) || key, lastAct: get64Num(detail.gxs_ids[key]) }; }); } - list.sort((a, b) => a.name.localeCompare(b.name)); - this.users = list; - } else { - this.users = [{ key: detail.gxs_id || '', name: detail.lobby_name, lastAct: Math.floor(Date.now() / 1000) }]; } + const ownId = detail.gxs_id; + if (ownId && ownId !== '00000000000000000000000000000000') { + const hasOwn = list.some((u) => u.key === ownId); + if (!hasOwn) { + list.push({ + key: ownId, + name: rs.userList.username(ownId) || ownId, + lastAct: Math.floor(Date.now() / 1000) + }); + } + } + + if (list.length === 0) { + list = [{ key: ownId || '', name: rs.userList.username(ownId) || detail.lobby_name || '???', lastAct: Math.floor(Date.now() / 1000) }]; + } + + list.sort((a, b) => a.name.localeCompare(b.name)); + this.users = list; + if (detail.chatType === 2) { this.startStatusPolling(); } @@ -1179,16 +1194,35 @@ const ChatRoomDetailView = () => { let participantCount = 0; let participantNames = []; + let participants = []; + if (room.gxs_ids) { if (Array.isArray(room.gxs_ids)) { - participantCount = room.gxs_ids.length; - participantNames = room.gxs_ids.map((u) => rs.userList.username(u.key) || u.key); + participants = room.gxs_ids.map((u) => ({ + key: u.key, + name: rs.userList.username(u.key) || u.key + })); } else if (typeof room.gxs_ids === 'object') { - const keys = Object.keys(room.gxs_ids); - participantCount = keys.length; - participantNames = keys.map((key) => rs.userList.username(key) || key); + participants = Object.keys(room.gxs_ids).map((key) => ({ + key: key, + name: rs.userList.username(key) || key + })); } } + + const ownId = room.gxs_id; + if (ownId && ownId !== '00000000000000000000000000000000') { + const hasOwn = participants.some((p) => p.key === ownId); + if (!hasOwn) { + participants.push({ + key: ownId, + name: rs.userList.username(ownId) || ownId + }); + } + } + + participantCount = participants.length; + participantNames = participants.map((p) => p.name); participantNames.sort((a, b) => a.localeCompare(b)); const lobbyHexId = rs.idToHex(room.lobby_id); @@ -1404,10 +1438,18 @@ const Layout = { subscribedRooms.map((info) => { const hexId = rs.idToHex(info.lobby_id); let count = 0; + let hasOwn = false; if (info.gxs_ids) { - if (Array.isArray(info.gxs_ids)) count = info.gxs_ids.length; - else if (typeof info.gxs_ids === 'object') + if (Array.isArray(info.gxs_ids)) { + count = info.gxs_ids.length; + hasOwn = info.gxs_ids.some((u) => u.key === info.gxs_id); + } else if (typeof info.gxs_ids === 'object') { count = Object.keys(info.gxs_ids).length; + hasOwn = info.gxs_ids[info.gxs_id] !== undefined; + } + } + if (!hasOwn && info.gxs_id && info.gxs_id !== '00000000000000000000000000000000') { + count++; } return m( '.chat-room-list-item' + @@ -1536,7 +1578,7 @@ const Layout = { ChatHubState.newRoomTopic = ''; ChatHubState.createRoomError = ''; // Refresh rooms list - ChatRoomsModel.loadRooms(); + ChatRoomsModel.loadSubscribedRooms(); m.redraw(); } else { ChatHubState.createRoomError = 'Failed to create room. Check parameters.'; From a03676c94a680f5a192e2d1b8442577a41791b28 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:47:12 +0200 Subject: [PATCH 09/40] Improved mails page Fixed some issues on mails reply, replay all,forward Added sort function Added show per page mails --- webui-src/app/mail/mail_attachment.js | 1 + webui-src/app/mail/mail_compose.js | 194 ++++++++++++-- webui-src/app/mail/mail_resolver.js | 98 ++++++- webui-src/app/mail/mail_util.js | 362 +++++++++++++++++++++++--- webui-src/app/scss/pages/_mail.scss | 13 +- webui-src/styles.css | 18 ++ 6 files changed, 616 insertions(+), 70 deletions(-) diff --git a/webui-src/app/mail/mail_attachment.js b/webui-src/app/mail/mail_attachment.js index 042d9e6..89ecbe8 100644 --- a/webui-src/app/mail/mail_attachment.js +++ b/webui-src/app/mail/mail_attachment.js @@ -52,6 +52,7 @@ const Layout = () => { 'tbody', v.attrs.list.map((msg) => m(util.MessageSummary, { + key: msg.msgId, details: msg, category: 'attachment', }) diff --git a/webui-src/app/mail/mail_compose.js b/webui-src/app/mail/mail_compose.js index 3fb7e16..b895548 100644 --- a/webui-src/app/mail/mail_compose.js +++ b/webui-src/app/mail/mail_compose.js @@ -3,12 +3,18 @@ const rs = require('rswebui'); const widget = require('widgets'); const peopleUtil = require('people/people_util'); +const UserAvatarsCache = {}; + const Layout = () => { + let showCc = false; + let showBcc = false; + let ownAvatars = {}; const Data = { allUsers: [], ownId: [], subject: '', identity: null, + bodyHtml: '', recipients: { to: { inputVal: '', @@ -27,13 +33,8 @@ const Layout = () => { }, }, }; - async function loadMailUserDetails(msgType, senderId, recipientList, isDirectMail) { + async function loadMailUserDetails(msgType, senderId, recipientList, isDirectMail, ccList) { Data.allUsers = await peopleUtil.sortUsers(rs.userList.users); - if (msgType === 'reply') { - Data.allUsers.forEach(async (user) => { - if (user.mGroupId === (await senderId)) Data.recipients.to.sendList.push(user); - }); - } // Wrap ownIds in a Promise const gxsIds = await new Promise((resolve) => { @@ -44,6 +45,20 @@ const Layout = () => { Data.ownId = gxsIds.filter((id) => id && id !== '0000000000000000' && Number(id) !== 0); + Data.ownId.forEach((id) => { + if (!ownAvatars[id]) { + rs.rsJsonApiRequest( + '/rsIdentity/getIdDetails', + { id }, + (data) => { + if (data?.details) { + ownAvatars[id] = data.details.mAvatar; + } + } + ); + } + }); + // Fetch own Node GPG ID const netStatus = await new Promise((resolve) => { rs.rsJsonApiRequest('/rsConfig/getConfigNetStatus', {}, (res) => { @@ -65,20 +80,91 @@ const Layout = () => { } } - if (msgType === 'reply') { + const resolvedSenderId = await senderId; + + if (msgType === 'reply' || msgType === 'replyAll') { + Data.allUsers.forEach((user) => { + if (user.mGroupId === resolvedSenderId) { + Data.recipients.to.sendList.push(user); + if (!UserAvatarsCache[resolvedSenderId]) { + rs.rsJsonApiRequest( + '/rsIdentity/getIdDetails', + { id: resolvedSenderId }, + (data) => { + if (data?.details) { + UserAvatarsCache[resolvedSenderId] = data.details.mAvatar; + } + } + ); + } + } + }); + } + + if (msgType === 'replyAll') { + // Add other "To" recipients + if (recipientList) { + Object.keys(recipientList).forEach((recip) => { + if (recip !== resolvedSenderId && !Data.ownId.includes(recip)) { + const user = Data.allUsers.find((u) => u.mGroupId === recip); + if (user && !Data.recipients.to.sendList.some((item) => item.mGroupId === recip)) { + Data.recipients.to.sendList.push(user); + if (!UserAvatarsCache[recip]) { + rs.rsJsonApiRequest( + '/rsIdentity/getIdDetails', + { id: recip }, + (data) => { + if (data?.details) { + UserAvatarsCache[recip] = data.details.mAvatar; + } + } + ); + } + } + } + }); + } + // Add other "Cc" recipients + if (ccList) { + Object.keys(ccList).forEach((recip) => { + if (recip !== resolvedSenderId && !Data.ownId.includes(recip)) { + const user = Data.allUsers.find((u) => u.mGroupId === recip); + if (user && !Data.recipients.cc.sendList.some((item) => item.mGroupId === recip)) { + Data.recipients.cc.sendList.push(user); + if (!UserAvatarsCache[recip]) { + rs.rsJsonApiRequest( + '/rsIdentity/getIdDetails', + { id: recip }, + (data) => { + if (data?.details) { + UserAvatarsCache[recip] = data.details.mAvatar; + } + } + ); + } + } + } + }); + } + } + + if (msgType === 'reply' || msgType === 'replyAll') { Data.identity = Data.ownId.filter((id) => Object.prototype.hasOwnProperty.call(recipientList, id) )[0]; } } async function loadDetails(attrs) { - const { msgType, senderId, recipientList, isDirectMail } = await attrs; - await loadMailUserDetails(msgType, senderId, recipientList, isDirectMail); + const { msgType, senderId, recipientList, ccList, isDirectMail } = await attrs; + await loadMailUserDetails(msgType, senderId, recipientList, isDirectMail, ccList); Object.keys(Data.recipients).forEach((item) => { Data.recipients[item].inputList = Data.allUsers; }); + if (Data.recipients.cc.sendList.length > 0) showCc = true; + if (Data.recipients.bcc.sendList.length > 0) showBcc = true; + if (msgType === 'compose') { if (!isDirectMail) { Data.identity = Data.ownId[0]; @@ -98,7 +184,7 @@ const Layout = () => { } } - if (msgType === 'reply') { + if (msgType === 'reply' || msgType === 'replyAll' || msgType === 'forward') { const { subject, replyMessage, timeStamp } = await attrs; const tmb = document.querySelector('#composerMailBody'); const time = timeStamp.toLocaleTimeString('UTC', { hour: '2-digit', minute: '2-digit' }); @@ -107,20 +193,21 @@ const Layout = () => { month: 'long', day: 'numeric', }); + const headerTitle = msgType === 'forward' ? 'Forwarded Message' : 'Original Message'; const replyMessageHeader = ` - -----Original Message----- + -----${headerTitle}-----
From: ${rs.userList.username(senderId)}
To: - ${Object.keys(recipientList).map( + ${recipientList ? Object.keys(recipientList).map( (recip) => ` ${rs.userList.username(recipientList[recip]._addr_string) || 'Unknown'}, ` - )} + ).join('') : ''}

Sent: @@ -130,13 +217,15 @@ const Layout = () => { ${subject}

+ ${msgType !== 'forward' ? ` On ${timeStamp.toLocaleDateString()} ${time}, ${rs.userList.username(senderId)} wrote: + ` : ''} `; - tmb.innerHTML = ` + const bodyHtml = `

@@ -146,7 +235,15 @@ const Layout = () => {
`; - Data.subject = subject.substring(0, 4) === 'Re: ' ? subject : `Re: ${subject}`; + if (tmb) { + tmb.innerHTML = bodyHtml; + } + Data.bodyHtml = bodyHtml; + if (msgType === 'forward') { + Data.subject = subject.substring(0, 5) === 'Fwd: ' ? subject : `Fwd: ${subject}`; + } else { + Data.subject = subject.substring(0, 4) === 'Re: ' ? subject : `Re: ${subject}`; + } } } return { @@ -161,6 +258,17 @@ const Layout = () => { } function handleClick(item, recipientType) { Data.recipients[recipientType].sendList.push(item); + if (item.mGroupId && !UserAvatarsCache[item.mGroupId]) { + rs.rsJsonApiRequest( + '/rsIdentity/getIdDetails', + { id: item.mGroupId }, + (data) => { + if (data?.details) { + UserAvatarsCache[item.mGroupId] = data.details.mAvatar; + } + } + ); + } // reset current input values after a sender is selected Data.recipients[recipientType].inputVal = ''; Data.recipients[recipientType].inputList = Data.allUsers; @@ -202,6 +310,14 @@ const Layout = () => { m('.widget__body.compose-mail', [ m('.compose-mail__from', [ m('label[for=idtags].bold', 'From: '), + Data.identity && m(peopleUtil.UserAvatar, { + avatar: ownAvatars[Data.identity], + firstLetter: rs.userList.userMap[Data.identity] && typeof rs.userList.userMap[Data.identity] === 'string' + ? rs.userList.userMap[Data.identity].slice(0, 1).toUpperCase() + : (rs.userList.username(Data.identity) || '').slice(0, 1).toUpperCase(), + identityId: Data.identity, + size: 24, + }), m( 'select[id=idtags]', { @@ -232,6 +348,12 @@ const Layout = () => { Data.recipients.to.sendList.length > 0 && Data.recipients.to.sendList.map((recipient) => m('.recipients__selected', [ + m(peopleUtil.UserAvatar, { + avatar: UserAvatarsCache[recipient.mGroupId], + firstLetter: recipient.mGroupName ? recipient.mGroupName.slice(0, 1).toUpperCase() : '', + identityId: recipient.mGroupId, + size: 20, + }), m('span', recipient.mGroupName), m('i.fas.fa-times', { onclick: () => removeSelectedItem(recipient, 'to'), @@ -252,14 +374,40 @@ const Layout = () => { ]), ]), ]), + m('.compose-mail__recipients__toggles', { + style: { + display: 'flex', + gap: '1rem', + alignItems: 'center', + marginLeft: 'auto', + paddingRight: '0.5rem', + userSelect: 'none', + } + }, [ + m('span.bold', { + style: { cursor: 'pointer', color: showCc ? '#019DFF' : '#555' }, + onclick: () => showCc = !showCc + }, 'Cc'), + m('span.bold', { + style: { cursor: 'pointer', color: showBcc ? '#019DFF' : '#555' }, + onclick: () => showBcc = !showBcc + }, 'Bcc') + ]) ]), - ['cc', 'bcc'].map((recipientType) => - m('.compose-mail__recipients__container', [ + ['cc', 'bcc'].map((recipientType) => { + const isVisible = recipientType === 'cc' ? showCc : showBcc; + return isVisible && m('.compose-mail__recipients__container', [ m('label.bold', `${recipientType}: `), m('.recipients', [ Data.recipients[recipientType].sendList.length > 0 && Data.recipients[recipientType].sendList.map((recipient) => m('.recipients__selected', [ + m(peopleUtil.UserAvatar, { + avatar: UserAvatarsCache[recipient.mGroupId], + firstLetter: recipient.mGroupName ? recipient.mGroupName.slice(0, 1).toUpperCase() : '', + identityId: recipient.mGroupId, + size: 20, + }), m('span', recipient.mGroupName), m('i.fas.fa-times', { onclick: () => removeSelectedItem(recipient, recipientType), @@ -284,15 +432,21 @@ const Layout = () => { ]), ]), ]), - ]) - ), + ]); + }), ]), m('input.compose-mail__subject[type=text][placeholder=Subject]', { value: Data.subject, oninput: (e) => (Data.subject = e.target.value), }), m('.compose-mail__message', [ - m('.compose-mail__message-body[placeholder=Message][contenteditable]#composerMailBody'), + m('.compose-mail__message-body[placeholder=Message][contenteditable]#composerMailBody', { + oncreate: (vnode) => { + if (Data.bodyHtml) { + vnode.dom.innerHTML = Data.bodyHtml; + } + } + }), ]), m('button.compose-mail__send-btn', { onclick: sendMail }, [ m('span', 'Send Mail'), diff --git a/webui-src/app/mail/mail_resolver.js b/webui-src/app/mail/mail_resolver.js index 7568d84..de0648e 100644 --- a/webui-src/app/mail/mail_resolver.js +++ b/webui-src/app/mail/mail_resolver.js @@ -41,6 +41,22 @@ const Messages = { Messages.spam = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_SPAM); Messages.attachment = Messages.all.filter((msg) => msg.count); + + Messages.important = Messages.all.filter( + (msg) => msg.msgtags && msg.msgtags.includes(util.RS_MSGTAGTYPE_IMPORTANT) + ); + Messages.work = Messages.all.filter( + (msg) => msg.msgtags && msg.msgtags.includes(util.RS_MSGTAGTYPE_WORK) + ); + Messages.personal = Messages.all.filter( + (msg) => msg.msgtags && msg.msgtags.includes(util.RS_MSGTAGTYPE_PERSONAL) + ); + Messages.todo = Messages.all.filter( + (msg) => msg.msgtags && msg.msgtags.includes(util.RS_MSGTAGTYPE_TODO) + ); + Messages.later = Messages.all.filter( + (msg) => msg.msgtags && msg.msgtags.includes(util.RS_MSGTAGTYPE_LATER) + ); } }); }, @@ -98,7 +114,14 @@ const Layout = () => { return [ m('.side-bar', [ - m('button.mail-compose-btn', { onclick: () => setShowCompose(true) }, 'Compose'), + m( + 'button.mail-compose-btn', + { + style: 'display: flex; align-items: center; justify-content: center; gap: 0.5rem;', + onclick: () => setShowCompose(true), + }, + [m('i.fas.fa-pen'), 'Compose'] + ), m(util.Sidebar, { tabs: Object.keys(sections), size: sectionsSize, @@ -128,9 +151,8 @@ const Layout = () => { vnode.children, ]) ), - m( + showCompose && m( '.composePopupOverlay#mailComposerPopup', - { style: { display: showCompose ? 'block' : 'none' } }, m( '.composePopup', m(compose, { msgType: 'compose', setShowCompose }), @@ -142,20 +164,80 @@ const Layout = () => { }; }; +const tabConfig = { + inbox: { title: 'Inbox', category: 'inbox' }, + outbox: { title: 'Outbox', category: 'outbox' }, + drafts: { title: 'Draft', category: 'drafts' }, + sent: { title: 'Sent', category: 'sent' }, + trash: { title: 'Trash', category: 'trash' }, + starred: { title: 'Starred', category: 'starred' }, + system: { title: 'System', category: 'system' }, + spam: { title: 'Spam', category: 'spam' }, + important: { title: 'Important', category: 'important' }, + work: { title: 'Work', category: 'work' }, + todo: { title: 'Todo', category: 'todo' }, + later: { title: 'Later', category: 'later' }, + personal: { title: 'Personal', category: 'personal' }, +}; + +const GenericMailList = () => { + return { + view: (vnode) => { + const { title, category, list } = vnode.attrs; + return [ + m('.widget__heading', m('h3', title)), + m('.widget__body', [ + m( + util.Table, + m( + 'tbody', + list.map((msg) => + m(util.MessageSummary, { + key: msg.msgId, + details: msg, + category: category, + }) + ) + ) + ), + ]), + ]; + }, + }; +}; + module.exports = { view: ({ attrs, attrs: { tab, msgId } }) => { // TODO: utilize multiple routing params if (Object.prototype.hasOwnProperty.call(attrs, 'msgId')) { return m(Layout, m(util.MessageView, { msgId })); } + + if (tab === 'attachment') { + return m( + Layout, + m(sectionsquickview.attachment, { + list: util.sortList(Messages[tab]), + }) + ); + } + + const config = tabConfig[tab]; + if (config) { + return m( + Layout, + m(GenericMailList, { + title: config.title, + category: config.category, + list: util.sortList(Messages[tab]), + }) + ); + } + return m( Layout, m(sections[tab] || sectionsquickview[tab], { - list: (Messages[tab] || []).sort((msgA, msgB) => { - const msgADate = new Date((msgA.ts.xint64 || 0) * 1000); - const msgBDate = new Date((msgB.ts.xint64 || 0) * 1000); - return msgADate < msgBDate; - }), + list: util.sortList(Messages[tab]), }) ); }, diff --git a/webui-src/app/mail/mail_util.js b/webui-src/app/mail/mail_util.js index 1242de4..e8f82f5 100644 --- a/webui-src/app/mail/mail_util.js +++ b/webui-src/app/mail/mail_util.js @@ -34,6 +34,36 @@ const MSG_ADDRESS_MODE_BCC = 0x03; const BOX_ALL = 0x06; +const MessageCache = {}; +const UserNicknamesCache = {}; + +const tagTypesCache = {}; +const defaultTagTypes = { + 1: { name: 'Important', color: '#ef4444' }, + 2: { name: 'Work', color: '#f97316' }, + 3: { name: 'Personal', color: '#22c55e' }, + 4: { name: 'Todo', color: '#3b82f6' }, + 5: { name: 'Later', color: '#a855f7' }, +}; + +function getTagDetails(tagId) { + return tagTypesCache[tagId] || defaultTagTypes[tagId] || { name: `Tag ${tagId}`, color: '#cbd5e1' }; +} + +function loadTagTypes() { + rs.rsJsonApiRequest('/rsMail/getMessageTagTypes', {}, (res) => { + if (res && res.body && res.body.tags && res.body.tags.types) { + res.body.tags.types.forEach((tag) => { + tagTypesCache[tag.key] = { + name: tag.value.first, + color: `#${tag.value.second.toString(16).padStart(6, '0')}`, + }; + }); + } + }); +} +loadTagTypes(); + // Utility functions const humanReadableSize = (fileSize) => { return fileSize / 1024 > 1024 @@ -65,10 +95,12 @@ const MessageSummary = () => { .then((res) => { if (res.body.retval) { details = res.body.msg; + details.msgtags = v.attrs.details.msgtags; files = details.files; isStarred = (details.msgflags & 0xf00) === RS_MSG_STAR; const flag = details.msgflags & 0xf0; msgStatus = flag === RS_MSG_NEW || flag === RS_MSG_UNREAD_BY_USER ? 'unread' : 'read'; + MessageCache[v.attrs.details.msgId] = details; } }) .then(() => { @@ -76,7 +108,12 @@ const MessageSummary = () => { rs.rsJsonApiRequest( '/rsIdentity/getIdDetails', { id: details.from._addr_string }, - (data) => (fromUserInfo = data.details) + (data) => { + fromUserInfo = data.details; + if (fromUserInfo) { + UserNicknamesCache[details.from._addr_string] = fromUserInfo.mNickname || ''; + } + } ); } }); @@ -85,18 +122,18 @@ const MessageSummary = () => { m( 'tr.msgbody', { - key: details.msgId, + key: v.attrs.details.msgId, class: msgStatus, onclick: () => - m.route.set('/mail/:tab/:msgId', { tab: v.attrs.category, msgId: details.msgId }), + m.route.set('/mail/:tab/:msgId', { tab: v.attrs.category, msgId: v.attrs.details.msgId }), }, [ m( 'td', - m(`input.star-check[type=checkbox][id=msg-${details.msgId}]`, { checked: isStarred }), + m(`input.star-check[type=checkbox][id=msg-${v.attrs.details.msgId}]`, { checked: isStarred }), // Use label with [for] to manipulate hidden checkbox m( - `label.star-check[for=msg-${details.msgId}]`, + `label.star-check[for=msg-${v.attrs.details.msgId}]`, { onclick: starMessage, class: (details.msgflags & 0xf00) === RS_MSG_STAR ? 'starred' : 'unstarred', @@ -105,10 +142,53 @@ const MessageSummary = () => { ) ), files && m('td', files.length), - m('td', details.title), + m('td', { style: 'border-bottom: inherit;' }, [ + m('div', { + style: { + display: 'flex', + alignItems: 'center', + gap: '0.5rem', + } + }, [ + m('span', details.title), + details.msgtags && details.msgtags.length > 0 && m('.mail-tags-container', { style: 'display: inline-flex; gap: 0.25rem;' }, + details.msgtags.map((tagId) => { + const tag = getTagDetails(tagId); + return m('span.mail-tag-badge', { + title: tag.name, + style: `display: inline-block; width: 10px; height: 10px; border-radius: 2px; background-color: ${tag.color};` + }); + }) + ) + ]) + ]), m( 'td', - fromUserInfo && Number(fromUserInfo.mId) !== 0 ? fromUserInfo.mNickname : '[Unknown]' + m( + 'div', + { + style: { + display: 'flex', + alignItems: 'center', + gap: '0.5rem', + justifyContent: 'start', + }, + }, + [ + fromUserInfo && + fromUserInfo.mAvatar && + fromUserInfo.mAvatar.mData && + fromUserInfo.mAvatar.mData.base64 && + fromUserInfo.mAvatar.mData.base64 !== '' && + m(peopleUtil.UserAvatar, { + avatar: fromUserInfo.mAvatar, + firstLetter: (fromUserInfo.mNickname || '').slice(0, 1).toUpperCase(), + identityId: details.from._addr_string, + size: 24, + }), + m('span', fromUserInfo && Number(fromUserInfo.mId) !== 0 ? fromUserInfo.mNickname : '[Unknown]'), + ] + ) ), m('td', new Date(details.ts * 1000).toLocaleString()), ] @@ -158,6 +238,7 @@ const AttachmentSection = () => { const MessageView = () => { let showCompose = false; + let composeType = 'reply'; // setFunction like react to show/hide popup function setShowCompose(bool) { showCompose = bool; @@ -222,11 +303,27 @@ const MessageView = () => { } else if (mode === MSG_ADDRESS_MODE_BCC && !MailData.bccList[addrString]) { MailData.bccList[addrString] = destDetail; } + if (addrString && !UserNicknamesCache[addrString]) { + rs.rsJsonApiRequest( + '/rsIdentity/getIdDetails', + { id: addrString }, + (data) => { + if (data?.details) { + UserNicknamesCache[addrString] = data.details.mNickname || ''; + } + } + ); + } }); rs.rsJsonApiRequest( '/rsIdentity/getIdDetails', { id: MailData?.sender?._addr_string }, - (data) => (MailData.avatar = data?.details?.mAvatar) + (data) => { + if (data?.details) { + MailData.avatar = data.details.mAvatar; + UserNicknamesCache[MailData.sender._addr_string] = data.details.mNickname || ''; + } + } ); } }, @@ -241,9 +338,9 @@ const MessageView = () => { m('i.fas.fa-arrow-left') ), m('.msg-view-nav__action', [ - m('button', { onclick: () => setShowCompose(true) }, 'Reply'), - m('button', 'Reply All'), - m('button', 'Forward'), + m('button', { onclick: () => { composeType = 'reply'; setShowCompose(true); } }, 'Reply'), + m('button', { onclick: () => { composeType = 'replyAll'; setShowCompose(true); } }, 'Reply All'), + m('button', { onclick: () => { composeType = 'forward'; setShowCompose(true); } }, 'Forward'), m('button', { onclick: confirmMailDelete }, 'Delete'), ]), ]), @@ -253,16 +350,14 @@ const MessageView = () => { MailData.sender && m(peopleUtil.UserAvatar, { avatar: MailData.avatar, - firstLetter: rs.userList.userMap[MailData.sender._addr_string] - ? rs.userList.userMap[MailData.sender._addr_string].slice(0, 1).toUpperCase() - : '', + firstLetter: (UserNicknamesCache[MailData.sender._addr_string] || rs.userList.username(MailData.sender._addr_string) || '').slice(0, 1).toUpperCase(), identityId: MailData.sender._addr_string, }), m('.msg-details__info', [ MailData.sender && m('.msg-details__info-item', [ m('b', 'From: '), - rs.userList.userMap[MailData.sender._addr_string] || 'Unknown', + UserNicknamesCache[MailData.sender._addr_string] || rs.userList.username(MailData.sender._addr_string) || 'Unknown', ]), m('.msg-details__info-item', [ m('b', 'To: '), @@ -270,7 +365,7 @@ const MessageView = () => { ? [ m('#truncate.truncated-view', [ Object.keys(MailData.toList).map((key, index) => - m('span', { key: index }, `${rs.userList.userMap[key] || 'Unknown'}, `) + m('span', { key: index }, `${UserNicknamesCache[key] || rs.userList.username(key) || 'Unknown'}, `) ), ]), m( @@ -295,7 +390,7 @@ const MessageView = () => { m('.msg-details__info-item', [ m('b', 'Cc: '), Object.keys(MailData.ccList).map((key, index) => - m('p', { key: index }, `${rs.userList.userMap[key]}, `) + m('span', { key: index }, `${UserNicknamesCache[key] || rs.userList.username(key) || 'Unknown'}, `) ), ]), MailData.bccList && @@ -303,7 +398,7 @@ const MessageView = () => { m('.msg-details__info-item', [ m('b', 'Bcc: '), Object.keys(MailData.bccList).map((key, index) => - m('p', { key: index }, `${rs.userList.userMap[key]}, `) + m('span', { key: index }, `${UserNicknamesCache[key] || rs.userList.username(key) || 'Unknown'}, `) ), ]), ]), @@ -316,16 +411,16 @@ const MessageView = () => { m('.msg-view__attachment-items', m(AttachmentSection, { files: MailData.files })), ]), ], - m( + showCompose && m( '.composePopupOverlay#mailComposerPopup', - { style: { display: showCompose ? 'block' : 'none' } }, m( '.composePopup', MailData.sender._addr_string ? m(compose, { - msgType: 'reply', + msgType: composeType, senderId: MailData.sender._addr_string, recipientList: MailData.toList, + ccList: MailData.ccList, subject: MailData.subject, replyMessage: MailData.message, timeStamp: new Date(MailData.timeStamp * 1000), @@ -339,19 +434,172 @@ const MessageView = () => { }; }; +const SortState = { + column: 'date', + direction: 'desc', +}; + +function setSort(column) { + if (SortState.column === column) { + SortState.direction = SortState.direction === 'asc' ? 'desc' : 'asc'; + } else { + SortState.column = column; + SortState.direction = (column === 'date' || column === 'attachments' || column === 'starred') ? 'desc' : 'asc'; + } +} + +function sortList(list) { + if (!list) return []; + return [...list].sort((msgA, msgB) => { + let valA, valB; + switch (SortState.column) { + case 'starred': { + const aStarred = (MessageCache[msgA.msgId]?.msgflags & 0xf00) === RS_MSG_STAR || (msgA.msgflags & 0xf00) === RS_MSG_STAR; + const bStarred = (MessageCache[msgB.msgId]?.msgflags & 0xf00) === RS_MSG_STAR || (msgB.msgflags & 0xf00) === RS_MSG_STAR; + valA = aStarred ? 1 : 0; + valB = bStarred ? 1 : 0; + break; + } + case 'attachments': { + const aCount = MessageCache[msgA.msgId]?.files?.length || msgA.count || 0; + const bCount = MessageCache[msgB.msgId]?.files?.length || msgB.count || 0; + valA = Number(aCount); + valB = Number(bCount); + break; + } + case 'subject': { + const aTitle = MessageCache[msgA.msgId]?.title || msgA.title || ''; + const bTitle = MessageCache[msgB.msgId]?.title || msgB.title || ''; + valA = aTitle.toLowerCase(); + valB = bTitle.toLowerCase(); + break; + } + case 'from': { + const aSenderId = MessageCache[msgA.msgId]?.from?._addr_string || msgA.from?._addr_string; + const bSenderId = MessageCache[msgB.msgId]?.from?._addr_string || msgB.from?._addr_string; + const aFrom = (aSenderId && (UserNicknamesCache[aSenderId] || rs.userList.userMap[aSenderId])) || ''; + const bFrom = (bSenderId && (UserNicknamesCache[bSenderId] || rs.userList.userMap[bSenderId])) || ''; + valA = aFrom.toLowerCase(); + valB = bFrom.toLowerCase(); + break; + } + case 'date': + default: { + const aTs = MessageCache[msgA.msgId]?.ts || msgA.ts?.xint64 || msgA.ts || 0; + const bTs = MessageCache[msgB.msgId]?.ts || msgB.ts?.xint64 || msgB.ts || 0; + valA = Number(aTs); + valB = Number(bTs); + break; + } + } + + if (valA < valB) return SortState.direction === 'asc' ? -1 : 1; + if (valA > valB) return SortState.direction === 'asc' ? 1 : -1; + return 0; + }); +} + const Table = () => { + let currentPage = 0; + const pageSize = 50; return { - view: (v) => - m('table.mails', [ - m('tr', [ - m('th[title=starred]', m('i.fas.fa-star')), - m('th[title=attachments]', m('i.fas.fa-paperclip')), - m('th', 'Subject'), - m('th', 'From'), - m('th', 'Date'), + view: (v) => { + const renderHeader = (colName, label, isIcon = false) => { + const isActive = SortState.column === colName; + const iconClass = isActive + ? (SortState.direction === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down') + : 'fas fa-sort'; + return m( + 'th.sortable-th', + { + onclick: () => setSort(colName), + style: { cursor: 'pointer', userSelect: 'none' }, + }, + [ + isIcon ? label : m('span', label), + ' ', + m(`i.${iconClass}`, { + style: { + marginLeft: '0.25rem', + opacity: isActive ? 1 : 0.2, + transition: 'opacity 0.2s', + }, + }), + ] + ); + }; + + let totalItems = 0; + let tbody = v.children[0]; + if (tbody && tbody.children) { + const flatChildren = Array.isArray(tbody.children) ? tbody.children.flat().filter(Boolean) : [tbody.children].filter(Boolean); + totalItems = flatChildren.length; + + const start = currentPage * pageSize; + const end = start + pageSize; + tbody.children = flatChildren.slice(start, end); + } + + const totalPages = Math.ceil(totalItems / pageSize) || 1; + if (currentPage >= totalPages) currentPage = totalPages - 1; + if (currentPage < 0) currentPage = 0; + + const paginationUI = totalItems > pageSize && m('.pagination', { + style: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + gap: '1rem', + padding: '1rem', + borderTop: '1px solid #eee', + fontSize: '1rem', + color: '#555', + userSelect: 'none' + } + }, [ + m('button', { + disabled: currentPage === 0, + onclick: () => currentPage--, + style: { + padding: '0.4rem 0.8rem', + background: currentPage === 0 ? '#ccc' : '#019dff', + color: '#fff', + border: 'none', + borderRadius: '4px', + cursor: currentPage === 0 ? 'not-allowed' : 'pointer', + boxShadow: 'none' + } + }, m('i.fas.fa-chevron-left')), + m('span.bold', `${totalItems > 0 ? currentPage * pageSize + 1 : 0} - ${Math.min((currentPage + 1) * pageSize, totalItems)} of ${totalItems}`), + m('button', { + disabled: currentPage >= totalPages - 1, + onclick: () => currentPage++, + style: { + padding: '0.4rem 0.8rem', + background: currentPage >= totalPages - 1 ? '#ccc' : '#019dff', + color: '#fff', + border: 'none', + borderRadius: '4px', + cursor: currentPage >= totalPages - 1 ? 'not-allowed' : 'pointer', + boxShadow: 'none' + } + }, m('i.fas.fa-chevron-right')) + ]); + + return m('.table-pagination-container', [ + m('table.mails', [ + m('tr', [ + renderHeader('starred', m('i.fas.fa-star'), true), + renderHeader('attachments', m('i.fas.fa-paperclip'), true), + renderHeader('subject', 'Subject'), + renderHeader('from', 'From'), + renderHeader('date', 'Date'), + ]), + tbody, ]), - v.children, - ]), + paginationUI, + ]); + }, }; }; @@ -376,25 +624,48 @@ const activeSideLink = { quicksideactive: -1, }; +const sidebarIcons = { + inbox: m('i.fas.fa-inbox', { style: 'color: #3b82f6; margin-right: 0.75rem; font-size: 24px; width: 24px; text-align: center;' }), + outbox: m('i.fas.fa-envelope-open-text', { style: 'color: #10b981; margin-right: 0.75rem; font-size: 24px; width: 24px; text-align: center;' }), + drafts: m('i.fas.fa-edit', { style: 'color: #6b7280; margin-right: 0.75rem; font-size: 24px; width: 24px; text-align: center;' }), + sent: m('i.fas.fa-envelope-open', { style: 'color: #f59e0b; margin-right: 0.75rem; font-size: 24px; width: 24px; text-align: center;' }), + trash: m('i.fas.fa-trash-alt', { style: 'color: #ef4444; margin-right: 0.75rem; font-size: 24px; width: 24px; text-align: center;' }), + starred: m('i.fas.fa-star', { style: 'color: #eab308; margin-right: 0.75rem; font-size: 24px; width: 24px; text-align: center;' }), + system: m('i.fas.fa-bell', { style: 'color: #3b82f6; margin-right: 0.75rem; font-size: 24px; width: 24px; text-align: center;' }), + spam: m('i.fas.fa-fire', { style: 'color: #f97316; margin-right: 0.75rem; font-size: 24px; width: 24px; text-align: center;' }), + attachment: m('i.fas.fa-paperclip', { style: 'color: #06b6d4; margin-right: 0.75rem; font-size: 24px; width: 24px; text-align: center;' }), + important: m('i.fas.fa-square', { style: 'color: #ef4444; margin-right: 0.75rem; font-size: 24px; width: 24px; text-align: center;' }), + work: m('i.fas.fa-square', { style: 'color: #f97316; margin-right: 0.75rem; font-size: 24px; width: 24px; text-align: center;' }), + personal: m('i.fas.fa-square', { style: 'color: #22c55e; margin-right: 0.75rem; font-size: 24px; width: 24px; text-align: center;' }), + todo: m('i.fas.fa-square', { style: 'color: #3b82f6; margin-right: 0.75rem; font-size: 24px; width: 24px; text-align: center;' }), + later: m('i.fas.fa-square', { style: 'color: #a855f7; margin-right: 0.75rem; font-size: 24px; width: 24px; text-align: center;' }), +}; + const Sidebar = () => { return { view: ({ attrs: { tabs, baseRoute, size } }) => m( '.sidebar', - tabs.map((panelName, index) => - m( + tabs.map((panelName, index) => { + const displayName = panelName.charAt(0).toUpperCase() + panelName.slice(1); + const labelText = size[panelName] > 0 ? `${displayName} (${size[panelName]})` : displayName; + return m( m.route.Link, { class: index === activeSideLink.sideactive ? 'selected-sidebar-link' : '', + style: 'display: flex; align-items: center;', onclick: () => { activeSideLink.sideactive = index; activeSideLink.quicksideactive = -1; }, href: baseRoute + panelName, }, - size[panelName] > 0 ? `${panelName} (${size[panelName]})` : panelName - ) - ) + [ + sidebarIcons[panelName] || null, + labelText, + ] + ); + }) ), }; }; @@ -406,21 +677,27 @@ const SidebarQuickView = () => { m( '.sidebarquickview', m('h6.bold', 'Quick View'), - tabs.map((panelName, index) => - m( + tabs.map((panelName, index) => { + const displayName = panelName.charAt(0).toUpperCase() + panelName.slice(1); + const labelText = size[panelName] > 0 ? `${displayName} (${size[panelName]})` : displayName; + return m( m.route.Link, { class: index === activeSideLink.quicksideactive ? 'selected-sidebarquickview-link' : '', + style: 'display: flex; align-items: center;', onclick: () => { activeSideLink.quicksideactive = index; activeSideLink.sideactive = -1; }, href: baseRoute + panelName, }, - size[panelName] > 0 ? `${panelName} (${size[panelName]})` : panelName - ) - ) + [ + sidebarIcons[panelName] || null, + labelText, + ] + ); + }) ), }; }; @@ -433,6 +710,9 @@ module.exports = { SearchBar, Sidebar, SidebarQuickView, + SortState, + setSort, + sortList, RS_MSG_BOXMASK, RS_MSG_INBOX, RS_MSG_SENTBOX, diff --git a/webui-src/app/scss/pages/_mail.scss b/webui-src/app/scss/pages/_mail.scss index 9050bc0..6149e30 100644 --- a/webui-src/app/scss/pages/_mail.scss +++ b/webui-src/app/scss/pages/_mail.scss @@ -12,7 +12,7 @@ .compose-mail { &__from { - @include flex($justify: space-between); + @include flex($justify: flex-start, $align: center, $gap: 0.5rem); padding-bottom: 0.5rem; border-bottom: 2px solid $light-color; } @@ -231,6 +231,17 @@ table.mails { cursor: auto; background-color: white; } + + & th.sortable-th { + cursor: pointer; + user-select: none; + transition: background-color 0.2s, color 0.2s; + + &:hover { + background-color: $light-color; + color: darken($light-color, 80%); + } + } } /* hide checkbox */ diff --git a/webui-src/styles.css b/webui-src/styles.css index 8137f9e..121c0dd 100644 --- a/webui-src/styles.css +++ b/webui-src/styles.css @@ -1665,3 +1665,21 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem .attach-modal .modal-buttons button:hover { opacity: 0.9; } + +table.mails th.sortable-th { + cursor: pointer; + user-select: none; + transition: background-color 0.2s, color 0.2s; +} + +table.mails th.sortable-th:hover { + background-color: #eef3f6; + color: #000; +} + +.compose-mail__from { + display: flex; + justify-content: flex-start; + align-items: center; + gap: 0.5rem; +} From 2053a29867dd9e579ae7dab0e9fb108668c9b7de Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Tue, 14 Jul 2026 21:10:23 +0200 Subject: [PATCH 10/40] Added emoji for chats --- webui-src/app/chat/chat.js | 307 ++++++++++++++++++++++++++++++++++--- webui-src/styles.css | 281 ++++++++++++++++++++++++++++++++- 2 files changed, 556 insertions(+), 32 deletions(-) diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index f57a105..c7b8770 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -175,6 +175,27 @@ const ChatRoomsModel = { }, }; +/** + * Wraps emoji characters in a span so CSS can size them independently. + */ +function renderTextWithEmoji(text) { + if (!text) return ''; + // Match emoji sequences (flags, ZWJ sequences, variation selectors, skin tones, etc.) + const emojiRegex = /(?:\p{Emoji_Presentation}|\p{Extended_Pictographic})(?:[\u{1F3FB}-\u{1F3FF}])?(?:\u{FE0F})?(?:\u{20E3})?(?:(?:\u{200D}(?:\p{Emoji_Presentation}|\p{Extended_Pictographic})(?:[\u{1F3FB}-\u{1F3FF}])?(?:\u{FE0F})?)*)/gu; + const parts = []; + let last = 0; + let match; + // eslint-disable-next-line no-cond-assign + while ((match = emojiRegex.exec(text)) !== null) { + if (match[0].length === 0) { emojiRegex.lastIndex++; continue; } + if (match.index > last) parts.push(text.slice(last, match.index)); + parts.push(m('span.chat-emoji', match[0])); + last = match.index + match[0].length; + } + if (last < text.length) parts.push(text.slice(last)); + return parts.length > 0 ? parts : text; +} + /** * Message displays a single Chat-Message
* currently removes formatting and in consequence inline links @@ -247,7 +268,7 @@ const Message = () => { '.message.compact', m('span.datetime', datetime), m('span.username', { style: { color: nickColor } }, username + ':'), - m('span.messagetext', text) + m('span.messagetext', renderTextWithEmoji(text)) ); } @@ -255,7 +276,7 @@ const Message = () => { '.message' + (msg.incoming ? '.incoming' : '.outgoing'), m('span.datetime', datetime), m('span.username', username), - m('span.messagetext', text) + m('span.messagetext', renderTextWithEmoji(text)) ); }, }; @@ -663,8 +684,12 @@ const ChatHubState = { activeMenu: null, showAttachModal: false, attachPath: '', + attachBrowseHint: false, isHashing: false, hashingError: '', + showEmojiPicker: false, + emojiSearch: '', + emojiCategory: 'Smileys', showCreateRoomModal: false, newRoomName: '', newRoomTopic: '', @@ -674,6 +699,142 @@ const ChatHubState = { createRoomError: '', }; +// ========================= Emoji Data ========================= +const EMOJI_CATEGORIES = ['Smileys', 'People', 'Animals', 'Food', 'Travel', 'Activities', 'Objects', 'Symbols']; +const EMOJI_ICONS = { + Smileys: '😊', People: '👥', Animals: '🐾', Food: '🍎', + Travel: '✈️', Activities: '⚽', Objects: '💡', Symbols: '❤️', +}; +const EMOJI_DATA = { + Smileys: [ + '😀','😁','😂','🤣','😃','😄','😅','😆','😉','😊','😋','😎','😍','😘','🥰','😗','😙','😚', + '🙂','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😯','😪','😫','🥱', + '😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','☹️','🙁','😖','😞','😟', + '😤','😢','😭','😦','😧','😨','😩','🤯','😬','😰','😱','🥵','🥶','😳','🤪','😵','😡','😠', + '🤬','😷','🤒','🤕','🤢','🤮','🤧','🥴','😇','🥳','🥺','🤠','🤡','🤥','🤫','🤭','🧐','🤓', + '😈','👿','👹','👺','💀','☠️','👻','👽','👾','🤖','😺','😸','😹','😻','😼','😽','🙀','😿','😾', + ], + People: [ + '👋','🤚','🖐️','✋','🖖','👌','🤌','🤏','✌️','🤞','🤟','🤘','🤙','👈','👉','👆','🖕','👇', + '☝️','👍','👎','✊','👊','🤛','🤜','👏','🙌','👐','🤲','🤝','🙏','✍️','💅','🤳','💪','🦾', + '🦿','🦵','🦶','👂','🦻','👃','🫀','🫁','🧠','🦷','🦴','👀','👁️','👅','👄','🫦','👶','🧒', + '👦','👧','🧑','👱','👨','🧔','👩','🧓','👴','👵','🙍','🙎','🙅','🙆','💁','🙋','🧏','🙇', + '🤦','🤷','👮','🕵️','💂','🥷','👷','🫅','🤴','👸','👲','🧕','🤵','👰','🤰','🫃','🫄','🤱', + '👼','🎅','🤶','🧑‍🎄','🦸','🦹','🧙','🧝','🧛','🧟','🧞','🧜','🧚','🧑‍🤝‍🧑','👫','👬','👭','💏','💑','👪', + ], + Animals: [ + '🐶','🐱','🐭','🐹','🐰','🦊','🐻','🐼','🐻‍❄️','🐨','🐯','🦁','🐮','🐷','🐸','🐵','🙈','🙉', + '🙊','🐒','🦆','🦅','🦉','🦇','🐝','🪱','🐛','🦋','🐌','🐞','🐜','🪲','🦗','🪳','🕷️','🦂', + '🐢','🐍','🦎','🦖','🦕','🐙','🦑','🦐','🦞','🦀','🐡','🐠','🐟','🐬','🐳','🐋','🦈','🦭', + '🐊','🐅','🐆','🦓','🦍','🦧','🦣','🐘','🦛','🦏','🐪','🐫','🦒','🦘','🦬','🐃','🐂','🐄', + '🐎','🐖','🐏','🐑','🦙','🐐','🦌','🐕','🐩','🦮','🐕‍🦺','🐈','🐈‍⬛','🐓','🦃','🦤','🦚','🦜', + '🦢','🦩','🕊️','🐇','🦝','🦨','🦡','🦫','🦦','🦥','🐁','🐀','🐿️','🦔','🐾','🐉','🐲','🌵', + ], + Food: [ + '🍎','🍊','🍋','🍌','🍍','🥭','🍓','🍒','🍑','🥝','🍅','🥥','🥑','🍆','🥔','🥕','🌽','🌶️', + '🫑','🥒','🥬','🥦','🧄','🧅','🍄','🥜','🌰','🍞','🥐','🥖','🫓','🥨','🧀','🥚','🍳','🧈', + '🥞','🧇','🥓','🥩','🍗','🍖','🦴','🌭','🍔','🍟','🍕','🫔','🌮','🌯','🥙','🧆','🥚','🍱', + '🍘','🍙','🍚','🍛','🍜','🍝','🍠','🍢','🍣','🍤','🍥','🥮','🍡','🥟','🥠','🥡','🦪','🍦', + '🍧','🍨','🍩','🍪','🎂','🍰','🧁','🥧','🍫','🍬','🍭','🍮','🍯','🍼','🥛','☕','🫖','🍵', + '🧃','🥤','🧋','🍶','🍺','🍻','🥂','🍷','🥃','🍸','🍹','🧉','🍾','🧊','🥄','🍴','🍽️','🥢', + ], + Travel: [ + '🚗','🚕','🚙','🚌','🚎','🏎️','🚓','🚑','🚒','🚐','🛻','🚚','🚛','🚜','🦯','🦽','🦼','🛺', + '🚲','🛴','🛵','🏍️','🛺','🚨','🚔','🚍','🚘','🚖','🚡','🚠','🚟','🚃','🚋','🚞','🚝','🚄', + '🚅','🚈','🚂','🚆','🚇','🚊','🚉','✈️','🛫','🛬','🛩️','💺','🛸','🚁','🛶','⛵','🚤','🛥️', + '🛳️','⛴️','🚢','⚓','🗺️','🧭','🏔️','⛰️','🌋','🗻','🏕️','🏖️','🏜️','🏝️','🏞️','🏟️','🏛️','🏗️', + '🧱','🪨','🪵','🛖','🏘️','🏚️','🏠','🏡','🏢','🏣','🏤','🏥','🏦','🏨','🏩','🏪','🏫','🏬', + '🏭','🏯','🏰','💒','🗼','🗽','⛪','🕌','🛕','🕍','⛩️','🕋','⛲','⛺','🌁','🌃','🏙️','🌄', + ], + Activities: [ + '⚽','🏀','🏈','⚾','🥎','🎾','🏐','🏉','🥏','🎱','🏓','🏸','🏒','🏑','🥍','🏏','🪃','🥅', + '⛳','🪁','🛝','🏹','🎣','🤿','🥊','🥋','🎽','🛹','🛷','⛸️','🥌','🎿','⛷️','🏂','🪂','🏋️', + '🤼','🤸','⛹️','🤺','🏇','🧘','🏄','🏊','🤽','🚣','🧗','🚵','🚴','🏆','🥇','🥈','🥉','🏅', + '🎖️','🏵️','🎗️','🎫','🎟️','🎪','🤹','🎭','🩰','🎨','🖼️','🎰','🎲','🧩','🪄','🎯','🪅','🎮', + '🕹️','🎳','🎻','🎷','🥁','🪘','🎺','🎸','🪗','🎹','🎵','🎶','🎼','🎤','🎧','📻','🎙️','🎚️', + '🎬','📽️','🎞️','📱','📲','☎️','📞','📟','📠','🔋','🪫','🔌','💡','🔦','🕯️','💸','💵','🪙', + ], + Objects: [ + '⌚','📱','📲','💻','⌨️','🖥️','🖨️','🖱️','🖲️','💾','💿','📀','🧮','📷','📸','📹','🎥','📽️', + '📞','☎️','📟','📠','📺','📻','🧭','⏱️','⏲️','⏰','🕰️','⌛','⏳','📡','🔋','🪫','🔌','💡', + '🔦','🕯️','🪔','🧱','💰','💴','💵','💶','💷','💸','💳','🪙','💹','✉️','📧','📨','📩','📤', + '📥','📦','📫','📪','📬','📭','📮','🗳️','✏️','✒️','🖊️','🖋️','📝','📁','📂','🗂️','📅','📆', + '🗒️','🗓️','📇','📈','📉','📊','📋','📌','📍','🗺️','📏','📐','✂️','🗃️','🗄️','🗑️','🔒','🔓', + '🔏','🔐','🔑','🗝️','🔨','🪓','⛏️','⚒️','🛠️','🗡️','⚔️','🔫','🪃','🏹','🛡️','🪚','🔧','🪛', + ], + Symbols: [ + '❤️','🧡','💛','💚','💙','💜','🖤','🤍','🤎','💔','❣️','💕','💞','💓','💗','💖','💘','💝', + '💟','☮️','✝️','☪️','🕉️','☸️','✡️','🔯','🕎','☯️','☦️','🛐','⛎','♈','♉','♊','♋','♌', + '♍','♎','♏','♐','♑','♒','♓','🆔','⚛️','🉑','☢️','☣️','📴','📳','🈶','🈚','🈸','🈺', + '🈷️','✴️','🆚','💮','🉐','㊙️','㊗️','🈴','🈵','🈹','🈲','🅰️','🅱️','🆎','🆑','🅾️','🆘', + '❌','⭕','🛑','⛔','📛','🚫','💯','💢','♨️','🚷','🚯','🚳','🚱','🔞','📵','🚭','❗','❕', + '❓','❔','‼️','⁉️','🔅','🔆','📶','🛜','📳','📴','🔱','📛','🔰','♻️','✅','🈯','💹','❎', + '🌐','💠','Ⓜ️','🌀','💤','🏧','🚾','♿','🅿️','🛗','🈳','🈹','🚰','🔤','🔡','🔠','🆖','🆗', + '🆙','🆒','🆕','🆓','🔟','📊','🔣','✔️','☑️','🔘','🔲','🔳','⬛','⬜','◼️','◻️','◾','◽', + '▪️','▫️','🔶','🔷','🔸','🔹','🔺','🔻','💠','🔘','🔲','🔳','🏁','🚩','🎌','🏴','🏳️','⭐', + '🌟','💫','✨','🌈','☀️','🌤️','⛅','🌥️','☁️','🌦️','🌧️','⛈️','🌩️','🌨️','❄️','☃️','⛄','🌬️', + ], +}; + +function insertEmojiIntoTextarea(emoji) { + const textarea = document.querySelector('.chat-hub-textarea'); + if (!textarea) return; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const before = textarea.value.substring(0, start); + const after = textarea.value.substring(end); + textarea.value = before + emoji + after; + const newPos = start + emoji.length; + textarea.selectionStart = newPos; + textarea.selectionEnd = newPos; + textarea.focus(); +} + +const EmojiPicker = () => ({ + view: () => { + const search = ChatHubState.emojiSearch.toLowerCase(); + const cat = ChatHubState.emojiCategory; + let emojis; + if (search) { + emojis = Object.values(EMOJI_DATA).flat(); + } else { + emojis = EMOJI_DATA[cat] || []; + } + return m('.emoji-picker', [ + // Search bar + m('.emoji-search-row', [ + m('i.fas.fa-search.emoji-search-icon'), + m('input.emoji-search-input[type=text][placeholder=Search emoji...]', { + value: ChatHubState.emojiSearch, + oninput: (e) => { ChatHubState.emojiSearch = e.target.value; }, + }), + ChatHubState.emojiSearch && m('button.emoji-search-clear', { + onclick: () => { ChatHubState.emojiSearch = ''; }, + }, m('i.fas.fa-times')), + ]), + // Category tabs (hidden while searching) + !search && m('.emoji-categories', EMOJI_CATEGORIES.map(c => + m('button.emoji-cat-btn' + (c === cat ? '.active' : ''), { + title: c, + onclick: () => { ChatHubState.emojiCategory = c; }, + }, EMOJI_ICONS[c]) + )), + // Emoji grid + m('.emoji-grid', + emojis.map(e => + m('button.emoji-btn', { + onclick: () => { + insertEmojiIntoTextarea(e); + ChatHubState.showEmojiPicker = false; + m.redraw(); + }, + }, e) + ) + ), + ]); + }, +}); + function loadOwnChatProfile() { rs.rsJsonApiRequest('/rsConfig/getConfigNetStatus', {}, (data) => { if (data && data.status) { @@ -859,10 +1020,22 @@ function pollHashStatus(localpath) { } const ChatConversationView = () => { + function onDocClick(e) { + if (ChatHubState.showEmojiPicker && !e.target.closest('.emoji-picker-wrapper')) { + ChatHubState.showEmojiPicker = false; + m.redraw(); + } + } return { oninit: () => { scrollChatToBottom(); }, + oncreate: () => { + document.addEventListener('click', onDocClick, true); + }, + onremove: () => { + document.removeEventListener('click', onDocClick, true); + }, view: () => { const chatType = ChatLobbyModel.currentLobby && ChatLobbyModel.currentLobby.chatType; const isRoom = chatType === 3; @@ -881,6 +1054,35 @@ const ChatConversationView = () => { m( '.chat-hub-input-area', [ + m( + 'button.chat-hub-attach-btn', + { + disabled: !canTalk, + style: !canTalk ? 'opacity: 0.5; cursor: not-allowed;' : '', + title: 'Attach file', + onclick: () => { + ChatHubState.showAttachModal = true; + ChatHubState.showEmojiPicker = false; + } + }, + m('i.fas.fa-paperclip') + ), + m('.emoji-picker-wrapper', [ + m( + 'button.chat-hub-emoji-btn', + { + disabled: !canTalk, + style: !canTalk ? 'opacity: 0.5; cursor: not-allowed;' : '', + title: 'Insert emoji', + onclick: (e) => { + e.stopPropagation(); + ChatHubState.showEmojiPicker = !ChatHubState.showEmojiPicker; + }, + }, + '😊' + ), + ChatHubState.showEmojiPicker && m(EmojiPicker), + ]), m('textarea.chat-hub-textarea', { placeholder: canTalk ? 'Type a message... Press Enter to send' : 'Waiting for tunnel to be secured...', disabled: !canTalk, @@ -899,17 +1101,6 @@ const ChatConversationView = () => { } }, }), - m( - 'button.chat-hub-attach-btn', - { - disabled: !canTalk, - style: !canTalk ? 'opacity: 0.5; cursor: not-allowed; margin-right: 0.5rem;' : 'margin-right: 0.5rem;', - onclick: () => { - ChatHubState.showAttachModal = true; - } - }, - m('i.fas.fa-paperclip') - ), m( 'button.chat-hub-send-btn', { @@ -931,29 +1122,94 @@ const ChatConversationView = () => { ), ] ), - ChatHubState.showAttachModal && m('.attach-modal-overlay', [ + ChatHubState.showAttachModal && m('.attach-modal-overlay', { + onclick: (e) => { + if (e.target === e.currentTarget && !ChatHubState.isHashing) { + ChatHubState.showAttachModal = false; + ChatHubState.attachPath = ''; + ChatHubState.attachBrowseHint = false; + ChatHubState.hashingError = ''; + } + } + }, [ m('.attach-modal', [ - m('h4', 'Attach File to Chat'), - m('p', 'Enter the absolute path of the file on your local system:'), - m('input[type=text][placeholder=e.g. C:\\Downloads\\file.zip]', { - value: ChatHubState.attachPath, - oninput: (e) => { ChatHubState.attachPath = e.target.value; }, - disabled: ChatHubState.isHashing, + m('.attach-modal-header', [ + m('i.fas.fa-paperclip.attach-modal-icon'), + m('h4', 'Attach File to Chat'), + ]), + m('p', 'Browse for a file or type the absolute path on your local system:'), + // Hidden native file input for browsing + m('input#attach-file-picker[type=file]', { + style: 'display:none', + onchange: (e) => { + const file = e.target.files && e.target.files[0]; + if (file) { + // file.path is only available in Electron/desktop; browsers restrict full path + const fullPath = file.path; + const hasFullPath = fullPath && (fullPath.includes('/') || fullPath.includes('\\')) && fullPath !== file.name; + if (hasFullPath) { + ChatHubState.attachPath = fullPath; + ChatHubState.attachBrowseHint = false; + } else { + // Browser security: only the filename is available, not the full path + ChatHubState.attachPath = file.name; + ChatHubState.attachBrowseHint = true; + } + // Reset the picker so the same file can be re-selected + e.target.value = ''; + ChatHubState.hashingError = ''; + m.redraw(); + } + }, }), + m('.attach-path-row', [ + m('input[type=text]', { + placeholder: 'e.g. C:\\Downloads\\file.zip', + value: ChatHubState.attachPath, + oninput: (e) => { + ChatHubState.attachPath = e.target.value; + ChatHubState.attachBrowseHint = false; // user is editing manually, hint no longer relevant + }, + disabled: ChatHubState.isHashing, + }), + m('button.attach-browse-btn', { + type: 'button', + disabled: ChatHubState.isHashing, + title: 'Browse for file', + onclick: () => { + const picker = document.getElementById('attach-file-picker'); + if (picker) picker.click(); + }, + }, + [m('i.fas.fa-folder-open'), m('span', ' Browse…')] + ), + ]), + ChatHubState.attachBrowseHint && m('.attach-path-hint', [ + m('i.fas.fa-info-circle'), + m('span', [ + ' Your browser cannot expose the full file path. ', + m('strong', 'Edit the path above'), + ' and add your folder prefix — e.g. change ', + m('code', 'file.zip'), + ' to ', + m('code', 'C:\\Downloads\\file.zip'), + ' — then click Attach.', + ]), + ]), ChatHubState.isHashing && m('.hashing-spinner', [ m('i.fas.fa-spinner.fa-spin'), m('span', ' Hashing file... Please wait.') ]), - ChatHubState.hashingError && m('p.error-text', ChatHubState.hashingError), + !ChatHubState.attachBrowseHint && ChatHubState.hashingError && m('p.error-text', ChatHubState.hashingError), m('.modal-buttons', [ m('button.btn.blue', { - disabled: ChatHubState.isHashing || !ChatHubState.attachPath.trim(), + disabled: ChatHubState.isHashing || !ChatHubState.attachPath.trim() || ChatHubState.attachBrowseHint, onclick: () => { const path = ChatHubState.attachPath.trim(); ChatHubState.isHashing = true; ChatHubState.hashingError = ''; m.redraw(); - + rs.rsJsonApiRequest('/rsFiles/ExtraFileHash', { localpath: path, period: 86400 * 7, @@ -963,17 +1219,18 @@ const ChatConversationView = () => { pollHashStatus(path); } else { ChatHubState.isHashing = false; - ChatHubState.hashingError = 'Failed to initiate file hashing. Check path.'; + ChatHubState.hashingError = 'Failed to initiate file hashing. Check the path and try again.'; m.redraw(); } }); } - }, 'Attach'), + }, [m('i.fas.fa-link'), m('span', ' Attach')]), m('button.btn.red', { disabled: ChatHubState.isHashing, onclick: () => { ChatHubState.showAttachModal = false; ChatHubState.attachPath = ''; + ChatHubState.attachBrowseHint = false; ChatHubState.hashingError = ''; } }, 'Cancel') diff --git a/webui-src/styles.css b/webui-src/styles.css index 121c0dd..0574de1 100644 --- a/webui-src/styles.css +++ b/webui-src/styles.css @@ -1388,7 +1388,7 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem max-width: 70%; padding: 0.625rem 0.875rem; border-radius: 0.75rem; - font-size: 0.925rem; + font-size: 1rem; line-height: 1.4; word-break: break-word; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); @@ -1493,20 +1493,23 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem background-color: #ffffff !important; display: flex !important; flex-direction: column !important; + flex: 1 !important; + overflow-y: auto !important; + min-height: 0 !important; } .chat-hub-messages.compact-container .message.compact, .messages.compact-container .message.compact { display: block !important; max-width: 100% !important; - padding: 0.1rem 0 !important; + padding: 0.15rem 0 !important; border-radius: 0 !important; background-color: transparent !important; border: none !important; box-shadow: none !important; align-self: flex-start !important; - font-size: 0.875rem !important; - line-height: 1.45 !important; + font-size: 1rem !important; + line-height: 1.5 !important; margin: 0 !important; white-space: nowrap !important; overflow: hidden !important; @@ -1551,6 +1554,21 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem word-break: break-word !important; display: inline !important; margin: 0 !important; + font-size: 1rem !important; +} + +/* Make emoji characters render larger than surrounding text in chat */ +.chat-hub-messages .message .messagetext, +.chat-hub-messages.compact-container .message.compact .messagetext, +.messages.compact-container .message.compact .messagetext { + font-family: 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', 'Roboto', Arial, sans-serif; +} + +.chat-emoji { + font-size: 1.45em; + line-height: 1; + vertical-align: -0.15em; + display: inline-block; } /* Fix RetroShare ID textarea - auto-size to content, no scrollbar */ @@ -1569,6 +1587,8 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem color: #64748b; cursor: pointer; padding: 0.5rem; + margin-right: 0.25rem; + flex-shrink: 0; display: flex; align-items: center; justify-content: center; @@ -1606,6 +1626,18 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem gap: 1rem; } +.attach-modal .attach-modal-header { + display: flex; + align-items: center; + gap: 0.6rem; + margin-bottom: 0.25rem; +} + +.attach-modal .attach-modal-icon { + font-size: 1.2rem; + color: #3b82f6; +} + .attach-modal h4 { margin: 0; font-size: 1.2rem; @@ -1618,18 +1650,75 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem color: #475569; } -.attach-modal input[type="text"] { - width: 100%; +.attach-modal .attach-path-row { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.attach-modal .attach-path-row input[type="text"] { + flex: 1; padding: 0.75rem; border: 1px solid #cbd5e1; border-radius: 0.375rem; font-size: 0.9rem; outline: none; transition: border-color 0.2s; + min-width: 0; } -.attach-modal input[type="text"]:focus { +.attach-modal .attach-path-row input[type="text"]:focus { border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); +} + +.attach-browse-btn { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 0.35rem; + padding: 0.625rem 0.9rem; + font-size: 0.875rem; + background-color: #f1f5f9; + color: #334155; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + cursor: pointer; + box-shadow: none; + transition: background-color 0.2s, border-color 0.2s; + white-space: nowrap; +} + +.attach-browse-btn:hover { + background-color: #e2e8f0; + border-color: #94a3b8; +} + +.attach-path-hint { + display: flex; + align-items: flex-start; + gap: 0.5rem; + padding: 0.6rem 0.75rem; + background-color: #fffbeb; + border: 1px solid #fcd34d; + border-left: 3px solid #f59e0b; + border-radius: 0.375rem; + font-size: 0.825rem; + color: #92400e; + line-height: 1.45; +} + +.attach-path-hint i { + color: #f59e0b; + margin-top: 0.1rem; + flex-shrink: 0; +} + +.attach-path-hint code { + font-family: monospace; + background-color: rgba(245, 158, 11, 0.15); + padding: 0.05rem 0.25rem; + border-radius: 0.2rem; } .attach-modal .hashing-spinner { @@ -1666,6 +1755,184 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem opacity: 0.9; } +/* ========================= Emoji Picker ========================= */ +.chat-hub-emoji-btn { + background-color: transparent; + border: none; + font-size: 1.3rem; + cursor: pointer; + padding: 0.35rem 0.4rem; + margin-right: 0.25rem; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.375rem; + line-height: 1; + transition: background-color 0.15s, transform 0.15s; + box-shadow: none; +} + +.chat-hub-emoji-btn:hover { + background-color: #f1f5f9; + transform: scale(1.1); +} + +.emoji-picker-wrapper { + position: relative; + flex-shrink: 0; + display: flex; + align-items: center; +} + +.emoji-picker { + position: absolute; + bottom: calc(100% + 0.5rem); + left: 0; + width: 320px; + background-color: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 0.625rem; + box-shadow: 0 8px 30px -4px rgba(0, 0, 0, 0.18), 0 4px 12px -2px rgba(0, 0, 0, 0.1); + z-index: 3000; + display: flex; + flex-direction: column; + overflow: hidden; + animation: emoji-pop 0.15s ease-out; +} + +@keyframes emoji-pop { + from { opacity: 0; transform: scale(0.92) translateY(6px); } + to { opacity: 1; transform: scale(1) translateY(0); } +} + +.emoji-search-row { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.6rem 0.75rem 0.4rem; + border-bottom: 1px solid #f1f5f9; +} + +.emoji-search-icon { + color: #94a3b8; + font-size: 0.8rem; + flex-shrink: 0; +} + +.emoji-search-input { + flex: 1; + border: 1px solid #e2e8f0; + border-radius: 0.375rem; + padding: 0.3rem 0.5rem; + font-size: 0.85rem; + outline: none; + background-color: #f8fafc; + transition: border-color 0.15s; +} + +.emoji-search-input:focus { + border-color: #3ba4d7; + background-color: #fff; +} + +.emoji-search-clear { + background: none; + border: none; + cursor: pointer; + color: #94a3b8; + padding: 0.2rem; + font-size: 0.8rem; + box-shadow: none; + display: flex; + align-items: center; +} + +.emoji-search-clear:hover { + color: #475569; +} + +.emoji-categories { + display: flex; + gap: 0.1rem; + padding: 0.35rem 0.5rem; + border-bottom: 1px solid #f1f5f9; + overflow-x: auto; + scrollbar-width: none; +} + +.emoji-categories::-webkit-scrollbar { + display: none; +} + +.emoji-cat-btn { + background: none; + border: none; + cursor: pointer; + font-size: 1.2rem; + padding: 0.3rem 0.35rem; + border-radius: 0.375rem; + line-height: 1; + box-shadow: none; + transition: background-color 0.1s; + flex-shrink: 0; +} + +.emoji-cat-btn:hover { + background-color: #f1f5f9; +} + +.emoji-cat-btn.active { + background-color: #e0f2fe; + box-shadow: inset 0 -2px 0 #3ba4d7; +} + +.emoji-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 0; + padding: 0.4rem 0.35rem; + max-height: 220px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: #cbd5e1 transparent; +} + +.emoji-grid::-webkit-scrollbar { + width: 4px; +} + +.emoji-grid::-webkit-scrollbar-track { + background: transparent; +} + +.emoji-grid::-webkit-scrollbar-thumb { + background-color: #cbd5e1; + border-radius: 4px; +} + +.emoji-btn { + background: none; + border: none; + cursor: pointer; + font-size: 1.7rem; + padding: 0.25rem; + border-radius: 0.3rem; + line-height: 1; + box-shadow: none; + text-align: center; + transition: background-color 0.1s, transform 0.1s; + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: 1; +} + +.emoji-btn:hover { + background-color: #f1f5f9; + transform: scale(1.2); +} + table.mails th.sortable-th { cursor: pointer; user-select: none; From 9cb3b9bb4c2e14d7f33148a931ad75fb482f27ac Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:42:35 +0200 Subject: [PATCH 11/40] Added vote feature for chat parcipants Added statusbar --- webui-src/app/chat/chat.js | 418 +++++++++++++++++++++++++++---------- webui-src/app/main.js | 6 +- webui-src/app/statusbar.js | 162 ++++++++++++++ webui-src/styles.css | 48 +++++ 4 files changed, 521 insertions(+), 113 deletions(-) create mode 100644 webui-src/app/statusbar.js diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index c7b8770..dcfb8ae 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -247,6 +247,15 @@ const Message = () => { } } + const isMuted = ChatHubState.mutedUsers && ChatHubState.mutedUsers.has(gxsId); + const details = ChatHubState.gxsDetails[gxsId]; + const opinion = details && details.mReputation ? details.mReputation.mOwnOpinion : 1; + const isBanned = opinion === 0; + + if (isMuted || isBanned) { + return null; + } + let username = rs.userList.username(gxsId) || msg.peerName || '???'; // If we only have the hex ID, try to fallback to the peerName from the message if (username === gxsId && msg.peerName) { @@ -695,8 +704,10 @@ const ChatHubState = { newRoomTopic: '', newRoomIdentity: '', newRoomPublic: true, + newRoomSigned: false, ownGxsIdentities: [], createRoomError: '', + userSortMethod: 'name', }; // ========================= Emoji Data ========================= @@ -1240,109 +1251,160 @@ const ChatConversationView = () => { ]), m('.chat-hub-rightbar', [ m('.rightbar-title', 'Participants'), - m('.rightbar-users-list', ChatLobbyModel.users.map((user) => { - const gxsId = user.key; - const name = user.name; - - // Load details for avatar if not cached - if (gxsId && ChatHubState.gxsDetails[gxsId] === undefined) { - ChatHubState.gxsDetails[gxsId] = null; // Mark as loading - rs.rsJsonApiRequest('/rsIdentity/getIdDetails', { id: gxsId }, (data) => { - if (data && data.details) { - ChatHubState.gxsDetails[gxsId] = data.details; - m.redraw(); - } - }); + m('.rightbar-users-list', (() => { + const sortedUsers = [...ChatLobbyModel.users]; + if (ChatHubState.userSortMethod === 'activity') { + sortedUsers.sort((a, b) => b.lastAct - a.lastAct); + } else { + sortedUsers.sort((a, b) => a.name.localeCompare(b.name)); } + return sortedUsers.map((user) => { + const gxsId = user.key; + const name = user.name; - const details = ChatHubState.gxsDetails[gxsId]; - const avatar = getSafeAvatar(details); - const firstLetter = (name || '?').slice(0, 1).toUpperCase(); - - // Calculate status color and tooltip - const now = Math.floor(Date.now() / 1000); - const tLastAct = user.lastAct || 0; - const isOwn = gxsId === rs.idToHex(ChatLobbyModel.currentLobby.gxs_id || ''); - const isMuted = ChatHubState.mutedUsers && ChatHubState.mutedUsers.has(gxsId); - - let statusColor = '#22c55e'; // active (green) - let statusTooltip = 'Active'; - - if (isMuted) { - statusColor = '#ef4444'; // muted (red) - statusTooltip = 'Muted'; - } else if (isOwn) { - statusColor = '#3ba4d7'; // own identity (blue) - statusTooltip = 'You'; - } else if (tLastAct + 600 < now) { - statusColor = '#cbd5e1'; // inactive > 10 mins (grey) - statusTooltip = 'Inactive'; - } else if (tLastAct + 300 < now) { - statusColor = '#eab308'; // away > 5 mins (yellow) - statusTooltip = 'Away'; - } - - return m('.user', { - onmouseenter: (e) => { - if (ChatHubState.activeMenu) return; // skip tooltip if menu is open - const rect = e.currentTarget.getBoundingClientRect(); - const rightbar = document.querySelector('.chat-hub-rightbar'); - if (rightbar) { - const parentRect = rightbar.getBoundingClientRect(); - const top = rect.top - parentRect.top + rect.height / 2; - ChatHubState.hoveredUser = { gxsId, name, top }; - } - }, - onmouseleave: () => { - ChatHubState.hoveredUser = null; - }, - onclick: (e) => { - e.preventDefault(); - e.stopPropagation(); - ChatHubState.hoveredUser = null; // hide tooltip - - const rect = e.currentTarget.getBoundingClientRect(); - const rightbar = document.querySelector('.chat-hub-rightbar'); - if (rightbar) { - const parentRect = rightbar.getBoundingClientRect(); - const top = rect.bottom - parentRect.top; - if (ChatHubState.activeMenu && ChatHubState.activeMenu.gxsId === gxsId) { - ChatHubState.activeMenu = null; - } else { - ChatHubState.activeMenu = { gxsId, name, top }; + // Load details for avatar if not cached + if (gxsId && ChatHubState.gxsDetails[gxsId] === undefined) { + ChatHubState.gxsDetails[gxsId] = null; // Mark as loading + rs.rsJsonApiRequest('/rsIdentity/getIdDetails', { id: gxsId }, (data) => { + if (data && data.details) { + ChatHubState.gxsDetails[gxsId] = data.details; + m.redraw(); } - m.redraw(); - } - }, - oncontextmenu: (e) => { - e.preventDefault(); - e.stopPropagation(); - ChatHubState.hoveredUser = null; // hide tooltip - - const rect = e.currentTarget.getBoundingClientRect(); - const rightbar = document.querySelector('.chat-hub-rightbar'); - if (rightbar) { - const parentRect = rightbar.getBoundingClientRect(); - const top = rect.bottom - parentRect.top; - ChatHubState.activeMenu = { gxsId, name, top }; - m.redraw(); - } + }); } - }, [ - m(peopleUtil.UserAvatar, { avatar, firstLetter, identityId: gxsId, size: 32 }), - m('span.user-name', name), - statusColor !== '#22c55e' && m('i.fas.fa-circle', { - style: { - color: statusColor, - fontSize: '0.65rem', - marginLeft: 'auto', - flexShrink: 0, - transition: 'color 0.3s ease', + + const details = ChatHubState.gxsDetails[gxsId]; + const avatar = getSafeAvatar(details); + const firstLetter = (name || '?').slice(0, 1).toUpperCase(); + + const opinion = details && details.mReputation ? details.mReputation.mOwnOpinion : 1; + const isBanned = opinion === 0; + if (isBanned) return null; + + // Calculate status color and tooltip + const now = Math.floor(Date.now() / 1000); + const tLastAct = user.lastAct || 0; + const isOwn = gxsId === rs.idToHex(ChatLobbyModel.currentLobby.gxs_id || ''); + const isMuted = ChatHubState.mutedUsers && ChatHubState.mutedUsers.has(gxsId); + + let statusColor = '#22c55e'; // active (green) + let statusTooltip = 'Active'; + + if (isMuted) { + statusColor = '#ef4444'; // muted (red) + statusTooltip = 'Muted'; + } else if (isOwn) { + statusColor = '#3ba4d7'; // own identity (blue) + statusTooltip = 'You'; + } else if (tLastAct + 600 < now) { + statusColor = '#cbd5e1'; // inactive > 10 mins (grey) + statusTooltip = 'Inactive'; + } else if (tLastAct + 300 < now) { + statusColor = '#eab308'; // away > 5 mins (yellow) + statusTooltip = 'Away'; + } + + return m('.user', { + onmouseenter: (e) => { + if (ChatHubState.activeMenu) return; // skip tooltip if menu is open + const rect = e.currentTarget.getBoundingClientRect(); + const rightbar = document.querySelector('.chat-hub-rightbar'); + if (rightbar) { + const parentRect = rightbar.getBoundingClientRect(); + const top = rect.top - parentRect.top + rect.height / 2; + ChatHubState.hoveredUser = { gxsId, name, top }; + } }, - title: statusTooltip - }) - ]); - })), + onmouseleave: () => { + ChatHubState.hoveredUser = null; + }, + onclick: (e) => { + e.preventDefault(); + e.stopPropagation(); + ChatHubState.hoveredUser = null; // hide tooltip + + const rect = e.currentTarget.getBoundingClientRect(); + const rightbar = document.querySelector('.chat-hub-rightbar'); + if (rightbar) { + const parentRect = rightbar.getBoundingClientRect(); + const itemBottom = rect.bottom - parentRect.top; + const estimatedMenuHeight = 310; + let top = itemBottom; + if (itemBottom + estimatedMenuHeight > parentRect.height) { + top = rect.top - parentRect.top - estimatedMenuHeight; + if (top < 10) top = 10; + } + if (ChatHubState.activeMenu && ChatHubState.activeMenu.gxsId === gxsId) { + ChatHubState.activeMenu = null; + } else { + ChatHubState.activeMenu = { gxsId, name, top }; + } + m.redraw(); + } + }, + oncontextmenu: (e) => { + e.preventDefault(); + e.stopPropagation(); + ChatHubState.hoveredUser = null; // hide tooltip + + const rect = e.currentTarget.getBoundingClientRect(); + const rightbar = document.querySelector('.chat-hub-rightbar'); + if (rightbar) { + const parentRect = rightbar.getBoundingClientRect(); + const itemBottom = rect.bottom - parentRect.top; + const estimatedMenuHeight = 310; + let top = itemBottom; + if (itemBottom + estimatedMenuHeight > parentRect.height) { + top = rect.top - parentRect.top - estimatedMenuHeight; + if (top < 10) top = 10; + } + ChatHubState.activeMenu = { gxsId, name, top }; + m.redraw(); + } + } + }, [ + m(peopleUtil.UserAvatar, { avatar, firstLetter, identityId: gxsId, size: 32 }), + m('span.user-name', name), + (() => { + if (isBanned) { + return m('i.fas.fa-ban', { + style: { + color: '#ef4444', + fontSize: '0.85rem', + marginLeft: 'auto', + flexShrink: 0, + }, + title: 'Banned' + }); + } + if (isMuted) { + return m('i.fas.fa-volume-mute', { + style: { + color: '#ef4444', + fontSize: '0.85rem', + marginLeft: 'auto', + flexShrink: 0, + }, + title: 'Muted' + }); + } + if (statusColor !== '#22c55e') { + return m('i.fas.fa-circle', { + style: { + color: statusColor, + fontSize: '0.65rem', + marginLeft: 'auto', + flexShrink: 0, + transition: 'color 0.3s ease', + }, + title: statusTooltip + }); + } + return null; + })() + ]); + }); + })()), ChatHubState.hoveredUser && (() => { const hUser = ChatHubState.hoveredUser; const details = ChatHubState.gxsDetails[hUser.gxsId]; @@ -1392,6 +1454,64 @@ const ChatConversationView = () => { e.stopPropagation(); } }, [ + m('.menu-item', { + onclick: () => { + ChatHubState.userSortMethod = 'activity'; + ChatHubState.activeMenu = null; + m.redraw(); + } + }, [ + m('i.fas.fa-circle', { + style: { + color: '#000', + marginRight: '0.5rem', + fontSize: '0.4rem', + width: '18px', + textAlign: 'center', + visibility: ChatHubState.userSortMethod === 'activity' ? 'visible' : 'hidden' + } + }), + 'Sort by Activity' + ]), + m('.menu-item', { + onclick: () => { + ChatHubState.userSortMethod = 'name'; + ChatHubState.activeMenu = null; + m.redraw(); + } + }, [ + m('i.fas.fa-circle', { + style: { + color: '#000', + marginRight: '0.5rem', + fontSize: '0.4rem', + width: '18px', + textAlign: 'center', + visibility: ChatHubState.userSortMethod === 'name' ? 'visible' : 'hidden' + } + }), + 'Sort by Name' + ]), + m('hr', { style: 'margin: 0.25rem 0; border: none; border-top: 1px solid #e2e8f0;' }), + !isOwn && m('.menu-item', { + onclick: () => { + ChatHubState.activeMenu = null; + people.setSelectedId(menu.gxsId, 'chat'); + } + }, [ + m('i.fas.fa-comments', { style: 'color: #3b82f6; margin-right: 0.5rem; width: 18px; text-align: center;' }), + 'Start private chat' + ]), + !isOwn && m('.menu-item', { + onclick: () => { + ChatHubState.activeMenu = null; + people.setSelectedId(menu.gxsId, 'details', true); + } + }, [ + m('i.fas.fa-envelope', { style: 'color: #10b981; margin-right: 0.5rem; width: 18px; text-align: center;' }), + 'Send Message' + ]), + !isOwn && m('hr', { style: 'margin: 0.25rem 0; border: none; border-top: 1px solid #e2e8f0;' }), !isOwn && m('.menu-item', { onclick: () => { if (isMuted) { @@ -1403,26 +1523,83 @@ const ChatConversationView = () => { m.redraw(); } }, [ - m('i.fas.fa-volume-mute', { style: 'color: #ef4444; margin-right: 0.5rem;' }), + m('i', { + class: isMuted ? 'fas fa-volume-up' : 'fas fa-volume-mute', + style: { + color: isMuted ? '#22c55e' : '#ef4444', + marginRight: '0.5rem', + fontSize: '0.95rem', + width: '18px', + textAlign: 'center' + } + }), isMuted ? 'Unmute participant' : 'Mute participant' ]), !isOwn && m('.menu-item', { onclick: () => { ChatHubState.activeMenu = null; - people.setSelectedId(menu.gxsId, 'chat'); + rs.rsJsonApiRequest('/rsreputations/setOwnOpinion', { id: menu.gxsId, opinion: 2 }, (data, success) => { + if (success) { + if (!ChatHubState.gxsDetails[menu.gxsId]) ChatHubState.gxsDetails[menu.gxsId] = { mReputation: {} }; + if (!ChatHubState.gxsDetails[menu.gxsId].mReputation) ChatHubState.gxsDetails[menu.gxsId].mReputation = {}; + ChatHubState.gxsDetails[menu.gxsId].mReputation.mOwnOpinion = 2; + m.redraw(); + rs.rsJsonApiRequest('/rsIdentity/getIdDetails', { id: menu.gxsId }, (d) => { + if (d && d.details) { + ChatHubState.gxsDetails[menu.gxsId] = d.details; + m.redraw(); + } + }); + } + }); } }, [ - m('i.fas.fa-comments', { style: 'color: #3b82f6; margin-right: 0.5rem;' }), - 'Start private chat' + m('span', { style: 'background-color: #22c55e; border-radius: 50%; width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; margin-right: 0.5rem; font-size: 0.7rem; color: #ffffff;' }, m('i.fas.fa-thumbs-up')), + 'Give positive opinion' ]), !isOwn && m('.menu-item', { onclick: () => { ChatHubState.activeMenu = null; - people.setSelectedId(menu.gxsId, 'details', true); + rs.rsJsonApiRequest('/rsreputations/setOwnOpinion', { id: menu.gxsId, opinion: 1 }, (data, success) => { + if (success) { + if (!ChatHubState.gxsDetails[menu.gxsId]) ChatHubState.gxsDetails[menu.gxsId] = { mReputation: {} }; + if (!ChatHubState.gxsDetails[menu.gxsId].mReputation) ChatHubState.gxsDetails[menu.gxsId].mReputation = {}; + ChatHubState.gxsDetails[menu.gxsId].mReputation.mOwnOpinion = 1; + m.redraw(); + rs.rsJsonApiRequest('/rsIdentity/getIdDetails', { id: menu.gxsId }, (d) => { + if (d && d.details) { + ChatHubState.gxsDetails[menu.gxsId] = d.details; + m.redraw(); + } + }); + } + }); } }, [ - m('i.fas.fa-envelope', { style: 'color: #10b981; margin-right: 0.5rem;' }), - 'Send Message' + m('span', { style: 'background-color: #f59e0b; border-radius: 50%; width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; margin-right: 0.5rem; font-size: 0.7rem; color: #ffffff;' }, m('i.fas.fa-hand-paper')), + 'Give neutral opinion' + ]), + !isOwn && m('.menu-item', { + onclick: () => { + ChatHubState.activeMenu = null; + rs.rsJsonApiRequest('/rsreputations/setOwnOpinion', { id: menu.gxsId, opinion: 0 }, (data, success) => { + if (success) { + if (!ChatHubState.gxsDetails[menu.gxsId]) ChatHubState.gxsDetails[menu.gxsId] = { mReputation: {} }; + if (!ChatHubState.gxsDetails[menu.gxsId].mReputation) ChatHubState.gxsDetails[menu.gxsId].mReputation = {}; + ChatHubState.gxsDetails[menu.gxsId].mReputation.mOwnOpinion = 0; + m.redraw(); + rs.rsJsonApiRequest('/rsIdentity/getIdDetails', { id: menu.gxsId }, (d) => { + if (d && d.details) { + ChatHubState.gxsDetails[menu.gxsId] = d.details; + m.redraw(); + } + }); + } + }); + } + }, [ + m('span', { style: 'background-color: #ef4444; border-radius: 50%; width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; margin-right: 0.5rem; font-size: 0.7rem; color: #ffffff;' }, m('i.fas.fa-thumbs-down')), + 'Ban this person (Sets negative opinion)' ]), m('.menu-item', { onclick: () => { @@ -1430,7 +1607,7 @@ const ChatConversationView = () => { people.setSelectedId(menu.gxsId, 'details'); } }, [ - m('i.fas.fa-user', { style: 'color: #8b5cf6; margin-right: 0.5rem;' }), + m('i.fas.fa-user', { style: 'color: #8b5cf6; margin-right: 0.5rem; width: 18px; text-align: center;' }), 'Show author in people tab' ]) ]); @@ -1803,11 +1980,23 @@ const Layout = { ]), m('.form-field', { style: 'display: flex; gap: 0.5rem; align-items: center; margin-top: 0.75rem;' }, [ - m('input[type=checkbox]', { - checked: ChatHubState.newRoomPublic, - onclick: (e) => { ChatHubState.newRoomPublic = e.target.checked; } - }), - m('label', { style: 'font-size: 0.9rem; color: #475569;' }, 'Public Room') + m('label', { style: 'display: inline-flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; color: #475569; cursor: pointer; user-select: none;' }, [ + m('input[type=checkbox]', { + checked: ChatHubState.newRoomPublic, + onclick: (e) => { ChatHubState.newRoomPublic = e.target.checked; } + }), + 'Public Room' + ]) + ]), + + m('.form-field', { style: 'display: flex; gap: 0.5rem; align-items: center; margin-top: 0.5rem;' }, [ + m('label', { style: 'display: inline-flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; color: #475569; cursor: pointer; user-select: none;' }, [ + m('input[type=checkbox]', { + checked: ChatHubState.newRoomSigned, + onclick: (e) => { ChatHubState.newRoomSigned = e.target.checked; } + }), + 'PGP signed identities' + ]) ]), ChatHubState.createRoomError && m('p.error-text', { style: 'color: #ef4444; font-size: 0.85rem; margin: 0.5rem 0 0 0;' }, ChatHubState.createRoomError), @@ -1820,7 +2009,10 @@ const Layout = { const topic = ChatHubState.newRoomTopic.trim(); const identity = ChatHubState.newRoomIdentity; const isPublic = ChatHubState.newRoomPublic; - const flags = isPublic ? 4 : 0; // RS_CHAT_LOBBY_FLAGS_PUBLIC + const isSigned = ChatHubState.newRoomSigned; + let flags = 0; + if (isPublic) flags |= 4; // RS_CHAT_LOBBY_FLAGS_PUBLIC + if (isSigned) flags |= 8; // RS_CHAT_LOBBY_FLAGS_SIGNED_ONLY rs.rsJsonApiRequest('/rsChats/createChatLobby', { lobby_name: name, @@ -1833,6 +2025,7 @@ const Layout = { ChatHubState.showCreateRoomModal = false; ChatHubState.newRoomName = ''; ChatHubState.newRoomTopic = ''; + ChatHubState.newRoomSigned = false; ChatHubState.createRoomError = ''; // Refresh rooms list ChatRoomsModel.loadSubscribedRooms(); @@ -1849,6 +2042,7 @@ const Layout = { ChatHubState.showCreateRoomModal = false; ChatHubState.newRoomName = ''; ChatHubState.newRoomTopic = ''; + ChatHubState.newRoomSigned = false; ChatHubState.createRoomError = ''; } }, 'Cancel') diff --git a/webui-src/app/main.js b/webui-src/app/main.js index 0f034bc..262160e 100644 --- a/webui-src/app/main.js +++ b/webui-src/app/main.js @@ -12,6 +12,7 @@ const channels = require('channels/channels'); const forums = require('forums/forums'); const boards = require('boards/boards'); const config = require('config/config_resolver'); +const statusbar = require('statusbar'); const navIcon = { home: m('i.fas.fa-home.sidenav-icon'), @@ -127,7 +128,10 @@ const Layout = () => { config: '/config/network', }, }), - m('.tab-content', vnode.children), + m('.main-container', { style: { display: 'flex', flexDirection: 'column', width: '100%', height: '100%', overflow: 'hidden' } }, [ + m('.tab-content', { style: { flex: '1', overflow: 'auto' } }, vnode.children), + m(statusbar) + ]), ]), }; }; diff --git a/webui-src/app/statusbar.js b/webui-src/app/statusbar.js new file mode 100644 index 0000000..e076e5c --- /dev/null +++ b/webui-src/app/statusbar.js @@ -0,0 +1,162 @@ +const m = require('mithril'); +const rs = require('rswebui'); + +const State = { + friendCount: 0, + onlineCount: 0, + dhtActive: false, + dhtOk: false, + dhtRsNetSize: 0, + dhtNetSize: 0, + natState: 1, // BAD_UNKNOWN + firewalled: true, + forwardPort: false, + stunOk: false, + extAddressOk: false, +}; + +function formatUnit(val) { + if (!val) return '0'; + if (val >= 1000000) return (val / 1000000).toFixed(1) + 'M'; + if (val >= 1000) return (val / 1000).toFixed(1) + 'k'; + return val.toString(); +} + +function updateStatus() { + if (!rs.loginKey.isVerified) return; + + // 1. Friends count + rs.rsJsonApiRequest('/rsPeers/getFriendList', {}, (data) => { + if (data && data.sslIds) { + State.friendCount = data.sslIds.length; + } + }); + rs.rsJsonApiRequest('/rsPeers/getOnlineList', {}, (data) => { + if (data && data.sslIds) { + State.onlineCount = data.sslIds.length; + } + }); + + // 2. Net / DHT config status + rs.rsJsonApiRequest('/rsConfig/getConfigNetStatus', {}, (data) => { + if (data && data.status) { + State.dhtActive = data.status.DHTActive; + State.dhtOk = data.status.netDhtOk; + State.dhtRsNetSize = data.status.netDhtRsNetSize; + State.dhtNetSize = data.status.netDhtNetSize; + State.firewalled = data.status.firewalled; + State.forwardPort = data.status.forwardPort; + State.stunOk = data.status.netStunOk; + State.extAddressOk = data.status.netExtAddressOk; + } + }); + + // 3. NAT netState + rs.rsJsonApiRequest('/rsConfig/getNetState', {}, (data) => { + if (data && data.retval !== undefined) { + State.natState = data.retval; + } else { + // Fallback calculation based on getConfigNetStatus + if (State.firewalled && !State.forwardPort) { + State.natState = 6; // WARNING_NATTED + } else { + State.natState = 8; // GOOD + } + } + }); +} + +let intervalId = null; + +const StatusBar = { + oninit() { + updateStatus(); + intervalId = setInterval(updateStatus, 10000); // update every 10s + }, + onremove() { + if (intervalId) { + clearInterval(intervalId); + } + }, + view() { + // DHT Status color & tooltip + let dhtColor = '#94a3b8'; // grey (off) + let dhtTooltip = 'DHT Off'; + if (State.dhtActive) { + if (State.dhtOk) { + if (State.dhtRsNetSize < 10) { + dhtColor = '#eab308'; // yellow (searching) + dhtTooltip = 'DHT Searching for RetroShare Peers'; + } else { + dhtColor = '#22c55e'; // green (good) + dhtTooltip = 'DHT Good'; + } + } else { + dhtColor = '#ef4444'; // red (error) + dhtTooltip = 'No peer found in DHT'; + } + } + + // NAT Status color & tooltip + let natColor = '#94a3b8'; + let natTooltip = 'Offline'; + switch (State.natState) { + case 1: // BAD_UNKNOWN + natColor = '#eab308'; + natTooltip = 'Network Status Unknown'; + break; + case 2: // BAD_OFFLINE + natColor = '#94a3b8'; + natTooltip = 'Offline'; + break; + case 3: // BAD_NATSYM + case 4: // BAD_NODHT_NAT + natColor = '#ef4444'; + natTooltip = State.natState === 4 ? 'DHT Disabled and Firewalled' : 'Nasty Firewall'; + break; + case 5: // WARNING_RESTART + natColor = '#eab308'; + natTooltip = 'Network Restarting'; + break; + case 6: // WARNING_NATTED + natColor = '#eab308'; + natTooltip = 'Behind Firewall'; + break; + case 7: // WARNING_NODHT + natColor = '#eab308'; + natTooltip = 'DHT Disabled'; + break; + case 8: // GOOD + natColor = '#22c55e'; + natTooltip = 'RetroShare Server'; + break; + case 9: // ADV_FORWARD + natColor = '#22c55e'; + natTooltip = 'Forwarded Port'; + break; + } + + return m('.statusbar', [ + m('.statusbar-left', { style: 'display: flex; align-items: center; gap: 0.75rem;' }, [ + m('.statusbar-item', [ + m('i.fas.fa-users', { style: 'margin-right: 0.5rem; color: #94a3b8;' }), + m('span', `Friends: ${State.onlineCount}/${State.friendCount}`), + ]), + m('.statusbar-divider'), + m('.statusbar-item', { title: natTooltip, style: 'cursor: help;' }, [ + m('span', { style: 'margin-right: 0.5rem;' }, 'NAT:'), + m('.status-bullet', { style: { backgroundColor: natColor } }), + ]), + m('.statusbar-divider'), + m('.statusbar-item', { title: dhtTooltip, style: 'cursor: help;' }, [ + m('span', { style: 'margin-right: 0.5rem;' }, 'DHT:'), + m('.status-bullet', { style: { backgroundColor: dhtColor } }), + State.dhtActive && State.dhtOk && m('span', { style: 'margin-left: 0.5rem;' }, `${formatUnit(State.dhtRsNetSize)} (${formatUnit(State.dhtNetSize)})`), + ]), + ]), + m('.statusbar-right'), + ]); + }, +}; + +module.exports = StatusBar; diff --git a/webui-src/styles.css b/webui-src/styles.css index 0574de1..55503f8 100644 --- a/webui-src/styles.css +++ b/webui-src/styles.css @@ -1950,3 +1950,51 @@ table.mails th.sortable-th:hover { align-items: center; gap: 0.5rem; } + +/* Status Bar Styles */ +.statusbar { + display: flex; + justify-content: space-between; + align-items: center; + height: 28px; + background-color: #14141b; + border-top: 1px solid #2e2e38; + padding: 0 1rem; + font-size: 0.8rem; + color: #94a3b8; + z-index: 100; + box-sizing: border-box; + user-select: none; + flex-shrink: 0; +} + +.statusbar-left { + display: flex; + align-items: center; +} + +.statusbar-right { + display: flex; + align-items: center; + gap: 1.5rem; +} + +.statusbar-item { + display: flex; + align-items: center; +} + +.statusbar-divider { + width: 1px; + height: 14px; + background-color: #2e2e38; +} + +.status-bullet { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.5); +} + From e06efd3b0d0b3a5af41c212677595b28ecb4ae12 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Fri, 17 Jul 2026 23:17:34 +0200 Subject: [PATCH 12/40] Added invite button for rooms improve layout net settings Added missed config settings --- webui-src/app/chat/chat.js | 144 ++++++++++++++++++++++--- webui-src/app/config/config_network.js | 68 ++++++------ webui-src/app/config/config_people.js | 21 +++- webui-src/styles.css | 64 +++++++++++ 4 files changed, 248 insertions(+), 49 deletions(-) diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index dcfb8ae..9808a0e 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -708,6 +708,9 @@ const ChatHubState = { ownGxsIdentities: [], createRoomError: '', userSortMethod: 'name', + showInviteModal: false, + friendsList: [], + selectedFriendsToInvite: new Set(), }; // ========================= Emoji Data ========================= @@ -926,6 +929,33 @@ const PublicLobbies = { // ************************* Chat Hub Sub-Components **************************** +function loadFriendsForInvite() { + ChatHubState.friendsList = []; + rs.rsJsonApiRequest('/rsPeers/getFriendList', {}, (data) => { + if (data && data.sslIds) { + data.sslIds.forEach((sslId) => { + rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { sslId }, (detData) => { + if (detData && detData.det) { + rs.rsJsonApiRequest('/rsPeers/isOnline', { sslId }, (onlineData) => { + ChatHubState.friendsList.push({ + id: sslId, + name: detData.det.name, + online: onlineData ? onlineData.retval : false + }); + // Sort online friends first, then alphabetical name + ChatHubState.friendsList.sort((a, b) => { + if (a.online !== b.online) return a.online ? -1 : 1; + return a.name.localeCompare(b.name); + }); + m.redraw(); + }); + } + }); + }); + } + }); +} + const ChatRoomHeader = () => { return { view: (vnode) => { @@ -975,21 +1005,35 @@ const ChatRoomHeader = () => { }, [m('i.fas.fa-sign-out-alt'), ' Leave Chat'] ) - : m( - 'button.red', - { - title: 'Leave Room', - onclick: () => { - ChatLobbyModel.unsubscribeChatLobby(lobbyHexId, () => { - ChatHubState.selectedRoom = null; - ChatHubState.selectedRoomId = null; - ChatHubState.selectedRoomType = null; - m.route.set('/chat'); - }); + : [ + m( + 'button', + { + title: 'Invite friends to this room', + style: 'margin-right: 0.75rem;', + onclick: () => { + ChatHubState.showInviteModal = true; + loadFriendsForInvite(); + } }, - }, - [m('i.fas.fa-sign-out-alt'), ' Leave'] - ), + [m('i.fas.fa-user-plus'), ' Invite'] + ), + m( + 'button.red', + { + title: 'Leave Room', + onclick: () => { + ChatLobbyModel.unsubscribeChatLobby(lobbyHexId, () => { + ChatHubState.selectedRoom = null; + ChatHubState.selectedRoomId = null; + ChatHubState.selectedRoomType = null; + m.route.set('/chat'); + }); + }, + }, + [m('i.fas.fa-sign-out-alt'), ' Leave'] + ) + ], ]), ]); }, @@ -2002,7 +2046,7 @@ const Layout = { ChatHubState.createRoomError && m('p.error-text', { style: 'color: #ef4444; font-size: 0.85rem; margin: 0.5rem 0 0 0;' }, ChatHubState.createRoomError), m('.modal-buttons', { style: 'display: flex; justify-content: flex-end; gap: 0.75rem; margin-top: 1rem;' }, [ - m('button.btn.blue', { + m('button', { disabled: !ChatHubState.newRoomName.trim() || !ChatHubState.newRoomIdentity, onclick: () => { const name = ChatHubState.newRoomName.trim(); @@ -2037,7 +2081,7 @@ const Layout = { }); } }, 'Create'), - m('button.btn.red', { + m('button.red', { onclick: () => { ChatHubState.showCreateRoomModal = false; ChatHubState.newRoomName = ''; @@ -2049,6 +2093,74 @@ const Layout = { ]) ]) ]), + ChatHubState.showInviteModal && m('.attach-modal-overlay', [ + m('.attach-modal', { style: 'max-width: 450px;' }, [ + m('h4', 'Invite Friends to ' + (ChatHubState.selectedRoom ? ChatHubState.selectedRoom.lobby_name : '')), + m('.friends-invite-list', { style: 'max-height: 250px; overflow-y: auto; margin-top: 1rem; border: 1px solid #e2e8f0; border-radius: 0.375rem; padding: 0.5rem;' }, [ + ChatHubState.friendsList.length === 0 + ? m('p', { style: 'text-align: center; color: #64748b; font-style: italic; margin: 1rem 0;' }, 'No friends available') + : ChatHubState.friendsList.map((friend) => { + const isChecked = ChatHubState.selectedFriendsToInvite.has(friend.id); + return m('.friend-invite-item', { + style: 'display: flex; align-items: center; justify-content: space-between; padding: 0.5rem; border-bottom: 1px solid #f1f5f9; cursor: pointer;', + onclick: () => { + if (isChecked) { + ChatHubState.selectedFriendsToInvite.delete(friend.id); + } else { + ChatHubState.selectedFriendsToInvite.add(friend.id); + } + } + }, [ + m('div', { style: 'display: flex; align-items: center; gap: 0.5rem;' }, [ + m('.status-bullet', { style: { backgroundColor: friend.online ? '#22c55e' : '#94a3b8', width: '8px', height: '8px', borderRadius: '50%', display: 'inline-block' } }), + m('span', { style: 'font-weight: 500;' }, friend.name) + ]), + m('input[type=checkbox]', { + checked: isChecked, + onclick: (e) => { + e.stopPropagation(); + if (e.target.checked) { + ChatHubState.selectedFriendsToInvite.add(friend.id); + } else { + ChatHubState.selectedFriendsToInvite.delete(friend.id); + } + } + }) + ]); + }) + ]), + m('.modal-buttons', { style: 'display: flex; justify-content: flex-end; gap: 0.75rem; margin-top: 1.5rem;' }, [ + m('button', { + disabled: ChatHubState.selectedFriendsToInvite.size === 0, + onclick: () => { + const lobbyHexId = rs.idToHex(ChatHubState.selectedRoom.lobby_id); + const invitePromises = []; + ChatHubState.selectedFriendsToInvite.forEach((friendId) => { + invitePromises.push( + new Promise((resolve) => { + rs.rsJsonApiRequest('/rsChats/invitePeerToLobby', { + lobby_id: lobbyHexId, + peer_id: friendId + }, () => resolve()); + }) + ); + }); + Promise.all(invitePromises).then(() => { + ChatHubState.showInviteModal = false; + ChatHubState.selectedFriendsToInvite.clear(); + m.redraw(); + }); + } + }, 'Invite'), + m('button.red', { + onclick: () => { + ChatHubState.showInviteModal = false; + ChatHubState.selectedFriendsToInvite.clear(); + } + }, 'Cancel') + ]) + ]) + ]), ]), m('.chat-hub-right-pane', [ diff --git a/webui-src/app/config/config_network.js b/webui-src/app/config/config_network.js index ffbb2d7..28fbdee 100644 --- a/webui-src/app/config/config_network.js +++ b/webui-src/app/config/config_network.js @@ -328,40 +328,44 @@ const SetSocksProxy = () => { }); }, view: () => - m('.proxy-server', [ + m('.proxy-server-container', [ m( - 'p', + 'p.proxy-description', 'Configure your TOR and I2P SOCKS proxy here. It will allow you to also connect to hidden nodes.' ), - Object.keys(socksProxyObj).map((proxyItem) => { - return m(`.proxy-server__${proxyItem}`, [ - m('h6', `${proxyItem.toUpperCase()} Socks Proxy: `), - m('input[type=text]', { - value: socksProxyObj[proxyItem].addr, - oninput: (e) => (socksProxyObj[proxyItem].addr = e.target.value), - onchange: () => handleProxyChange(proxyItem), - }), - m('input[type=number]', { - value: socksProxyObj[proxyItem].port, - oninput: (e) => (socksProxyObj[proxyItem].port = parseInt(e.target.value)), - onchange: () => handleProxyChange(proxyItem), - }), - socksProxyObj[proxyItem].outgoing !== undefined && - m('.proxy-outgoing', [ - m('.proxy-outgoing__status', { - style: { - backgroundColor: socksProxyObj[proxyItem].outgoing ? '#00dd44' : '#808080', - }, - }), - m( - 'p', - `${proxyItem.toUpperCase()} outgoing ${ - socksProxyObj[proxyItem].outgoing ? 'on' : 'off' - }` - ), - ]), - ]); - }), + m('.proxy-rows-container', + Object.keys(socksProxyObj).map((proxyItem) => { + const isTor = proxyItem === 'tor'; + const labelText = isTor ? 'TOR Socks Proxy:' : 'I2P Socks Proxy:'; + const outgoingText = isTor ? 'TOR outgoing' : 'I2P outgoing'; + const isOutgoing = socksProxyObj[proxyItem].outgoing; + return m('.proxy-row', [ + m('label.proxy-label', labelText), + m('input[type=text].proxy-addr-input', { + value: socksProxyObj[proxyItem].addr, + oninput: (e) => (socksProxyObj[proxyItem].addr = e.target.value), + onchange: () => handleProxyChange(proxyItem), + }), + m('input[type=number].proxy-port-input', { + value: socksProxyObj[proxyItem].port, + oninput: (e) => (socksProxyObj[proxyItem].port = parseInt(e.target.value)), + onchange: () => handleProxyChange(proxyItem), + }), + socksProxyObj[proxyItem].outgoing !== undefined && + m('.proxy-status-container', [ + m('.proxy-status-bullet', { + style: { + backgroundColor: isOutgoing ? '#22c55e' : '#808080', + }, + }), + m( + 'span.proxy-status-text', + `${outgoingText} ${isOutgoing ? 'on' : 'off'}` + ), + ]), + ]); + }) + ), ]), }; }; @@ -397,7 +401,7 @@ const Component = () => { m(displayIPAddresses, { details }), ]), m('.widget__heading', m('h3', 'Hidden Service Configuration')), - m('.widget__body', [m('.grid-2col', [m(SetSocksProxy)])]), + m('.widget__body', [m(SetSocksProxy)]), ]), ]), }; diff --git a/webui-src/app/config/config_people.js b/webui-src/app/config/config_people.js index 979c717..e3c7944 100644 --- a/webui-src/app/config/config_people.js +++ b/webui-src/app/config/config_people.js @@ -5,6 +5,7 @@ const Reputation = () => { let addFriendIdAsContacts = undefined; let usePositiveDefault = undefined; let deleteBannedAfter = undefined; + let rememberBannedAfter = undefined; let negativeThreshold = undefined; let positiveThreshold = undefined; @@ -25,6 +26,11 @@ const Reputation = () => { {}, (data) => (deleteBannedAfter = data.retval) ); + rs.rsJsonApiRequest( + '/rsreputations/rememberBannedIdThreshold', + {}, + (data) => (rememberBannedAfter = data.retval) + ); rs.rsJsonApiRequest( '/rsreputations/thresholdForRemotelyPositiveReputation', {}, @@ -95,7 +101,7 @@ const Reputation = () => { () => {} ), }), - m('p', 'Delete banned identities after(in days, 0 means indefinitely):'), + m('p', 'Delete banned identities after(0 means never):'), m('input[type=number]', { oninput: (e) => (deleteBannedAfter = parseInt(e.target.value)), value: deleteBannedAfter, @@ -108,6 +114,19 @@ const Reputation = () => { () => {} ), }), + m('p', 'Reset reputation of banned identities after (0 means never):'), + m('input[type=number]', { + oninput: (e) => (rememberBannedAfter = parseInt(e.target.value)), + value: rememberBannedAfter, + onchange: () => + rs.rsJsonApiRequest( + '/rsreputations/setRememberBannedIdThreshold', + { + days: rememberBannedAfter, + }, + () => {} + ), + }), ]), ]), ]), diff --git a/webui-src/styles.css b/webui-src/styles.css index 55503f8..cb79ce8 100644 --- a/webui-src/styles.css +++ b/webui-src/styles.css @@ -1998,3 +1998,67 @@ table.mails th.sortable-th:hover { box-shadow: 0 0 4px rgba(0, 0, 0, 0.5); } +/* Hidden Service Configuration layout overrides */ +.proxy-server-container { + width: 100%; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.proxy-description { + color: #334155; + font-size: 0.95rem; + margin-bottom: 0.5rem; +} + +.proxy-rows-container { + display: flex; + flex-direction: column; + gap: 0.75rem; + width: 100%; +} + +.proxy-row { + display: grid; + grid-template-columns: 160px 220px 220px auto; + gap: 0.75rem; + align-items: center; + width: 100%; +} + +.proxy-label { + font-size: 0.95rem; + font-weight: 500; + color: #1e293b; +} + +.proxy-addr-input { + width: 100% !important; + max-width: none !important; +} + +.proxy-port-input { + width: 100% !important; + max-width: none !important; +} + +.proxy-status-container { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.proxy-status-bullet { + width: 14px; + height: 14px; + border-radius: 50%; + display: inline-block; + border: 1px solid #475569; +} + +.proxy-status-text { + font-size: 0.95rem; + color: #1e293b; +} + From 62e9b7846bc437cdcc945fe2e0bc27f045722ffc Mon Sep 17 00:00:00 2001 From: Sumit Kumar Soni Date: Mon, 20 Jul 2026 16:45:45 +0530 Subject: [PATCH 13/40] fix: chat message box scrolling --- webui-src/app/scss/pages/_chat.scss | 6 +- webui-src/styles.css | 2061 +-------------------------- 2 files changed, 5 insertions(+), 2062 deletions(-) diff --git a/webui-src/app/scss/pages/_chat.scss b/webui-src/app/scss/pages/_chat.scss index e70fe09..c39e8d0 100644 --- a/webui-src/app/scss/pages/_chat.scss +++ b/webui-src/app/scss/pages/_chat.scss @@ -1087,8 +1087,8 @@ textarea.chatMsg { line-height: 1.45 !important; margin: 0 !important; white-space: nowrap !important; - overflow: hidden !important; - text-overflow: ellipsis !important; + // overflow: hidden !important; + // text-overflow: ellipsis !important; &:hover { background-color: #f8fafc !important; @@ -1123,4 +1123,4 @@ textarea.chatMsg { } - \ No newline at end of file + diff --git a/webui-src/styles.css b/webui-src/styles.css index cb79ce8..eb029fc 100644 --- a/webui-src/styles.css +++ b/webui-src/styles.css @@ -1,2064 +1,7 @@ -h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem}h5{font-size:1.25rem}h6{font-size:1.125rem}p{font-size:1rem}.small{font-size:.75rem}.bold{font-weight:bold}h1,h2,h3,h4,h5,h6,p{font-weight:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Bold.woff2") format("woff2"),url("./webfonts/Roboto-Bold.woff") format("woff"),url("./webfonts/Roboto-Bold.ttf") format("truetype");font-weight:700;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Bold.woff2") format("woff2"),url("./webfonts/Roboto-Bold.woff") format("woff"),url("./webfonts/Roboto-Bold.ttf") format("truetype");font-weight:bold;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-BoldItalic.woff2") format("woff2"),url("./webfonts/Roboto-BoldItalic.woff") format("woff"),url("./webfonts/Roboto-BoldItalic.ttf") format("truetype");font-weight:700;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-BoldItalic.woff2") format("woff2"),url("./webfonts/Roboto-BoldItalic.woff") format("woff"),url("./webfonts/Roboto-BoldItalic.ttf") format("truetype");font-weight:bold;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Medium.woff2") format("woff2"),url("./webfonts/Roboto-Medium.woff") format("woff"),url("./webfonts/Roboto-Medium.ttf") format("truetype");font-weight:500;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-MediumItalic.woff2") format("woff2"),url("./webfonts/Roboto-MediumItalic.woff") format("woff"),url("./webfonts/Roboto-MediumItalic.ttf") format("truetype");font-weight:500;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Regular.woff2") format("woff2"),url("./webfonts/Roboto-Regular.woff") format("woff"),url("./webfonts/Roboto-Regular.ttf") format("truetype");font-weight:400;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Regular.woff2") format("woff2"),url("./webfonts/Roboto-Regular.woff") format("woff"),url("./webfonts/Roboto-Regular.ttf") format("truetype");font-weight:normal;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Italic.woff2") format("woff2"),url("./webfonts/Roboto-Italic.woff") format("woff"),url("./webfonts/Roboto-Italic.ttf") format("truetype");font-weight:400;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Italic.woff2") format("woff2"),url("./webfonts/Roboto-Italic.woff") format("woff"),url("./webfonts/Roboto-Italic.ttf") format("truetype");font-weight:normal;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Light.woff2") format("woff2"),url("./webfonts/Roboto-Light.woff") format("woff"),url("./webfonts/Roboto-Light.ttf") format("truetype");font-weight:300;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-LightItalic.woff2") format("woff2"),url("./webfonts/Roboto-LightItalic.woff") format("woff"),url("./webfonts/Roboto-LightItalic.ttf") format("truetype");font-weight:300;font-style:italic}/*! +h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem}h5{font-size:1.25rem}h6{font-size:1.125rem}p{font-size:1rem}.small{font-size:.75rem}.bold{font-weight:bold}h1,h2,h3,h4,h5,h6,p{font-weight:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Bold.woff2") format("woff2"),url("./webfonts/Roboto-Bold.woff") format("woff"),url("./webfonts/Roboto-Bold.ttf") format("truetype");font-weight:700;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Bold.woff2") format("woff2"),url("./webfonts/Roboto-Bold.woff") format("woff"),url("./webfonts/Roboto-Bold.ttf") format("truetype");font-weight:bold;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-BoldItalic.woff2") format("woff2"),url("./webfonts/Roboto-BoldItalic.woff") format("woff"),url("./webfonts/Roboto-BoldItalic.ttf") format("truetype");font-weight:700;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-BoldItalic.woff2") format("woff2"),url("./webfonts/Roboto-BoldItalic.woff") format("woff"),url("./webfonts/Roboto-BoldItalic.ttf") format("truetype");font-weight:bold;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Medium.woff2") format("woff2"),url("./webfonts/Roboto-Medium.woff") format("woff"),url("./webfonts/Roboto-Medium.ttf") format("truetype");font-weight:500;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-MediumItalic.woff2") format("woff2"),url("./webfonts/Roboto-MediumItalic.woff") format("woff"),url("./webfonts/Roboto-MediumItalic.ttf") format("truetype");font-weight:500;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Regular.woff2") format("woff2"),url("./webfonts/Roboto-Regular.woff") format("woff"),url("./webfonts/Roboto-Regular.ttf") format("truetype");font-weight:400;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Regular.woff2") format("woff2"),url("./webfonts/Roboto-Regular.woff") format("woff"),url("./webfonts/Roboto-Regular.ttf") format("truetype");font-weight:normal;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Italic.woff2") format("woff2"),url("./webfonts/Roboto-Italic.woff") format("woff"),url("./webfonts/Roboto-Italic.ttf") format("truetype");font-weight:400;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Italic.woff2") format("woff2"),url("./webfonts/Roboto-Italic.woff") format("woff"),url("./webfonts/Roboto-Italic.ttf") format("truetype");font-weight:normal;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Light.woff2") format("woff2"),url("./webfonts/Roboto-Light.woff") format("woff"),url("./webfonts/Roboto-Light.ttf") format("truetype");font-weight:300;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-LightItalic.woff2") format("woff2"),url("./webfonts/Roboto-LightItalic.woff") format("woff"),url("./webfonts/Roboto-LightItalic.ttf") format("truetype");font-weight:300;font-style:italic}/*! * Font Awesome Free 5.9.0 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) */.fa,.fas,.far,.fal,.fab{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-0.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:solid .08em #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fas.fa-pull-left,.far.fa-pull-left,.fal.fa-pull-left,.fab.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fas.fa-pull-right,.far.fa-pull-right,.fal.fa-pull-right,.fab.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);transform:scale(1, -1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(-1, -1);transform:scale(-1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-flip-both{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:""}.fa-accessible-icon:before{content:""}.fa-accusoft:before{content:""}.fa-acquisitions-incorporated:before{content:""}.fa-ad:before{content:""}.fa-address-book:before{content:""}.fa-address-card:before{content:""}.fa-adjust:before{content:""}.fa-adn:before{content:""}.fa-adobe:before{content:""}.fa-adversal:before{content:""}.fa-affiliatetheme:before{content:""}.fa-air-freshener:before{content:""}.fa-airbnb:before{content:""}.fa-algolia:before{content:""}.fa-align-center:before{content:""}.fa-align-justify:before{content:""}.fa-align-left:before{content:""}.fa-align-right:before{content:""}.fa-alipay:before{content:""}.fa-allergies:before{content:""}.fa-amazon:before{content:""}.fa-amazon-pay:before{content:""}.fa-ambulance:before{content:""}.fa-american-sign-language-interpreting:before{content:""}.fa-amilia:before{content:""}.fa-anchor:before{content:""}.fa-android:before{content:""}.fa-angellist:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angry:before{content:""}.fa-angrycreative:before{content:""}.fa-angular:before{content:""}.fa-ankh:before{content:""}.fa-app-store:before{content:""}.fa-app-store-ios:before{content:""}.fa-apper:before{content:""}.fa-apple:before{content:""}.fa-apple-alt:before{content:""}.fa-apple-pay:before{content:""}.fa-archive:before{content:""}.fa-archway:before{content:""}.fa-arrow-alt-circle-down:before{content:""}.fa-arrow-alt-circle-left:before{content:""}.fa-arrow-alt-circle-right:before{content:""}.fa-arrow-alt-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-arrow-circle-left:before{content:""}.fa-arrow-circle-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-down:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrows-alt:before{content:""}.fa-arrows-alt-h:before{content:""}.fa-arrows-alt-v:before{content:""}.fa-artstation:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-asterisk:before{content:""}.fa-asymmetrik:before{content:""}.fa-at:before{content:""}.fa-atlas:before{content:""}.fa-atlassian:before{content:""}.fa-atom:before{content:""}.fa-audible:before{content:""}.fa-audio-description:before{content:""}.fa-autoprefixer:before{content:""}.fa-avianex:before{content:""}.fa-aviato:before{content:""}.fa-award:before{content:""}.fa-aws:before{content:""}.fa-baby:before{content:""}.fa-baby-carriage:before{content:""}.fa-backspace:before{content:""}.fa-backward:before{content:""}.fa-bacon:before{content:""}.fa-balance-scale:before{content:""}.fa-balance-scale-left:before{content:""}.fa-balance-scale-right:before{content:""}.fa-ban:before{content:""}.fa-band-aid:before{content:""}.fa-bandcamp:before{content:""}.fa-barcode:before{content:""}.fa-bars:before{content:""}.fa-baseball-ball:before{content:""}.fa-basketball-ball:before{content:""}.fa-bath:before{content:""}.fa-battery-empty:before{content:""}.fa-battery-full:before{content:""}.fa-battery-half:before{content:""}.fa-battery-quarter:before{content:""}.fa-battery-three-quarters:before{content:""}.fa-battle-net:before{content:""}.fa-bed:before{content:""}.fa-beer:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-bell:before{content:""}.fa-bell-slash:before{content:""}.fa-bezier-curve:before{content:""}.fa-bible:before{content:""}.fa-bicycle:before{content:""}.fa-biking:before{content:""}.fa-bimobject:before{content:""}.fa-binoculars:before{content:""}.fa-biohazard:before{content:""}.fa-birthday-cake:before{content:""}.fa-bitbucket:before{content:""}.fa-bitcoin:before{content:""}.fa-bity:before{content:""}.fa-black-tie:before{content:""}.fa-blackberry:before{content:""}.fa-blender:before{content:""}.fa-blender-phone:before{content:""}.fa-blind:before{content:""}.fa-blog:before{content:""}.fa-blogger:before{content:""}.fa-blogger-b:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-bold:before{content:""}.fa-bolt:before{content:""}.fa-bomb:before{content:""}.fa-bone:before{content:""}.fa-bong:before{content:""}.fa-book:before{content:""}.fa-book-dead:before{content:""}.fa-book-medical:before{content:""}.fa-book-open:before{content:""}.fa-book-reader:before{content:""}.fa-bookmark:before{content:""}.fa-bootstrap:before{content:""}.fa-border-all:before{content:""}.fa-border-none:before{content:""}.fa-border-style:before{content:""}.fa-bowling-ball:before{content:""}.fa-box:before{content:""}.fa-box-open:before{content:""}.fa-boxes:before{content:""}.fa-braille:before{content:""}.fa-brain:before{content:""}.fa-bread-slice:before{content:""}.fa-briefcase:before{content:""}.fa-briefcase-medical:before{content:""}.fa-broadcast-tower:before{content:""}.fa-broom:before{content:""}.fa-brush:before{content:""}.fa-btc:before{content:""}.fa-buffer:before{content:""}.fa-bug:before{content:""}.fa-building:before{content:""}.fa-bullhorn:before{content:""}.fa-bullseye:before{content:""}.fa-burn:before{content:""}.fa-buromobelexperte:before{content:""}.fa-bus:before{content:""}.fa-bus-alt:before{content:""}.fa-business-time:before{content:""}.fa-buysellads:before{content:""}.fa-calculator:before{content:""}.fa-calendar:before{content:""}.fa-calendar-alt:before{content:""}.fa-calendar-check:before{content:""}.fa-calendar-day:before{content:""}.fa-calendar-minus:before{content:""}.fa-calendar-plus:before{content:""}.fa-calendar-times:before{content:""}.fa-calendar-week:before{content:""}.fa-camera:before{content:""}.fa-camera-retro:before{content:""}.fa-campground:before{content:""}.fa-canadian-maple-leaf:before{content:""}.fa-candy-cane:before{content:""}.fa-cannabis:before{content:""}.fa-capsules:before{content:""}.fa-car:before{content:""}.fa-car-alt:before{content:""}.fa-car-battery:before{content:""}.fa-car-crash:before{content:""}.fa-car-side:before{content:""}.fa-caret-down:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-caret-square-down:before{content:""}.fa-caret-square-left:before{content:""}.fa-caret-square-right:before{content:""}.fa-caret-square-up:before{content:""}.fa-caret-up:before{content:""}.fa-carrot:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-cart-plus:before{content:""}.fa-cash-register:before{content:""}.fa-cat:before{content:""}.fa-cc-amazon-pay:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-apple-pay:before{content:""}.fa-cc-diners-club:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-cc-visa:before{content:""}.fa-centercode:before{content:""}.fa-centos:before{content:""}.fa-certificate:before{content:""}.fa-chair:before{content:""}.fa-chalkboard:before{content:""}.fa-chalkboard-teacher:before{content:""}.fa-charging-station:before{content:""}.fa-chart-area:before{content:""}.fa-chart-bar:before{content:""}.fa-chart-line:before{content:""}.fa-chart-pie:before{content:""}.fa-check:before{content:""}.fa-check-circle:before{content:""}.fa-check-double:before{content:""}.fa-check-square:before{content:""}.fa-cheese:before{content:""}.fa-chess:before{content:""}.fa-chess-bishop:before{content:""}.fa-chess-board:before{content:""}.fa-chess-king:before{content:""}.fa-chess-knight:before{content:""}.fa-chess-pawn:before{content:""}.fa-chess-queen:before{content:""}.fa-chess-rook:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-down:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-chevron-up:before{content:""}.fa-child:before{content:""}.fa-chrome:before{content:""}.fa-chromecast:before{content:""}.fa-church:before{content:""}.fa-circle:before{content:""}.fa-circle-notch:before{content:""}.fa-city:before{content:""}.fa-clinic-medical:before{content:""}.fa-clipboard:before{content:""}.fa-clipboard-check:before{content:""}.fa-clipboard-list:before{content:""}.fa-clock:before{content:""}.fa-clone:before{content:""}.fa-closed-captioning:before{content:""}.fa-cloud:before{content:""}.fa-cloud-download-alt:before{content:""}.fa-cloud-meatball:before{content:""}.fa-cloud-moon:before{content:""}.fa-cloud-moon-rain:before{content:""}.fa-cloud-rain:before{content:""}.fa-cloud-showers-heavy:before{content:""}.fa-cloud-sun:before{content:""}.fa-cloud-sun-rain:before{content:""}.fa-cloud-upload-alt:before{content:""}.fa-cloudscale:before{content:""}.fa-cloudsmith:before{content:""}.fa-cloudversify:before{content:""}.fa-cocktail:before{content:""}.fa-code:before{content:""}.fa-code-branch:before{content:""}.fa-codepen:before{content:""}.fa-codiepie:before{content:""}.fa-coffee:before{content:""}.fa-cog:before{content:""}.fa-cogs:before{content:""}.fa-coins:before{content:""}.fa-columns:before{content:""}.fa-comment:before{content:""}.fa-comment-alt:before{content:""}.fa-comment-dollar:before{content:""}.fa-comment-dots:before{content:""}.fa-comment-medical:before{content:""}.fa-comment-slash:before{content:""}.fa-comments:before{content:""}.fa-comments-dollar:before{content:""}.fa-compact-disc:before{content:""}.fa-compass:before{content:""}.fa-compress:before{content:""}.fa-compress-arrows-alt:before{content:""}.fa-concierge-bell:before{content:""}.fa-confluence:before{content:""}.fa-connectdevelop:before{content:""}.fa-contao:before{content:""}.fa-cookie:before{content:""}.fa-cookie-bite:before{content:""}.fa-copy:before{content:""}.fa-copyright:before{content:""}.fa-couch:before{content:""}.fa-cpanel:before{content:""}.fa-creative-commons:before{content:""}.fa-creative-commons-by:before{content:""}.fa-creative-commons-nc:before{content:""}.fa-creative-commons-nc-eu:before{content:""}.fa-creative-commons-nc-jp:before{content:""}.fa-creative-commons-nd:before{content:""}.fa-creative-commons-pd:before{content:""}.fa-creative-commons-pd-alt:before{content:""}.fa-creative-commons-remix:before{content:""}.fa-creative-commons-sa:before{content:""}.fa-creative-commons-sampling:before{content:""}.fa-creative-commons-sampling-plus:before{content:""}.fa-creative-commons-share:before{content:""}.fa-creative-commons-zero:before{content:""}.fa-credit-card:before{content:""}.fa-critical-role:before{content:""}.fa-crop:before{content:""}.fa-crop-alt:before{content:""}.fa-cross:before{content:""}.fa-crosshairs:before{content:""}.fa-crow:before{content:""}.fa-crown:before{content:""}.fa-crutch:before{content:""}.fa-css3:before{content:""}.fa-css3-alt:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-cut:before{content:""}.fa-cuttlefish:before{content:""}.fa-d-and-d:before{content:""}.fa-d-and-d-beyond:before{content:""}.fa-dashcube:before{content:""}.fa-database:before{content:""}.fa-deaf:before{content:""}.fa-delicious:before{content:""}.fa-democrat:before{content:""}.fa-deploydog:before{content:""}.fa-deskpro:before{content:""}.fa-desktop:before{content:""}.fa-dev:before{content:""}.fa-deviantart:before{content:""}.fa-dharmachakra:before{content:""}.fa-dhl:before{content:""}.fa-diagnoses:before{content:""}.fa-diaspora:before{content:""}.fa-dice:before{content:""}.fa-dice-d20:before{content:""}.fa-dice-d6:before{content:""}.fa-dice-five:before{content:""}.fa-dice-four:before{content:""}.fa-dice-one:before{content:""}.fa-dice-six:before{content:""}.fa-dice-three:before{content:""}.fa-dice-two:before{content:""}.fa-digg:before{content:""}.fa-digital-ocean:before{content:""}.fa-digital-tachograph:before{content:""}.fa-directions:before{content:""}.fa-discord:before{content:""}.fa-discourse:before{content:""}.fa-divide:before{content:""}.fa-dizzy:before{content:""}.fa-dna:before{content:""}.fa-dochub:before{content:""}.fa-docker:before{content:""}.fa-dog:before{content:""}.fa-dollar-sign:before{content:""}.fa-dolly:before{content:""}.fa-dolly-flatbed:before{content:""}.fa-donate:before{content:""}.fa-door-closed:before{content:""}.fa-door-open:before{content:""}.fa-dot-circle:before{content:""}.fa-dove:before{content:""}.fa-download:before{content:""}.fa-draft2digital:before{content:""}.fa-drafting-compass:before{content:""}.fa-dragon:before{content:""}.fa-draw-polygon:before{content:""}.fa-dribbble:before{content:""}.fa-dribbble-square:before{content:""}.fa-dropbox:before{content:""}.fa-drum:before{content:""}.fa-drum-steelpan:before{content:""}.fa-drumstick-bite:before{content:""}.fa-drupal:before{content:""}.fa-dumbbell:before{content:""}.fa-dumpster:before{content:""}.fa-dumpster-fire:before{content:""}.fa-dungeon:before{content:""}.fa-dyalog:before{content:""}.fa-earlybirds:before{content:""}.fa-ebay:before{content:""}.fa-edge:before{content:""}.fa-edit:before{content:""}.fa-egg:before{content:""}.fa-eject:before{content:""}.fa-elementor:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-ello:before{content:""}.fa-ember:before{content:""}.fa-empire:before{content:""}.fa-envelope:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-text:before{content:""}.fa-envelope-square:before{content:""}.fa-envira:before{content:""}.fa-equals:before{content:""}.fa-eraser:before{content:""}.fa-erlang:before{content:""}.fa-ethereum:before{content:""}.fa-ethernet:before{content:""}.fa-etsy:before{content:""}.fa-euro-sign:before{content:""}.fa-evernote:before{content:""}.fa-exchange-alt:before{content:""}.fa-exclamation:before{content:""}.fa-exclamation-circle:before{content:""}.fa-exclamation-triangle:before{content:""}.fa-expand:before{content:""}.fa-expand-arrows-alt:before{content:""}.fa-expeditedssl:before{content:""}.fa-external-link-alt:before{content:""}.fa-external-link-square-alt:before{content:""}.fa-eye:before{content:""}.fa-eye-dropper:before{content:""}.fa-eye-slash:before{content:""}.fa-facebook:before{content:""}.fa-facebook-f:before{content:""}.fa-facebook-messenger:before{content:""}.fa-facebook-square:before{content:""}.fa-fan:before{content:""}.fa-fantasy-flight-games:before{content:""}.fa-fast-backward:before{content:""}.fa-fast-forward:before{content:""}.fa-fax:before{content:""}.fa-feather:before{content:""}.fa-feather-alt:before{content:""}.fa-fedex:before{content:""}.fa-fedora:before{content:""}.fa-female:before{content:""}.fa-fighter-jet:before{content:""}.fa-figma:before{content:""}.fa-file:before{content:""}.fa-file-alt:before{content:""}.fa-file-archive:before{content:""}.fa-file-audio:before{content:""}.fa-file-code:before{content:""}.fa-file-contract:before{content:""}.fa-file-csv:before{content:""}.fa-file-download:before{content:""}.fa-file-excel:before{content:""}.fa-file-export:before{content:""}.fa-file-image:before{content:""}.fa-file-import:before{content:""}.fa-file-invoice:before{content:""}.fa-file-invoice-dollar:before{content:""}.fa-file-medical:before{content:""}.fa-file-medical-alt:before{content:""}.fa-file-pdf:before{content:""}.fa-file-powerpoint:before{content:""}.fa-file-prescription:before{content:""}.fa-file-signature:before{content:""}.fa-file-upload:before{content:""}.fa-file-video:before{content:""}.fa-file-word:before{content:""}.fa-fill:before{content:""}.fa-fill-drip:before{content:""}.fa-film:before{content:""}.fa-filter:before{content:""}.fa-fingerprint:before{content:""}.fa-fire:before{content:""}.fa-fire-alt:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-firefox:before{content:""}.fa-first-aid:before{content:""}.fa-first-order:before{content:""}.fa-first-order-alt:before{content:""}.fa-firstdraft:before{content:""}.fa-fish:before{content:""}.fa-fist-raised:before{content:""}.fa-flag:before{content:""}.fa-flag-checkered:before{content:""}.fa-flag-usa:before{content:""}.fa-flask:before{content:""}.fa-flickr:before{content:""}.fa-flipboard:before{content:""}.fa-flushed:before{content:""}.fa-fly:before{content:""}.fa-folder:before{content:""}.fa-folder-minus:before{content:""}.fa-folder-open:before{content:""}.fa-folder-plus:before{content:""}.fa-font:before{content:""}.fa-font-awesome:before{content:""}.fa-font-awesome-alt:before{content:""}.fa-font-awesome-flag:before{content:""}.fa-font-awesome-logo-full:before{content:""}.fa-fonticons:before{content:""}.fa-fonticons-fi:before{content:""}.fa-football-ball:before{content:""}.fa-fort-awesome:before{content:""}.fa-fort-awesome-alt:before{content:""}.fa-forumbee:before{content:""}.fa-forward:before{content:""}.fa-foursquare:before{content:""}.fa-free-code-camp:before{content:""}.fa-freebsd:before{content:""}.fa-frog:before{content:""}.fa-frown:before{content:""}.fa-frown-open:before{content:""}.fa-fulcrum:before{content:""}.fa-funnel-dollar:before{content:""}.fa-futbol:before{content:""}.fa-galactic-republic:before{content:""}.fa-galactic-senate:before{content:""}.fa-gamepad:before{content:""}.fa-gas-pump:before{content:""}.fa-gavel:before{content:""}.fa-gem:before{content:""}.fa-genderless:before{content:""}.fa-get-pocket:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-ghost:before{content:""}.fa-gift:before{content:""}.fa-gifts:before{content:""}.fa-git:before{content:""}.fa-git-alt:before{content:""}.fa-git-square:before{content:""}.fa-github:before{content:""}.fa-github-alt:before{content:""}.fa-github-square:before{content:""}.fa-gitkraken:before{content:""}.fa-gitlab:before{content:""}.fa-gitter:before{content:""}.fa-glass-cheers:before{content:""}.fa-glass-martini:before{content:""}.fa-glass-martini-alt:before{content:""}.fa-glass-whiskey:before{content:""}.fa-glasses:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-globe:before{content:""}.fa-globe-africa:before{content:""}.fa-globe-americas:before{content:""}.fa-globe-asia:before{content:""}.fa-globe-europe:before{content:""}.fa-gofore:before{content:""}.fa-golf-ball:before{content:""}.fa-goodreads:before{content:""}.fa-goodreads-g:before{content:""}.fa-google:before{content:""}.fa-google-drive:before{content:""}.fa-google-play:before{content:""}.fa-google-plus:before{content:""}.fa-google-plus-g:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-wallet:before{content:""}.fa-gopuram:before{content:""}.fa-graduation-cap:before{content:""}.fa-gratipay:before{content:""}.fa-grav:before{content:""}.fa-greater-than:before{content:""}.fa-greater-than-equal:before{content:""}.fa-grimace:before{content:""}.fa-grin:before{content:""}.fa-grin-alt:before{content:""}.fa-grin-beam:before{content:""}.fa-grin-beam-sweat:before{content:""}.fa-grin-hearts:before{content:""}.fa-grin-squint:before{content:""}.fa-grin-squint-tears:before{content:""}.fa-grin-stars:before{content:""}.fa-grin-tears:before{content:""}.fa-grin-tongue:before{content:""}.fa-grin-tongue-squint:before{content:""}.fa-grin-tongue-wink:before{content:""}.fa-grin-wink:before{content:""}.fa-grip-horizontal:before{content:""}.fa-grip-lines:before{content:""}.fa-grip-lines-vertical:before{content:""}.fa-grip-vertical:before{content:""}.fa-gripfire:before{content:""}.fa-grunt:before{content:""}.fa-guitar:before{content:""}.fa-gulp:before{content:""}.fa-h-square:before{content:""}.fa-hacker-news:before{content:""}.fa-hacker-news-square:before{content:""}.fa-hackerrank:before{content:""}.fa-hamburger:before{content:""}.fa-hammer:before{content:""}.fa-hamsa:before{content:""}.fa-hand-holding:before{content:""}.fa-hand-holding-heart:before{content:""}.fa-hand-holding-usd:before{content:""}.fa-hand-lizard:before{content:""}.fa-hand-middle-finger:before{content:""}.fa-hand-paper:before{content:""}.fa-hand-peace:before{content:""}.fa-hand-point-down:before{content:""}.fa-hand-point-left:before{content:""}.fa-hand-point-right:before{content:""}.fa-hand-point-up:before{content:""}.fa-hand-pointer:before{content:""}.fa-hand-rock:before{content:""}.fa-hand-scissors:before{content:""}.fa-hand-spock:before{content:""}.fa-hands:before{content:""}.fa-hands-helping:before{content:""}.fa-handshake:before{content:""}.fa-hanukiah:before{content:""}.fa-hard-hat:before{content:""}.fa-hashtag:before{content:""}.fa-hat-wizard:before{content:""}.fa-haykal:before{content:""}.fa-hdd:before{content:""}.fa-heading:before{content:""}.fa-headphones:before{content:""}.fa-headphones-alt:before{content:""}.fa-headset:before{content:""}.fa-heart:before{content:""}.fa-heart-broken:before{content:""}.fa-heartbeat:before{content:""}.fa-helicopter:before{content:""}.fa-highlighter:before{content:""}.fa-hiking:before{content:""}.fa-hippo:before{content:""}.fa-hips:before{content:""}.fa-hire-a-helper:before{content:""}.fa-history:before{content:""}.fa-hockey-puck:before{content:""}.fa-holly-berry:before{content:""}.fa-home:before{content:""}.fa-hooli:before{content:""}.fa-hornbill:before{content:""}.fa-horse:before{content:""}.fa-horse-head:before{content:""}.fa-hospital:before{content:""}.fa-hospital-alt:before{content:""}.fa-hospital-symbol:before{content:""}.fa-hot-tub:before{content:""}.fa-hotdog:before{content:""}.fa-hotel:before{content:""}.fa-hotjar:before{content:""}.fa-hourglass:before{content:""}.fa-hourglass-end:before{content:""}.fa-hourglass-half:before{content:""}.fa-hourglass-start:before{content:""}.fa-house-damage:before{content:""}.fa-houzz:before{content:""}.fa-hryvnia:before{content:""}.fa-html5:before{content:""}.fa-hubspot:before{content:""}.fa-i-cursor:before{content:""}.fa-ice-cream:before{content:""}.fa-icicles:before{content:""}.fa-icons:before{content:""}.fa-id-badge:before{content:""}.fa-id-card:before{content:""}.fa-id-card-alt:before{content:""}.fa-igloo:before{content:""}.fa-image:before{content:""}.fa-images:before{content:""}.fa-imdb:before{content:""}.fa-inbox:before{content:""}.fa-indent:before{content:""}.fa-industry:before{content:""}.fa-infinity:before{content:""}.fa-info:before{content:""}.fa-info-circle:before{content:""}.fa-instagram:before{content:""}.fa-intercom:before{content:""}.fa-internet-explorer:before{content:""}.fa-invision:before{content:""}.fa-ioxhost:before{content:""}.fa-italic:before{content:""}.fa-itch-io:before{content:""}.fa-itunes:before{content:""}.fa-itunes-note:before{content:""}.fa-java:before{content:""}.fa-jedi:before{content:""}.fa-jedi-order:before{content:""}.fa-jenkins:before{content:""}.fa-jira:before{content:""}.fa-joget:before{content:""}.fa-joint:before{content:""}.fa-joomla:before{content:""}.fa-journal-whills:before{content:""}.fa-js:before{content:""}.fa-js-square:before{content:""}.fa-jsfiddle:before{content:""}.fa-kaaba:before{content:""}.fa-kaggle:before{content:""}.fa-key:before{content:""}.fa-keybase:before{content:""}.fa-keyboard:before{content:""}.fa-keycdn:before{content:""}.fa-khanda:before{content:""}.fa-kickstarter:before{content:""}.fa-kickstarter-k:before{content:""}.fa-kiss:before{content:""}.fa-kiss-beam:before{content:""}.fa-kiss-wink-heart:before{content:""}.fa-kiwi-bird:before{content:""}.fa-korvue:before{content:""}.fa-landmark:before{content:""}.fa-language:before{content:""}.fa-laptop:before{content:""}.fa-laptop-code:before{content:""}.fa-laptop-medical:before{content:""}.fa-laravel:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-laugh:before{content:""}.fa-laugh-beam:before{content:""}.fa-laugh-squint:before{content:""}.fa-laugh-wink:before{content:""}.fa-layer-group:before{content:""}.fa-leaf:before{content:""}.fa-leanpub:before{content:""}.fa-lemon:before{content:""}.fa-less:before{content:""}.fa-less-than:before{content:""}.fa-less-than-equal:before{content:""}.fa-level-down-alt:before{content:""}.fa-level-up-alt:before{content:""}.fa-life-ring:before{content:""}.fa-lightbulb:before{content:""}.fa-line:before{content:""}.fa-link:before{content:""}.fa-linkedin:before{content:""}.fa-linkedin-in:before{content:""}.fa-linode:before{content:""}.fa-linux:before{content:""}.fa-lira-sign:before{content:""}.fa-list:before{content:""}.fa-list-alt:before{content:""}.fa-list-ol:before{content:""}.fa-list-ul:before{content:""}.fa-location-arrow:before{content:""}.fa-lock:before{content:""}.fa-lock-open:before{content:""}.fa-long-arrow-alt-down:before{content:""}.fa-long-arrow-alt-left:before{content:""}.fa-long-arrow-alt-right:before{content:""}.fa-long-arrow-alt-up:before{content:""}.fa-low-vision:before{content:""}.fa-luggage-cart:before{content:""}.fa-lyft:before{content:""}.fa-magento:before{content:""}.fa-magic:before{content:""}.fa-magnet:before{content:""}.fa-mail-bulk:before{content:""}.fa-mailchimp:before{content:""}.fa-male:before{content:""}.fa-mandalorian:before{content:""}.fa-map:before{content:""}.fa-map-marked:before{content:""}.fa-map-marked-alt:before{content:""}.fa-map-marker:before{content:""}.fa-map-marker-alt:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-markdown:before{content:""}.fa-marker:before{content:""}.fa-mars:before{content:""}.fa-mars-double:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mask:before{content:""}.fa-mastodon:before{content:""}.fa-maxcdn:before{content:""}.fa-medal:before{content:""}.fa-medapps:before{content:""}.fa-medium:before{content:""}.fa-medium-m:before{content:""}.fa-medkit:before{content:""}.fa-medrt:before{content:""}.fa-meetup:before{content:""}.fa-megaport:before{content:""}.fa-meh:before{content:""}.fa-meh-blank:before{content:""}.fa-meh-rolling-eyes:before{content:""}.fa-memory:before{content:""}.fa-mendeley:before{content:""}.fa-menorah:before{content:""}.fa-mercury:before{content:""}.fa-meteor:before{content:""}.fa-microchip:before{content:""}.fa-microphone:before{content:""}.fa-microphone-alt:before{content:""}.fa-microphone-alt-slash:before{content:""}.fa-microphone-slash:before{content:""}.fa-microscope:before{content:""}.fa-microsoft:before{content:""}.fa-minus:before{content:""}.fa-minus-circle:before{content:""}.fa-minus-square:before{content:""}.fa-mitten:before{content:""}.fa-mix:before{content:""}.fa-mixcloud:before{content:""}.fa-mizuni:before{content:""}.fa-mobile:before{content:""}.fa-mobile-alt:before{content:""}.fa-modx:before{content:""}.fa-monero:before{content:""}.fa-money-bill:before{content:""}.fa-money-bill-alt:before{content:""}.fa-money-bill-wave:before{content:""}.fa-money-bill-wave-alt:before{content:""}.fa-money-check:before{content:""}.fa-money-check-alt:before{content:""}.fa-monument:before{content:""}.fa-moon:before{content:""}.fa-mortar-pestle:before{content:""}.fa-mosque:before{content:""}.fa-motorcycle:before{content:""}.fa-mountain:before{content:""}.fa-mouse-pointer:before{content:""}.fa-mug-hot:before{content:""}.fa-music:before{content:""}.fa-napster:before{content:""}.fa-neos:before{content:""}.fa-network-wired:before{content:""}.fa-neuter:before{content:""}.fa-newspaper:before{content:""}.fa-nimblr:before{content:""}.fa-node:before{content:""}.fa-node-js:before{content:""}.fa-not-equal:before{content:""}.fa-notes-medical:before{content:""}.fa-npm:before{content:""}.fa-ns8:before{content:""}.fa-nutritionix:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-oil-can:before{content:""}.fa-old-republic:before{content:""}.fa-om:before{content:""}.fa-opencart:before{content:""}.fa-openid:before{content:""}.fa-opera:before{content:""}.fa-optin-monster:before{content:""}.fa-osi:before{content:""}.fa-otter:before{content:""}.fa-outdent:before{content:""}.fa-page4:before{content:""}.fa-pagelines:before{content:""}.fa-pager:before{content:""}.fa-paint-brush:before{content:""}.fa-paint-roller:before{content:""}.fa-palette:before{content:""}.fa-palfed:before{content:""}.fa-pallet:before{content:""}.fa-paper-plane:before{content:""}.fa-paperclip:before{content:""}.fa-parachute-box:before{content:""}.fa-paragraph:before{content:""}.fa-parking:before{content:""}.fa-passport:before{content:""}.fa-pastafarianism:before{content:""}.fa-paste:before{content:""}.fa-patreon:before{content:""}.fa-pause:before{content:""}.fa-pause-circle:before{content:""}.fa-paw:before{content:""}.fa-paypal:before{content:""}.fa-peace:before{content:""}.fa-pen:before{content:""}.fa-pen-alt:before{content:""}.fa-pen-fancy:before{content:""}.fa-pen-nib:before{content:""}.fa-pen-square:before{content:""}.fa-pencil-alt:before{content:""}.fa-pencil-ruler:before{content:""}.fa-penny-arcade:before{content:""}.fa-people-carry:before{content:""}.fa-pepper-hot:before{content:""}.fa-percent:before{content:""}.fa-percentage:before{content:""}.fa-periscope:before{content:""}.fa-person-booth:before{content:""}.fa-phabricator:before{content:""}.fa-phoenix-framework:before{content:""}.fa-phoenix-squadron:before{content:""}.fa-phone:before{content:""}.fa-phone-alt:before{content:""}.fa-phone-slash:before{content:""}.fa-phone-square:before{content:""}.fa-phone-square-alt:before{content:""}.fa-phone-volume:before{content:""}.fa-photo-video:before{content:""}.fa-php:before{content:""}.fa-pied-piper:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-pied-piper-hat:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-piggy-bank:before{content:""}.fa-pills:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-p:before{content:""}.fa-pinterest-square:before{content:""}.fa-pizza-slice:before{content:""}.fa-place-of-worship:before{content:""}.fa-plane:before{content:""}.fa-plane-arrival:before{content:""}.fa-plane-departure:before{content:""}.fa-play:before{content:""}.fa-play-circle:before{content:""}.fa-playstation:before{content:""}.fa-plug:before{content:""}.fa-plus:before{content:""}.fa-plus-circle:before{content:""}.fa-plus-square:before{content:""}.fa-podcast:before{content:""}.fa-poll:before{content:""}.fa-poll-h:before{content:""}.fa-poo:before{content:""}.fa-poo-storm:before{content:""}.fa-poop:before{content:""}.fa-portrait:before{content:""}.fa-pound-sign:before{content:""}.fa-power-off:before{content:""}.fa-pray:before{content:""}.fa-praying-hands:before{content:""}.fa-prescription:before{content:""}.fa-prescription-bottle:before{content:""}.fa-prescription-bottle-alt:before{content:""}.fa-print:before{content:""}.fa-procedures:before{content:""}.fa-product-hunt:before{content:""}.fa-project-diagram:before{content:""}.fa-pushed:before{content:""}.fa-puzzle-piece:before{content:""}.fa-python:before{content:""}.fa-qq:before{content:""}.fa-qrcode:before{content:""}.fa-question:before{content:""}.fa-question-circle:before{content:""}.fa-quidditch:before{content:""}.fa-quinscape:before{content:""}.fa-quora:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-quran:before{content:""}.fa-r-project:before{content:""}.fa-radiation:before{content:""}.fa-radiation-alt:before{content:""}.fa-rainbow:before{content:""}.fa-random:before{content:""}.fa-raspberry-pi:before{content:""}.fa-ravelry:before{content:""}.fa-react:before{content:""}.fa-reacteurope:before{content:""}.fa-readme:before{content:""}.fa-rebel:before{content:""}.fa-receipt:before{content:""}.fa-recycle:before{content:""}.fa-red-river:before{content:""}.fa-reddit:before{content:""}.fa-reddit-alien:before{content:""}.fa-reddit-square:before{content:""}.fa-redhat:before{content:""}.fa-redo:before{content:""}.fa-redo-alt:before{content:""}.fa-registered:before{content:""}.fa-remove-format:before{content:""}.fa-renren:before{content:""}.fa-reply:before{content:""}.fa-reply-all:before{content:""}.fa-replyd:before{content:""}.fa-republican:before{content:""}.fa-researchgate:before{content:""}.fa-resolving:before{content:""}.fa-restroom:before{content:""}.fa-retweet:before{content:""}.fa-rev:before{content:""}.fa-ribbon:before{content:""}.fa-ring:before{content:""}.fa-road:before{content:""}.fa-robot:before{content:""}.fa-rocket:before{content:""}.fa-rocketchat:before{content:""}.fa-rockrms:before{content:""}.fa-route:before{content:""}.fa-rss:before{content:""}.fa-rss-square:before{content:""}.fa-ruble-sign:before{content:""}.fa-ruler:before{content:""}.fa-ruler-combined:before{content:""}.fa-ruler-horizontal:before{content:""}.fa-ruler-vertical:before{content:""}.fa-running:before{content:""}.fa-rupee-sign:before{content:""}.fa-sad-cry:before{content:""}.fa-sad-tear:before{content:""}.fa-safari:before{content:""}.fa-salesforce:before{content:""}.fa-sass:before{content:""}.fa-satellite:before{content:""}.fa-satellite-dish:before{content:""}.fa-save:before{content:""}.fa-schlix:before{content:""}.fa-school:before{content:""}.fa-screwdriver:before{content:""}.fa-scribd:before{content:""}.fa-scroll:before{content:""}.fa-sd-card:before{content:""}.fa-search:before{content:""}.fa-search-dollar:before{content:""}.fa-search-location:before{content:""}.fa-search-minus:before{content:""}.fa-search-plus:before{content:""}.fa-searchengin:before{content:""}.fa-seedling:before{content:""}.fa-sellcast:before{content:""}.fa-sellsy:before{content:""}.fa-server:before{content:""}.fa-servicestack:before{content:""}.fa-shapes:before{content:""}.fa-share:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-share-square:before{content:""}.fa-shekel-sign:before{content:""}.fa-shield-alt:before{content:""}.fa-ship:before{content:""}.fa-shipping-fast:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-shoe-prints:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-shopping-cart:before{content:""}.fa-shopware:before{content:""}.fa-shower:before{content:""}.fa-shuttle-van:before{content:""}.fa-sign:before{content:""}.fa-sign-in-alt:before{content:""}.fa-sign-language:before{content:""}.fa-sign-out-alt:before{content:""}.fa-signal:before{content:""}.fa-signature:before{content:""}.fa-sim-card:before{content:""}.fa-simplybuilt:before{content:""}.fa-sistrix:before{content:""}.fa-sitemap:before{content:""}.fa-sith:before{content:""}.fa-skating:before{content:""}.fa-sketch:before{content:""}.fa-skiing:before{content:""}.fa-skiing-nordic:before{content:""}.fa-skull:before{content:""}.fa-skull-crossbones:before{content:""}.fa-skyatlas:before{content:""}.fa-skype:before{content:""}.fa-slack:before{content:""}.fa-slack-hash:before{content:""}.fa-slash:before{content:""}.fa-sleigh:before{content:""}.fa-sliders-h:before{content:""}.fa-slideshare:before{content:""}.fa-smile:before{content:""}.fa-smile-beam:before{content:""}.fa-smile-wink:before{content:""}.fa-smog:before{content:""}.fa-smoking:before{content:""}.fa-smoking-ban:before{content:""}.fa-sms:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-snowboarding:before{content:""}.fa-snowflake:before{content:""}.fa-snowman:before{content:""}.fa-snowplow:before{content:""}.fa-socks:before{content:""}.fa-solar-panel:before{content:""}.fa-sort:before{content:""}.fa-sort-alpha-down:before{content:""}.fa-sort-alpha-down-alt:before{content:""}.fa-sort-alpha-up:before{content:""}.fa-sort-alpha-up-alt:before{content:""}.fa-sort-amount-down:before{content:""}.fa-sort-amount-down-alt:before{content:""}.fa-sort-amount-up:before{content:""}.fa-sort-amount-up-alt:before{content:""}.fa-sort-down:before{content:""}.fa-sort-numeric-down:before{content:""}.fa-sort-numeric-down-alt:before{content:""}.fa-sort-numeric-up:before{content:""}.fa-sort-numeric-up-alt:before{content:""}.fa-sort-up:before{content:""}.fa-soundcloud:before{content:""}.fa-sourcetree:before{content:""}.fa-spa:before{content:""}.fa-space-shuttle:before{content:""}.fa-speakap:before{content:""}.fa-speaker-deck:before{content:""}.fa-spell-check:before{content:""}.fa-spider:before{content:""}.fa-spinner:before{content:""}.fa-splotch:before{content:""}.fa-spotify:before{content:""}.fa-spray-can:before{content:""}.fa-square:before{content:""}.fa-square-full:before{content:""}.fa-square-root-alt:before{content:""}.fa-squarespace:before{content:""}.fa-stack-exchange:before{content:""}.fa-stack-overflow:before{content:""}.fa-stackpath:before{content:""}.fa-stamp:before{content:""}.fa-star:before{content:""}.fa-star-and-crescent:before{content:""}.fa-star-half:before{content:""}.fa-star-half-alt:before{content:""}.fa-star-of-david:before{content:""}.fa-star-of-life:before{content:""}.fa-staylinked:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-steam-symbol:before{content:""}.fa-step-backward:before{content:""}.fa-step-forward:before{content:""}.fa-stethoscope:before{content:""}.fa-sticker-mule:before{content:""}.fa-sticky-note:before{content:""}.fa-stop:before{content:""}.fa-stop-circle:before{content:""}.fa-stopwatch:before{content:""}.fa-store:before{content:""}.fa-store-alt:before{content:""}.fa-strava:before{content:""}.fa-stream:before{content:""}.fa-street-view:before{content:""}.fa-strikethrough:before{content:""}.fa-stripe:before{content:""}.fa-stripe-s:before{content:""}.fa-stroopwafel:before{content:""}.fa-studiovinari:before{content:""}.fa-stumbleupon:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-subscript:before{content:""}.fa-subway:before{content:""}.fa-suitcase:before{content:""}.fa-suitcase-rolling:before{content:""}.fa-sun:before{content:""}.fa-superpowers:before{content:""}.fa-superscript:before{content:""}.fa-supple:before{content:""}.fa-surprise:before{content:""}.fa-suse:before{content:""}.fa-swatchbook:before{content:""}.fa-swimmer:before{content:""}.fa-swimming-pool:before{content:""}.fa-symfony:before{content:""}.fa-synagogue:before{content:""}.fa-sync:before{content:""}.fa-sync-alt:before{content:""}.fa-syringe:before{content:""}.fa-table:before{content:""}.fa-table-tennis:before{content:""}.fa-tablet:before{content:""}.fa-tablet-alt:before{content:""}.fa-tablets:before{content:""}.fa-tachometer-alt:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-tape:before{content:""}.fa-tasks:before{content:""}.fa-taxi:before{content:""}.fa-teamspeak:before{content:""}.fa-teeth:before{content:""}.fa-teeth-open:before{content:""}.fa-telegram:before{content:""}.fa-telegram-plane:before{content:""}.fa-temperature-high:before{content:""}.fa-temperature-low:before{content:""}.fa-tencent-weibo:before{content:""}.fa-tenge:before{content:""}.fa-terminal:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-th:before{content:""}.fa-th-large:before{content:""}.fa-th-list:before{content:""}.fa-the-red-yeti:before{content:""}.fa-theater-masks:before{content:""}.fa-themeco:before{content:""}.fa-themeisle:before{content:""}.fa-thermometer:before{content:""}.fa-thermometer-empty:before{content:""}.fa-thermometer-full:before{content:""}.fa-thermometer-half:before{content:""}.fa-thermometer-quarter:before{content:""}.fa-thermometer-three-quarters:before{content:""}.fa-think-peaks:before{content:""}.fa-thumbs-down:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbtack:before{content:""}.fa-ticket-alt:before{content:""}.fa-times:before{content:""}.fa-times-circle:before{content:""}.fa-tint:before{content:""}.fa-tint-slash:before{content:""}.fa-tired:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-toilet:before{content:""}.fa-toilet-paper:before{content:""}.fa-toolbox:before{content:""}.fa-tools:before{content:""}.fa-tooth:before{content:""}.fa-torah:before{content:""}.fa-torii-gate:before{content:""}.fa-tractor:before{content:""}.fa-trade-federation:before{content:""}.fa-trademark:before{content:""}.fa-traffic-light:before{content:""}.fa-train:before{content:""}.fa-tram:before{content:""}.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-trash:before{content:""}.fa-trash-alt:before{content:""}.fa-trash-restore:before{content:""}.fa-trash-restore-alt:before{content:""}.fa-tree:before{content:""}.fa-trello:before{content:""}.fa-tripadvisor:before{content:""}.fa-trophy:before{content:""}.fa-truck:before{content:""}.fa-truck-loading:before{content:""}.fa-truck-monster:before{content:""}.fa-truck-moving:before{content:""}.fa-truck-pickup:before{content:""}.fa-tshirt:before{content:""}.fa-tty:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-tv:before{content:""}.fa-twitch:before{content:""}.fa-twitter:before{content:""}.fa-twitter-square:before{content:""}.fa-typo3:before{content:""}.fa-uber:before{content:""}.fa-ubuntu:before{content:""}.fa-uikit:before{content:""}.fa-umbrella:before{content:""}.fa-umbrella-beach:before{content:""}.fa-underline:before{content:""}.fa-undo:before{content:""}.fa-undo-alt:before{content:""}.fa-uniregistry:before{content:""}.fa-universal-access:before{content:""}.fa-university:before{content:""}.fa-unlink:before{content:""}.fa-unlock:before{content:""}.fa-unlock-alt:before{content:""}.fa-untappd:before{content:""}.fa-upload:before{content:""}.fa-ups:before{content:""}.fa-usb:before{content:""}.fa-user:before{content:""}.fa-user-alt:before{content:""}.fa-user-alt-slash:before{content:""}.fa-user-astronaut:before{content:""}.fa-user-check:before{content:""}.fa-user-circle:before{content:""}.fa-user-clock:before{content:""}.fa-user-cog:before{content:""}.fa-user-edit:before{content:""}.fa-user-friends:before{content:""}.fa-user-graduate:before{content:""}.fa-user-injured:before{content:""}.fa-user-lock:before{content:""}.fa-user-md:before{content:""}.fa-user-minus:before{content:""}.fa-user-ninja:before{content:""}.fa-user-nurse:before{content:""}.fa-user-plus:before{content:""}.fa-user-secret:before{content:""}.fa-user-shield:before{content:""}.fa-user-slash:before{content:""}.fa-user-tag:before{content:""}.fa-user-tie:before{content:""}.fa-user-times:before{content:""}.fa-users:before{content:""}.fa-users-cog:before{content:""}.fa-usps:before{content:""}.fa-ussunnah:before{content:""}.fa-utensil-spoon:before{content:""}.fa-utensils:before{content:""}.fa-vaadin:before{content:""}.fa-vector-square:before{content:""}.fa-venus:before{content:""}.fa-venus-double:before{content:""}.fa-venus-mars:before{content:""}.fa-viacoin:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-vial:before{content:""}.fa-vials:before{content:""}.fa-viber:before{content:""}.fa-video:before{content:""}.fa-video-slash:before{content:""}.fa-vihara:before{content:""}.fa-vimeo:before{content:""}.fa-vimeo-square:before{content:""}.fa-vimeo-v:before{content:""}.fa-vine:before{content:""}.fa-vk:before{content:""}.fa-vnv:before{content:""}.fa-voicemail:before{content:""}.fa-volleyball-ball:before{content:""}.fa-volume-down:before{content:""}.fa-volume-mute:before{content:""}.fa-volume-off:before{content:""}.fa-volume-up:before{content:""}.fa-vote-yea:before{content:""}.fa-vr-cardboard:before{content:""}.fa-vuejs:before{content:""}.fa-walking:before{content:""}.fa-wallet:before{content:""}.fa-warehouse:before{content:""}.fa-water:before{content:""}.fa-wave-square:before{content:""}.fa-waze:before{content:""}.fa-weebly:before{content:""}.fa-weibo:before{content:""}.fa-weight:before{content:""}.fa-weight-hanging:before{content:""}.fa-weixin:before{content:""}.fa-whatsapp:before{content:""}.fa-whatsapp-square:before{content:""}.fa-wheelchair:before{content:""}.fa-whmcs:before{content:""}.fa-wifi:before{content:""}.fa-wikipedia-w:before{content:""}.fa-wind:before{content:""}.fa-window-close:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-windows:before{content:""}.fa-wine-bottle:before{content:""}.fa-wine-glass:before{content:""}.fa-wine-glass-alt:before{content:""}.fa-wix:before{content:""}.fa-wizards-of-the-coast:before{content:""}.fa-wolf-pack-battalion:before{content:""}.fa-won-sign:before{content:""}.fa-wordpress:before{content:""}.fa-wordpress-simple:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpexplorer:before{content:""}.fa-wpforms:before{content:""}.fa-wpressr:before{content:""}.fa-wrench:before{content:""}.fa-x-ray:before{content:""}.fa-xbox:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-y-combinator:before{content:""}.fa-yahoo:before{content:""}.fa-yammer:before{content:""}.fa-yandex:before{content:""}.fa-yandex-international:before{content:""}.fa-yarn:before{content:""}.fa-yelp:before{content:""}.fa-yen-sign:before{content:""}.fa-yin-yang:before{content:""}.fa-yoast:before{content:""}.fa-youtube:before{content:""}.fa-youtube-square:before{content:""}.fa-zhihu:before{content:""}.sr-only{border:0;clip:rect(0, 0, 0, 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}/*! * Font Awesome Free 5.9.0 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - */@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:auto;src:url("./webfonts/fa-solid-900.eot");src:url("./webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"),url("./webfonts/fa-solid-900.woff2") format("woff2"),url("./webfonts/fa-solid-900.woff") format("woff"),url("./webfonts/fa-solid-900.ttf") format("truetype"),url("./webfonts/fa-solid-900.svg#fontawesome") format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900}html{font-size:87.5%;box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}body,h1,h2,h3,h4,h5,h6,p,figure,blockquote,dl,dd{margin:0;padding:0}ul[role=list],ol[role=list]{list-style:none}html:focus-within{scroll-behavior:smooth}body{text-rendering:optimizeSpeed;line-height:1.5;font-family:"Roboto",Arial,Helvetica,sans-serif !important;letter-spacing:-0.025ch}a:not([class]){text-decoration-skip-ink:auto}img,picture{max-width:100%;display:block}input,button,textarea,select{font:inherit}@media(prefers-reduced-motion: reduce){html:focus-within{scroll-behavior:auto}*,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important;scroll-behavior:auto !important}}#main{height:100vh}.content{display:flex;height:100%;overflow:hidden}.tab-content{display:flex;height:100%;width:100%;background-color:#eef3f6;animation:fadein .3s;overflow:auto}input[type=text],input[type=password],input[type=number],textarea{box-sizing:border-box;background:#fff;max-width:100%;font-size:1rem;font-weight:400;border:1px solid #ccc;border-radius:.25rem;padding:.25rem .5rem;outline:rgba(0,0,0,0)}input:focus{border:1px solid #3ba4d7;box-shadow:inset 0 0 5px #ccc}input.stretched{width:90%}input.small{max-width:70%;padding:.1rem}input.searchbar{width:40%}a{cursor:pointer}a[title=Back]{width:max-content;height:max-content;padding:.475rem .75rem;border-radius:50%;transition:100ms}a[title=Back]:hover{background:#eef3f6}table{padding:20px;table-layout:fixed;width:100%;border-collapse:collapse;text-align:center;color:#333;font-size:1.125rem}table th{font-size:1.125rem;color:#000;border-bottom:2px solid #eee}table tr{border-bottom:1px solid #eee}h3{color:#444}hr{margin-left:0;color:#aaa}.grid-2col{display:grid;grid-template-columns:auto auto;gap:1rem;justify-content:start}.grid-2col input[type=checkbox]{margin-top:20px}.error{color:red}.tooltip{color:#333;position:relative;display:inline-block;margin:0 .25rem}.tooltiptext{visibility:hidden;position:absolute;top:100%;left:50%;min-width:250px;margin-left:-120px;z-index:1;color:#ccc;background-color:#333;font-size:.875rem;text-align:center;padding:.25rem;border-radius:.5rem}.tooltip:hover .tooltiptext{visibility:visible;animation:fadein .5s}blockquote{color:#14141b;padding:.75rem 1rem .75rem 2rem;border-radius:.25rem}blockquote.info{position:relative;line-height:1.2;color:rgba(20,20,27,.8);border:1px solid rgba(17,143,204,.8)}blockquote.info::before{font-family:"Font Awesome 5 Free";position:absolute;top:.5rem;left:.5rem;content:"";color:#019dff}@keyframes fadein{from{opacity:0}to{opacity:1}}.fadein{animation:fadein .5s}@keyframes swipe-from-left{from{margin-left:100%}to{margin-left:0}}button{width:max-content;height:max-content;color:#fff;background:#019dff;font-size:1rem;padding:.4rem 1rem;border:0;border-radius:5px;cursor:pointer;box-shadow:inset -3px -3px 0 rgb(0,94.5826771654,154)}button:active{outline:none;box-shadow:inset 3px 3px 0 rgb(0,94.5826771654,154)}button.red{width:max-content;height:max-content;color:#fff;background:#ff3a4a;font-size:1rem;padding:.4rem 1rem;border:0;border-radius:5px;cursor:pointer;box-shadow:inset -3px -3px 0 rgb(211,0,17.1370558376)}button.red:active{outline:none;box-shadow:inset 3px 3px 0 rgb(211,0,17.1370558376)}.media-item{display:flex;margin-top:.5rem;padding:1rem;border:1px solid rgba(20,20,27,.1);border-radius:4px}.media-item__details{flex-basis:40%;display:flex;align-items:start;gap:.5rem}.media-item__details img{width:6rem;object-fit:contain}.media-item__desc{flex-basis:60%}.active-link{background:hsla(0,0%,100%,.1) !important}.nav-menu{background-color:#14141b;box-shadow:0 5px 5px #222;display:flex;flex-direction:column;align-items:center;height:100%;padding:.5rem .25rem;margin-right:0rem}.nav-menu__logo{padding:1.2rem 0;display:flex;align-items:center;gap:.3rem}.nav-menu__logo img{width:1.6rem}.nav-menu__logo h5{line-height:1;color:#fff}.nav-menu__box{padding:2rem .125rem;display:flex;flex-direction:column;gap:.5rem;position:relative}.nav-menu__box .item{margin:0;padding:.675rem .5rem;width:10rem;display:flex;align-items:center;line-height:1;border-radius:.5rem;text-decoration:none;color:#ccc;text-transform:capitalize;transition:0ms}.nav-menu__box .item:hover{background-color:rgba(238,243,246,.15)}.nav-menu__box .item i.sidenav-icon{width:2.5rem;height:1.4rem;display:grid;place-items:center}.nav-menu__box .item.item-selected{color:#9bdaff;background-color:rgba(155,218,255,.15);font-weight:medium}.nav-menu__box button.toggle-nav{display:none;position:absolute;padding:0;top:0;right:-1rem;background:rgb(77.5,186.5157480315,255);width:1.5rem;height:1.5rem;aspect-ratio:1;justify-content:center;align-items:center;border-radius:50%;box-shadow:none}.nav-menu.collapsed .nav-menu__logo .logo-container{display:flex;flex-direction:column;align-items:center;gap:.5rem}.nav-menu.collapsed .nav-menu__logo .logo-container>*:not(img){display:block}.nav-menu.collapsed .nav-menu__logo .nav-menu__logo-text{display:none !important}.nav-menu.collapsed .nav-menu__box .item{padding:.675rem 0;width:2.5rem;justify-content:center;transition:300ms}.nav-menu.collapsed .nav-menu__box .item span,.nav-menu.collapsed .nav-menu__box .item p{display:none !important}.nav-menu.collapsed button i{rotate:180deg}.nav-menu:hover button.toggle-nav{display:flex}.sidebar{width:13rem;background-color:#fff;display:flex;flex-direction:column}.sidebar a{text-decoration:none;text-transform:capitalize;padding:1rem;cursor:pointer;color:#999}.sidebar a:hover{color:#222}.sidebar .selected-sidebar-link{font-weight:bold;color:#222;border-left:5px solid #3ba4d7;animation:expand-left-border .1s}.sidebarquickview>h6{padding:.5rem}.sidebarquickview a{text-decoration:none;text-transform:capitalize;padding:.5rem 1rem;display:block;color:#999}.sidebarquickview a a:hover{color:#222}.sidebarquickview .selected-sidebarquickview-link{font-weight:bold;color:#222;border-left:5px solid #3ba4d7;animation:expand-left-border .1s}.node-panel{width:100%;padding:.5rem;animation:fadein .5s}@keyframes expand-left-border{from{border-left:0}to{border-left:5px solid #3ba4d7}}@media(max-width: 700px){.tab-content{flex-direction:column}.sidebar{width:100% !important;flex-direction:row !important;overflow-x:auto !important;overflow-y:hidden !important;white-space:nowrap !important;border-bottom:1px solid rgba(20,20,27,.1) !important;background:#fff !important;z-index:50 !important;flex-shrink:0 !important;height:auto !important;padding:0 !important}.sidebar a{display:inline-block !important;padding:.8rem 1.2rem !important;border-bottom:3px solid rgba(0,0,0,0) !important;border-left:none !important}.sidebar .selected-sidebar-link{border-left:none !important;border-bottom:3px solid #3ba4d7 !important;animation:none !important}.sidebarquickview>h4,.sidebarquickview>h6{display:none !important}}.posts{height:100%;margin-top:1rem;flex-direction:column;overflow:auto}.posts__heading{display:flex;flex-direction:column;justify-content:space-between}.posts-container{height:100%;padding:1rem;display:grid;grid-template-columns:repeat(auto-fill, minmax(150px, 1fr));gap:2rem;border:1px solid rgba(20,20,27,.1);border-radius:4px;overflow:auto}.posts-container-card{min-height:240px;flex-direction:column;border:1px solid rgba(20,20,27,.5);border-radius:4px;cursor:pointer;text-align:center}.posts-container-card img{flex-basis:90%;object-fit:cover}.posts-container-card p{padding:0 .125rem;flex-basis:10%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.progress-bar{width:100%;height:2rem;position:relative;text-align:center;background-color:#eef3f6;border-radius:20px;overflow:hidden}.progress-bar__status{position:absolute;top:0;left:0;height:100%;color:#14141b;background-color:#019dff}.progress-bar__percent{position:absolute;inset:0;margin:auto;width:fit-content;height:fit-content}.progress-bar-chunks{position:relative;margin-top:.5rem;width:100%;height:2rem;display:flex;border-radius:.25rem;overflow:hidden;background-color:#eef3f6}.progress-bar-chunks .chunk{width:100%}.progress-bar-chunks .chunk[data-chunkVal="0"]{background-color:rgba(155,218,255,.2)}.progress-bar-chunks .chunk[data-chunkVal="1"]{background-color:#ff3a4a}.progress-bar-chunks .chunk[data-chunkVal="2"]{background-color:#019dff}.progress-bar-chunks .chunk[data-chunkVal="3"]{background-color:#fcba03}.progress-bar-chunks__percent{position:absolute;inset:0;margin:auto;width:fit-content;height:fit-content}.widget{height:100%;padding:1rem;display:flex;flex-direction:column;gap:.5rem;background-color:#fff;border-radius:.5rem;overflow:auto}.widget .top-heading{display:flex;justify-content:space-between}.widget__heading{display:flex;justify-content:space-between;align-items:center;border-bottom:2px solid #999}.widget__body{height:100%;display:flex;flex-direction:column;overflow:auto}.widget__body-heading{display:flex;justify-content:space-between;align-items:center}.widget__body-heading .action{display:flex;gap:.5rem}.widget__body-content{height:100%;overflow:auto}.widget__body-box{display:flex;flex-direction:column;gap:.5rem}.widget-half{max-width:50%}#modal-container{display:none;position:fixed;z-index:1;height:100%;top:0;left:0;width:100%;background-color:rgba(0,0,0,.2)}.modal-content{position:absolute;color:#555;width:40%;min-height:10rem;height:max-content;padding:1.5rem;inset:0;margin:auto;background-color:#fff;border-radius:.5rem;animation:fadein .5s;display:flex;flex-direction:column}.modal-content button:last-child{margin-top:auto}.modal-content .close-btn{position:absolute;right:1.5rem}.modal-content .widget{padding:0}#notification-container{position:absolute;bottom:0;right:0}.login-page{background-image:linear-gradient(-45deg, rgba(1, 157, 255, 0.75), rgba(17, 143, 204, 0.75));height:100%;animation:fadein .5s}.login-page .login-container{background-color:#fff;box-shadow:3px 3px 5px rgba(20,20,27,.4);margin:auto;position:relative;top:100px;max-width:400px;max-height:500px;border-radius:5px;display:flex;flex-direction:column;align-items:center}.login-page .login-container input{padding:.375rem .75rem;border-radius:.275rem}.login-page .login-container *{margin-bottom:1rem}.login-page .login-container>img{margin:1rem 0 2rem}.login-page .login-container extra{margin:0}.login-page .login-container>a{text-decoration:underline;cursor:pointer}.login-page .extra>label,.login-page .extra>br,.login-page .extra>input{margin-bottom:0}.homepage{margin:2rem auto 0;display:flex;flex-direction:column;gap:4rem}.homepage .logo{display:flex;justify-content:center;align-items:center}.homepage .logo img{width:90px}.homepage .logo .retroshareText{display:flex;flex-direction:column;align-items:center}.homepage .logo .retroshareText .retrotext{font-size:36px;font-weight:600;line-height:1.125}.homepage .logo .retroshareText .retrotext>span{color:#118fcc}.homepage .logo .retroshareText>b{font-size:14px;line-height:1}.homepage .certificate{display:flex;flex-direction:column;gap:4rem}.homepage .certificate__heading{text-align:center}.homepage .certificate__heading>h1{margin-bottom:1rem}.homepage .certificate__content{display:flex;flex-direction:column;gap:2rem;padding:2rem;text-align:center;border:1.5px solid rgba(17,143,204,.2);border-radius:6px;box-shadow:0px 0px 8px 2px rgba(20,20,27,.05)}.homepage .certificate__content .rsId>p{margin-bottom:.5rem;color:#118fcc}.homepage .certificate__content .retroshareID{padding:.25rem;display:flex;align-items:center;justify-self:start;font-size:1.25rem;border-radius:4px;background:rgba(20,20,27,.05)}.homepage .certificate__content .retroshareID .textArea{padding:0;width:100%;min-height:75px;font-size:1rem;font-family:monospace;background:rgba(0,0,0,0);border:none;resize:none}.homepage .certificate__content .retroshareID i{color:#118fcc}.homepage .certificate__content .retroshareID>i{margin:0 .5rem;cursor:pointer}.homepage .certificate__content .webhelp{padding:.5rem;background:#f5f5f5;display:flex;justify-content:center;align-items:center;gap:.5rem;border-radius:4px;border:1px solid rgba(20,20,27,.5);width:fit-content;cursor:pointer}.homepage .certificate__content .webhelp-container{display:grid;place-items:center}.homepage .certificate__content .webhelp:hover{background:#eef3f6;border:1px solid #14141b}.homepage .certificate__content .webhelp>i{font-size:1.2rem;color:green}.homepage .certificate__content .add-friend>h6,.homepage .certificate__content .webhelp-container>h6{font-weight:normal;margin-bottom:.5rem}.friend{color:#444;font-size:1.2em;margin:1rem .5rem;padding:1.5rem;border:1px solid #aaa;border-radius:20px}.friend i{float:left;padding:0 10px;cursor:pointer}.friend h4{margin-bottom:5px}.friend button{font-size:.9em}.friend.hidden{display:none}.friend .brief-info.online{color:green}.friend .location{margin:5px;border-top:1px solid #bbb;display:grid;grid-template-columns:auto auto;justify-content:start}.friend .brief-info{display:flex;align-items:center;justify-self:start}.friend .fa-times-circle{color:#555}.friend .fa-check-circle{color:green}.identity{color:#444;font-size:1.1em;margin:20px;padding:10px;border:1px solid #aaa;border-radius:20px}.identity>h4{margin:5px;font-size:1.3em}.identity button{font-size:.9em}.identity .details{display:grid;grid-template-columns:140px auto;grid-row-gap:5px;justify-content:left}.defaultAvatar{width:3rem;height:3rem;aspect-ratio:1;background:#b0c4de;border-radius:50%;display:grid;place-items:center}.defaultAvatar p{font-weight:900;color:#666f7f;transform:translateY(1px)}img.avatar{display:block;width:3rem;height:max-content;aspect-ratio:1;margin-right:.3em;border-radius:50%}.counter{margin-left:.5em}.counter:before{content:"("}.counter:after{content:")"}.chatInit{margin-left:.5em;color:green;cursor:pointer}.lobby{margin:10px;border:1px solid #aaa;border-radius:20px}.lobby .mainname{margin:20px;font-weight:100;font-size:1.2em}.topic{color:#666}.lobby>.topic{font-size:.95em;margin-left:25px;margin-bottom:5px}.lefttitle{margin-top:15px;margin-bottom:0;font-weight:100;font-size:1.2em}.leftname{margin-top:5px;margin-bottom:5px;padding:5px;font-weight:100;font-size:1em}.leftlobby>.topic{font-size:.75em;margin-left:15px;margin-bottom:5px}.subscribed,.public{cursor:pointer}.leftlobby{border:1px solid #aaa;border-radius:10px;margin-top:5px;background-color:#fff}.leftlobby.selected-lobby,.selectedidentity{color:#fff;background-color:#3ba4d7}.rightbar{position:absolute;width:185px;background-color:#fff;overflow:auto;top:130px;bottom:15px;right:15px}.user{padding:5px}.lobbyName{padding:15px;margin-top:2rem}.lobbies{position:absolute;width:185px;left:165px;bottom:15px;top:130px;overflow:auto}.messages,.setup{position:absolute;background-color:#fff;top:130px;left:360px;right:215px;overflow:auto}.messages{bottom:115px}.messagetext{white-space:break-spaces;margin-right:5px}.message>*{margin-left:5px}.username{color:#006400;font-weight:bolder}.chatMessage{position:absolute;background-color:#fff;height:85px;bottom:15px;right:215px;left:360px}textarea.chatMsg{height:100%;width:100%}.chatatchar{margin-left:.2em;margin-right:.2em;color:silver}.setupicon{margin-left:1em;cursor:pointer}.leaveicon{margin-left:1em;cursor:pointer;color:#d40000}.selectidentity{margin:15px;font-size:1.2em}.setup>.identity{cursor:pointer}.setup{bottom:15px}.createDistantChat{margin-top:1em}.no-lobbies .messages,.no-lobbies .chatMessage,.no-lobbies .setup{left:165px}@media(min-width: 900px){.node-panel.chat-room{display:grid !important;grid-template-columns:250px 1fr 200px !important;grid-template-rows:auto 1fr auto !important;grid-template-areas:"lobbies header rightbar" "lobbies messages rightbar" "lobbies input rightbar" !important;padding:0 !important;height:100% !important}.node-panel.chat-room .lobbyName{grid-area:header;padding:10px;border-bottom:1px solid #eee;margin:0;z-index:10;background:#fff}.node-panel.chat-room .lobbies{grid-area:lobbies;position:static !important;width:auto !important;height:auto !important;border-right:1px solid #ccc;overflow-y:auto;display:block !important;top:auto !important;bottom:auto !important;left:auto !important}.node-panel.chat-room .messages{grid-area:messages;position:static !important;width:auto !important;height:auto !important;overflow-y:auto;padding:10px;left:auto !important;right:auto !important;top:auto !important;bottom:auto !important;margin:0 !important}.node-panel.chat-room .rightbar{grid-area:rightbar;position:static !important;width:auto !important;border-left:1px solid #ccc;overflow-y:auto;display:block !important}.node-panel.chat-room .chatMessage{grid-area:input;position:static !important;width:auto !important;height:auto !important;border-top:1px solid #eee;left:auto !important;right:auto !important;bottom:auto !important;flex:0 0 auto;padding:10px !important;background:#fff;z-index:10}}@media(max-width: 899px){.node-panel.chat-room{display:flex !important;flex-direction:column !important;height:100% !important;position:relative !important}.node-panel.chat-room .lobbyName{flex:0 0 auto}.node-panel.chat-room .messages{flex:1 !important;overflow-y:auto !important;position:relative !important;top:0 !important;bottom:0 !important;left:0 !important;right:0 !important;width:100% !important;height:auto !important;margin:0 !important}.node-panel.chat-room .chatMessage{flex:0 0 auto !important;position:relative !important;bottom:0 !important;left:0 !important;right:0 !important;width:100% !important;height:auto !important;z-index:100}.node-panel.chat-room .rightbar,.node-panel.chat-room .lobbies{display:none !important;position:fixed !important;top:60px !important;bottom:0 !important;width:80% !important;background:#fff !important;z-index:200 !important;box-shadow:2px 0 10px rgba(0,0,0,.2) !important}.node-panel.chat-room.show-lobbies .lobbies{display:block !important;left:0 !important}.node-panel.chat-room.show-users .rightbar{display:block !important;right:0 !important}.chat-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.4);z-index:150}.show-lobbies .chat-overlay,.show-users .chat-overlay{display:block}.mobile-menu-icons{display:flex;gap:15px;font-size:1.2rem}.mobile-menu-icons i{cursor:pointer;padding:5px}}@media(min-width: 900px){.mobile-menu-icons{display:none}}.side-bar{display:flex;flex-direction:column;background:#fff}.side-bar .mail-compose-btn{width:96%;margin:.25rem;padding:.75rem 0}.compose-mail__from{display:flex;justify-content:space-between;padding-bottom:.5rem;border-bottom:2px solid #eef3f6}.compose-mail__recipients{padding:.5rem 0;display:flex;flex-direction:column;gap:.5rem;border-bottom:2px solid #eef3f6}.compose-mail__recipients__container{display:flex;gap:.5rem}.compose-mail__recipients__container>label{text-transform:capitalize}.compose-mail__recipients__container .recipients{width:100%;display:flex;gap:.5rem;flex-wrap:wrap}.compose-mail__recipients__container .recipients__selected{padding:.125rem .5rem;display:flex;align-items:center;gap:.5rem;border:1px solid #eef3f6;border-radius:3px;cursor:default}.compose-mail__recipients__container .recipients__selected i{cursor:pointer;padding:.25rem}.compose-mail__recipients__container .recipients__input{display:flex;position:relative;flex-grow:1}.compose-mail__recipients__container .recipients__input-field{flex-grow:1;min-width:200px;padding:0;border:none;box-shadow:none}.compose-mail__recipients__container .recipients__input-field:focus+.recipients__input-list{display:flex}.compose-mail__recipients__container .recipients__input-list{z-index:1;position:absolute;top:1rem;padding:0;width:100%;max-height:15rem;flex-direction:column;overflow:auto;display:none;background:#fff;border-top:1px solid #eef3f6;border-bottom:1px solid #eef3f6}.compose-mail__recipients__container .recipients__input-list:hover{display:flex}.compose-mail__recipients__container .recipients__input-list li{list-style:none;padding:.25rem .5rem;cursor:pointer;background:#fff;border:1px solid #eef3f6;border-top:0px}.compose-mail__recipients__container .recipients__input-list li:hover{background:#eef3f6}.compose-mail__recipients__container .recipients__input-list li:last-child{border-bottom:0px}.compose-mail__recipients .remove-recipient{padding:.125rem .5rem}.compose-mail input[type=text].compose-mail__subject{padding:.5rem 0;border:none;box-shadow:none;border-bottom:2px solid #eef3f6;border-radius:0}.compose-mail__message{margin:.5rem 0;height:100%;display:flex;flex-direction:column;overflow:auto}.compose-mail__message-body{height:100%;outline:rgba(0,0,0,0)}.compose-mail__send-btn{display:flex;align-items:center;gap:.5rem}.compose-mail__send-btn i{transform:translateY(-1px)}.msg-view{height:100%;display:flex;flex-direction:column;gap:1rem;overflow:auto}.msg-view-nav{display:flex;justify-content:space-between;align-items:column}.msg-view-nav__action{display:flex;gap:.5rem}.msg-view__header{display:flex;flex-direction:column;gap:1rem}.msg-view__header>h3{line-height:1}.msg-view__header .msg-details{display:flex;gap:1rem}.msg-view__header .msg-details__avatar{height:max-content}.msg-view__header .msg-details__info{display:flex;flex-direction:column}.msg-view__header .msg-details__info-item{display:flex;gap:.5rem}.msg-view__body{height:100%;overflow:auto;font-size:14px !important}.msg-view__attachment{height:50%;overflow:auto;display:flex;flex-direction:column}.msg-view__attachment-items{height:100%;overflow:auto}.mail-tag{width:8rem;padding:.5rem}.msgHeader{display:flex}.msgHeaderDetails{display:flex;flex-direction:column}table.mails th:nth-child(1){width:5%;color:#fcba03}table.mails th:nth-child(2){width:5%;color:hsl(202.5,30.7692307692%,44.9019607843%)}table.mails th:nth-child(3){width:50%;text-align:start}table.mails th:nth-child(4),table.mails th:nth-child(5){width:20%;text-align:start}table.mails td:nth-child(3){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.mails td:nth-child(4),table.mails td:nth-child(5){text-align:start}table.mails tr:hover{background-color:#eef3f6;cursor:pointer}table.mails tr.unread{color:#000;background-color:#eef3f6}table.mails>tr:hover{cursor:auto;background-color:#fff}input.star-check{display:none}input.star-check+label.star-check{color:gray}input.star-check:checked+label.star-check{color:#fcba03}#truncate{height:6rem;overflow:auto}#truncate.truncated-view{height:1.75rem;overflow:hidden}.toggle-truncate{font-size:.75rem;padding:0 .25rem;background:#999;color:#14141b;box-shadow:none;border-radius:2px}table.attachment-container{padding:0}table.attachment-container>tr{border:0}table.attachment-container .attachment-header{width:100%;display:flex;justify-content:space-between}table.attachment-container .attachment-header th{text-align:start}table.attachment-container .attachment-header th:nth-child(1){flex-basis:45%}table.attachment-container .attachment-header th:nth-child(2){flex-basis:15%}table.attachment-container .attachment-header th:nth-child(3){flex-basis:10%}table.attachment-container .attachment-header th:nth-child(4){flex-basis:20%}table.attachment-container .attachment-header th:nth-child(5){text-align:center;flex-basis:10%}table.attachment-container .attachment{width:100%;display:flex;justify-content:space-between;text-align:start}table.attachment-container .attachment__name{flex-basis:45%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}table.attachment-container .attachment__name span{margin-left:8px}table.attachment-container .attachment__from{flex-basis:15%}table.attachment-container .attachment__size{flex-basis:10%}table.attachment-container .attachment__date{flex-basis:20%}table.attachment-container .attachment td:nth-child(5){display:flex;justify-content:center;align-items:center;flex-basis:10%}table.attachment-container .attachment td:nth-child(5) button{font-size:.875rem}.view-toggle{height:max-content;border:1px solid #019dff;border-radius:4px;display:flex}.view-toggle *{padding:4px 12px;border-radius:4px}.composePopupOverlay{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1;background-color:rgba(0,0,0,.2)}.composePopupOverlay .composePopup{position:absolute;inset:0;margin:auto;width:80%;height:90%}.composePopupOverlay .composePopup>.widget{padding:2rem}.composePopupOverlay .composePopup .close-btn{position:absolute;top:1.5rem;right:1.5rem}.file-view{width:100%;padding:1rem;margin-top:1.5rem;border-radius:8px;border:1px solid #ccc;animation:fadein .5s}.file-view__heading{display:flex;justify-content:space-between;margin-bottom:.5rem}.file-view__heading-chunk{display:flex;gap:1rem}.file-view__body{display:flex;flex-direction:column;gap:1rem}.file-view__body-details{display:flex;align-items:center}.file-view__body-details-stat{width:100%;display:grid;grid-template-columns:repeat(5, 1fr)}.file-view__body-details-stat span>i{margin-right:.5rem}.file-view__body-details-action{display:flex;gap:1rem;height:100%}.file-view__body-details-action button,.file-view__body-details-action button.red{padding:.25rem .75rem}table.myfiles td{word-wrap:break-word}table.myfiles th:nth-child(1){width:2%}table.myfiles th:nth-child(2){width:50%}table.myfiles td:nth-child(2){text-align:start}table.friendsfiles td{word-wrap:break-word}table.friendsfiles th:nth-child(1){width:2%}table.friendsfiles th:nth-child(2){width:50%}table.friendsfiles th:nth-child(4){width:40%}table.friendsfiles td:nth-child(2){text-align:start}.file-search-container{margin-top:1rem;padding:8px;display:flex;gap:8px;border:1px solid rgba(20,20,27,.2);border-radius:6px;height:100%;overflow:auto}.file-search-container__keywords{flex-basis:15%;padding-right:.25rem;border-right:1px solid rgba(20,20,27,.1)}.file-search-container__keywords .keywords-container{display:flex;flex-direction:column;border-top:2.5px solid rgba(20,20,27,.08);margin-top:.125rem;padding-top:.25rem}.file-search-container__keywords .keywords-container a{font-size:1.2rem;text-decoration:none;color:#14141b}.file-search-container__keywords .keywords-container a.selected{color:#019dff}.file-search-container__results{flex-basis:85%;height:100%;overflow:auto}.file-search-container__results .results-container .results-header tr{display:flex}.file-search-container__results .results-container .results-header tr th{font-size:1.25rem;font-weight:bold;text-align:left}.file-search-container__results .results-container .results-header tr th:nth-child(1){flex-basis:40%}.file-search-container__results .results-container .results-header tr th:nth-child(2){flex-basis:10%;text-align:center}.file-search-container__results .results-container .results-header tr th:nth-child(3){flex-basis:40%}.file-search-container__results .results-container .results-header tr th:nth-child(4){flex-basis:10%}.file-search-container__results .results-container .results{height:100%;overflow:auto}.file-search-container__results .results-container .results tr{display:flex}.file-search-container__results .results-container .results tr .results__hash,.file-search-container__results .results-container .results tr .results__name{text-align:left;flex-basis:40%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.file-search-container__results .results-container .results tr .results__hash span,.file-search-container__results .results-container .results tr .results__name span{margin-left:8px}.file-search-container__results .results-container .results tr .results__size{flex-basis:10%}.file-search-container__results .results-container .results tr .results__download{flex-basis:10%;display:flex;justify-content:start;align-items:center}.search-form{display:flex;width:40%}.search-form input{width:100%}.search-form button{margin-left:.5rem}.shareManagerPopupOverlay{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1;background-color:rgba(0,0,0,.2)}.shareManagerPopupOverlay .shareManagerPopup{position:absolute;inset:0;margin:auto;width:80%;height:90%}.shareManagerPopupOverlay .shareManagerPopup>.widget{padding:1.5rem}.shareManagerPopupOverlay .shareManagerPopup .close-btn{position:absolute;top:1.5rem;right:1.5rem}.share-manager{display:flex;flex-direction:column;justify-content:space-between}.share-manager__table{margin:1rem 0 auto}.share-manager__table thead{font-weight:bold;text-align:left}.share-manager__table thead td:nth-child(1),.share-manager__table thead td:nth-child(2){padding-left:.5rem}.share-manager__table thead td:nth-child(3) .tooltip,.share-manager__table thead td:nth-child(4) .tooltip{font-weight:normal;font-size:1rem}.share-manager__table tbody{text-align:left}.share-manager__table tbody td:nth-child(4){font-size:1rem}.share-manager__table td input{border:0 !important}.share-manager__table td input[type=text]{width:100%}.share-manager__table td:nth-child(1){width:45%}.share-manager__table td:nth-child(2){width:20%}.share-manager__table td:nth-child(3){width:10%}.share-manager__table td:nth-child(4){width:25%}.share-manager__actions{display:flex;justify-content:space-between}.share-manager__form{display:flex;flex-direction:column;gap:.5rem}.share-manager__form_input{display:flex;flex-direction:column;gap:.5rem}.share-manager__form_input input{flex-grow:1}.share-manager .share-flags input.share-flags-check{display:none}.share-manager .share-flags input.share-flags-check+label.share-flags-label{color:gray;margin-right:.25rem;padding:.25rem .25rem .125rem;border:1px solid #6d6d6d;border-radius:.5rem}.share-manager .share-flags input.share-flags-check:checked+label.share-flags-label{color:#118fcc}.share-manager label span{display:inline-block;width:1.125rem}.manage-visibility label{width:100%;cursor:pointer}.manage-visibility{display:flex;justify-content:space-between}@media(max-width: 700px){.file-view__body-details{flex-direction:column;align-items:flex-start;gap:1rem}.file-view__body-details-stat{grid-template-columns:1fr;gap:.5rem}.file-view__body-details-stat span{display:flex;align-items:center}.share-manager__table,.share-manager__table thead,.share-manager__table tbody,.share-manager__table tr,.share-manager__table td{display:block;width:100% !important}.share-manager__table thead{display:none}.share-manager__table tr{border:1px solid #ccc;border-radius:8px;margin-bottom:1rem;padding:.5rem;background:#fff}.share-manager__table td{margin-bottom:.5rem;border:none !important;padding-left:0 !important}table.myfiles,table.myfiles tr,table.myfiles td,table.friendsfiles,table.friendsfiles tr,table.friendsfiles td{display:block;width:100% !important}table.myfiles th,table.friendsfiles th{display:none}table.myfiles tr,table.friendsfiles tr{border:1px solid #ccc;border-radius:8px;margin-bottom:1rem;padding:.5rem;background:#fff}.file-search-container{flex-direction:column}.file-search-container__keywords{flex-basis:auto;width:100%;border-right:none;border-bottom:1px solid rgba(20,20,27,.1);padding-bottom:1rem;margin-bottom:1rem}.results-container,.results-container thead,.results-container tbody,.results-container tr,.results-container td{display:block;width:100% !important}.results-container thead{display:none}.results-container tr{border-bottom:1px solid #eee;padding:1rem 0}.results-container td{margin-bottom:.5rem;word-break:break-all}}.file-section{margin-top:2rem;display:flex;flex-direction:column}.comments-section{margin-top:2rem;display:flex;justify-content:space-between}.comments-section__menu{display:flex;gap:1rem}.comments-section__menu-id{display:flex;align-items:center;gap:.25rem}#toggleunsub{position:relative;background:gray}table.channels th:nth-child(1){width:50%;text-align:start}table.channels td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.channels tr:hover{background-color:#eef3f6;cursor:pointer}table.channels tr.hidden{display:none}table{padding:.5rem}table.comments{border:1px solid #eee}table.comments th{height:40px}table.comments th:nth-child(1){width:2%}table.comments th:nth-child(2){width:40%}table.comments td{word-wrap:break-word}table.comments td:nth-child(2){text-align:start}table.files th:first-child{text-align:start;width:60%}table.files tr td:first-child{text-align:start}table.files td{word-wrap:break-word}#mtags{width:160px;text-align:center;font-size:medium;margin-left:10px;height:40px}.forums-node-panel{position:relative;bottom:200px;margin-left:200px;animation:fadein .5s}table.forums th:nth-child(1){width:50%;text-align:start}table.forums td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.forums tr:hover{background-color:#eef3f6;cursor:pointer}table.forums tr.hidden{display:none}#searchforum{position:relative;margin-left:250px}#forumdetails{position:relative;padding:10px}.p{margin:0}#toggleunsub{position:relative;background:gray}table.threads tr:hover{background-color:#eef3f6;cursor:pointer}table.threads td{word-wrap:break-word}table.threadreply th:nth-child(2){width:50%}table.threadreply th:nth-child(1){width:2%}table.threadreply td:nth-child(2){width:50%;text-align:start}table.threadreply td{word-wrap:break-word}table.threadreply tr:hover{background-color:#eef3f6;cursor:pointer}table.boards th:nth-child(1){width:50%;text-align:start}table.boards td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.boards tr:hover{background-color:#eef3f6;cursor:pointer}table.boards tr.hidden{display:none}#toggleunsub{position:relative;background:gray}#options{width:100px;text-align:center;font-size:medium;margin-left:20px;height:40px}#composepopup{height:80%;width:70%}#mtags{width:160px;text-align:center;font-size:medium;margin-left:10px;height:40px}.mail .permission-flag{margin-bottom:1rem;display:flex;gap:1rem}.mail-tags{padding:.5rem;border:1px solid rgba(20,20,27,.2);border-radius:6px}.mail-tags__container{display:flex;flex-direction:column}.mail-tags__container .tag-item{display:flex;align-items:center;gap:4px;border-bottom:1px solid rgba(20,20,27,.1);padding:2px 0}.mail-tags__container .tag-item:last-child{border:none}.mail-tags__container .tag-item__color{width:1.25rem;height:1.25rem;aspect-ratio:1}.mail-tags__container .tag-item__name{font-size:1.125rem}.mail-tags__container .tag-item__modify{margin-left:auto;font-size:.75rem;display:flex;gap:4px}.mail-tags__container .tag-item:hover{background-color:#eef3f6}.mail-tags__container .tag-item button,.mail-tags__container .tag-item button.red{padding:.25rem .6rem}.mail-tags-form .input-field{margin-bottom:.5rem}.mail-tags-form .input-field label{margin-right:.5rem}.external-address{margin:0;padding-left:1rem;height:100px;overflow:hidden auto}.external-address::-webkit-scrollbar{display:none}.proxy-server{display:flex;flex-direction:column;gap:4px}.proxy-server__tor>h4,.proxy-server__i2p>h4{margin-bottom:.25rem}.proxy-server__tor>input,.proxy-server__i2p>input{margin-right:.5rem}.proxy-server__tor .proxy-outgoing,.proxy-server__i2p .proxy-outgoing{display:inline-flex;align-items:center;gap:.5rem}.proxy-server__tor .proxy-outgoing__status,.proxy-server__i2p .proxy-outgoing__status{width:1rem;height:1rem;aspect-ratio:1;border:1px solid #000;border-radius:50%}.config-files{display:flex;flex-direction:column;gap:1rem} - -/* Custom improvements for Network Page */ - -.network-container { - display: flex; - height: 100%; - width: 100%; - overflow: hidden; - background-color: #f1f5f9; -} - -.network-left-pane { - width: 320px; - min-width: 300px; - max-width: 350px; - border-right: 1px solid #cbd5e1; - display: flex; - flex-direction: column; - background: #ffffff; - box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05); -} - -.own-profile-card { - padding: 1.25rem; - border-bottom: 1px solid #e2e8f0; - background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.own-profile-card .profile-header { - display: flex; - align-items: center; - gap: 1rem; -} - -.own-profile-card .profile-info { - display: flex; - flex-direction: column; - flex: 1; - overflow: hidden; -} - -.own-profile-card .profile-info .profile-name { - font-weight: 700; - color: #1e293b; - font-size: 1.1rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.own-profile-card .profile-info .profile-status { - font-size: 0.85rem; - color: #10b981; - font-weight: 500; - display: flex; - align-items: center; - gap: 0.35rem; -} - -.own-profile-card .profile-info .profile-status::before { - content: ''; - display: inline-block; - width: 8px; - height: 8px; - background-color: #10b981; - border-radius: 50%; -} - -.own-profile-card .own-identity-select-container { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.own-profile-card .own-identity-select-container label { - font-size: 0.75rem; - color: #64748b; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.own-profile-card .own-identity-select-container select.own-identity-select { - width: 100%; - padding: 0.375rem 0.5rem; - font-size: 0.85rem; - border: 1px solid #cbd5e1; - border-radius: 0.375rem; - background-color: #ffffff; - color: #334155; - outline: none; - cursor: pointer; - transition: border-color 0.2s; -} - -.own-profile-card .own-identity-select-container select.own-identity-select:focus { - border-color: #3ba4d7; -} - -.friends-list-container { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - position: relative; -} - -.friends-list-container .people-context-menu { - position: absolute; - left: 2rem; - width: 210px; - background-color: #ffffff; - border: 1px solid #e2e8f0; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); - border-radius: 0.375rem; - z-index: 1010; - padding: 0.25rem 0; - display: flex; - flex-direction: column; -} - -.friends-list-container .people-context-menu .menu-item { - padding: 0.5rem 1rem; - font-size: 0.85rem; - color: #334155; - cursor: pointer; - display: flex; - align-items: center; - transition: background-color 0.2s; -} - -.friends-list-container .people-context-menu .menu-item:hover { - background-color: #f1f5f9; - color: #0f172a; -} - -.friends-list-container .searchbar-container { - padding: 0.75rem 1rem; - border-bottom: 1px solid #e2e8f0; -} - -.friends-list-container .searchbar-container input.searchbar { - width: 100%; - padding: 0.5rem 0.75rem; - font-size: 0.9rem; - border: 1px solid #cbd5e1; - border-radius: 0.375rem; - background-color: #f8fafc; - outline: none; - transition: all 0.2s; -} - -.friends-list-container .searchbar-container input.searchbar:focus { - background-color: #ffffff; - border-color: #3ba4d7; - box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); -} - -.friends-list-container .friends-scroll { - flex: 1; - overflow-y: auto; - padding: 0.5rem 0; -} - -.friend-list-item { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem 1rem; - margin: 0.125rem 0.5rem; - border-radius: 0.5rem; - cursor: pointer; - transition: all 0.2s; -} - -.friend-list-item:hover { - background-color: #f1f5f9; -} - -.friend-list-item.selected { - background-color: #e0f2fe; -} - -.friend-list-item.selected .friend-meta .friend-name { - color: #0369a1; - font-weight: 600; -} - -.friend-list-item .friend-avatar { - flex-shrink: 0; -} - -.friend-list-item .friend-meta { - flex: 1; - min-width: 0; -} - -.friend-list-item .friend-meta .friend-name { - font-size: 0.95rem; - color: #334155; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - transition: color 0.2s; -} - -.friend-list-item .friend-meta .friend-status { - font-size: 0.8rem; - color: #94a3b8; -} - -.friend-list-item .friend-meta .friend-status.online { - color: #10b981; - font-weight: 500; -} - -.network-right-pane { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - background-color: #f8fafc; -} - -.network-pane-placeholder { - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - color: #94a3b8; - gap: 1rem; - padding: 2rem; - text-align: center; -} - -.network-pane-placeholder i { - font-size: 4rem; - color: #cbd5e1; -} - -.network-pane-placeholder p { - font-size: 1.1rem; - max-width: 400px; -} - -.network-tabs { - display: flex; - background-color: #ffffff; - border-bottom: 1px solid #cbd5e1; - padding: 0.5rem 1rem 0; - gap: 0.5rem; -} - -.network-tabs .tab-btn { - padding: 0.625rem 1.25rem; - font-size: 0.95rem; - font-weight: 600; - color: #64748b; - background: transparent; - border: none; - border-radius: 0.375rem 0.375rem 0 0; - border-bottom: 3px solid transparent; - cursor: pointer; - box-shadow: none; - transition: all 0.2s; -} - -.network-tabs .tab-btn:hover { - color: #334155; - background-color: #f1f5f9; -} - -.network-tabs .tab-btn.active { - color: #3ba4d7; - border-bottom-color: #3ba4d7; - background-color: transparent; -} - -.network-tab-content { - flex: 1; - overflow-y: auto; - padding: 1.5rem; -} - -.network-detail-view { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.network-detail-view .detail-header { - display: flex; - align-items: center; - gap: 1.5rem; - padding-bottom: 1.5rem; - border-bottom: 1px solid #e2e8f0; -} - -.network-detail-view .detail-header .detail-title { - flex: 1; -} - -.network-detail-view .detail-header .detail-title h2 { - font-size: 1.75rem; - font-weight: 800; - color: #1e293b; - margin-bottom: 0.25rem; -} - -.network-detail-view .detail-header .detail-title .detail-subtitle { - font-size: 0.9rem; - color: #64748b; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.network-detail-view .detail-header .detail-actions { - display: flex; - gap: 0.75rem; -} - -.network-detail-view .detail-header .detail-actions button { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - font-size: 0.9rem; -} - -.network-detail-view .detail-section { - background-color: #ffffff; - border-radius: 0.5rem; - border: 1px solid #e2e8f0; - padding: 1.25rem; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); -} - -.network-detail-view .detail-section h3 { - font-size: 1.1rem; - font-weight: 700; - color: #334155; - margin-bottom: 1rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid #f1f5f9; -} - -.network-detail-view .detail-section .info-grid { - display: grid; - grid-template-columns: 120px 1fr; - row-gap: 0.75rem; - font-size: 0.9rem; -} - -.network-detail-view .detail-section .info-grid .info-label { - font-weight: 600; - color: #64748b; -} - -.network-detail-view .detail-section .info-grid .info-value { - color: #1e293b; - word-break: break-all; -} - -.network-detail-view .locations-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 1rem; -} - -.location-card { - background-color: #ffffff; - border: 1px solid #e2e8f0; - border-radius: 0.5rem; - padding: 1rem; - display: flex; - flex-direction: column; - gap: 0.5rem; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); -} - -.location-card .loc-header { - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid #f1f5f9; - padding-bottom: 0.5rem; - margin-bottom: 0.25rem; -} - -.location-card .loc-header .loc-name { - font-weight: 700; - color: #334155; - font-size: 0.95rem; -} - -.location-card .loc-header .loc-status { - font-size: 0.75rem; - font-weight: 600; - padding: 0.125rem 0.5rem; - border-radius: 0.25rem; -} - -.location-card .loc-header .loc-status.online { - background-color: #d1fae5; - color: #065f46; -} - -.location-card .loc-header .loc-status.offline { - background-color: #f1f5f9; - color: #475569; -} - -.location-card .loc-body { - font-size: 0.85rem; - display: grid; - grid-template-columns: 80px 1fr; - row-gap: 0.25rem; -} - -.location-card .loc-body .loc-label { - color: #64748b; -} - -.location-card .loc-body .loc-val { - color: #334155; - word-break: break-all; -} - -.location-card .loc-footer { - margin-top: 0.5rem; - display: flex; - justify-content: flex-end; -} - -.location-card .loc-footer button { - font-size: 0.8rem; - padding: 0.25rem 0.75rem; -} - -.network-chat-view { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; - background-color: #f8fafc; -} - -.network-chat-view .chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.25rem; - display: flex; - flex-direction: column; - gap: 1rem; -} - -.chat-bubble-container { - display: flex; - flex-direction: column; - max-width: 70%; -} - -.chat-bubble-container.outgoing { - align-self: flex-end; - align-items: flex-end; -} - -.chat-bubble-container.outgoing .chat-bubble { - background-color: #3ba4d7; - color: #ffffff; - border-bottom-right-radius: 0.125rem; -} - -.chat-bubble-container.incoming { - align-self: flex-start; - align-items: flex-start; -} - -.chat-bubble-container.incoming .chat-bubble { - background-color: #ffffff; - color: #1e293b; - border: 1px solid #e2e8f0; - border-bottom-left-radius: 0.125rem; -} - -.chat-bubble-container .chat-sender { - font-size: 0.75rem; - color: #64748b; - margin-bottom: 0.25rem; - padding: 0 0.25rem; -} - -.chat-bubble-container .chat-bubble { - padding: 0.625rem 0.875rem; - border-radius: 0.75rem; - font-size: 0.925rem; - line-height: 1.4; - white-space: break-spaces; - word-break: break-word; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.chat-bubble-container .chat-time { - font-size: 0.7rem; - color: #94a3b8; - margin-top: 0.25rem; - padding: 0 0.25rem; -} - -.network-chat-view .chat-input-area { - padding: 1rem; - background-color: #ffffff; - border-top: 1px solid #cbd5e1; - display: flex; - gap: 0.75rem; - align-items: center; -} - -.network-chat-view .chat-input-area textarea.chat-textarea { - flex: 1; - resize: none; - height: 40px; - padding: 0.5rem 0.75rem; - border: 1px solid #cbd5e1; - border-radius: 0.375rem; - font-size: 0.9rem; - outline: none; - transition: all 0.2s; -} - -.network-chat-view .chat-input-area textarea.chat-textarea:focus { - border-color: #3ba4d7; - box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); -} - -.network-chat-view .chat-input-area button.send-btn { - padding: 0.5rem 1.25rem; - font-size: 0.9rem; - height: 40px; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.network-chat-view .chat-warning { - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - color: #64748b; - text-align: center; - padding: 2rem; - gap: 1rem; -} - -.network-chat-view .chat-warning i { - font-size: 3rem; - color: #cbd5e1; -} - -.network-chat-view .chat-warning h4 { - font-weight: 700; - color: #334155; -} - -.network-chat-view .chat-warning p { - max-width: 350px; - font-size: 0.9rem; -} - -/* People Page Modern Split-Pane Layout */ -.people-container { - display: flex; - height: calc(100vh - 55px); - width: 100%; - overflow: hidden; -} - -.people-left-pane { - width: 320px; - border-right: 1px solid #cbd5e1; - display: flex; - flex-direction: column; - background-color: #ffffff; - overflow: hidden; -} - -.people-right-pane { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - background-color: #f8fafc; -} - -.people-filter-group { - display: flex; - padding: 0.75rem 1rem 0.25rem 1rem; - gap: 0.25rem; - border-bottom: 1px solid #e2e8f0; -} - -.people-filter-group button.filter-btn { - flex: 1; - padding: 0.375rem 0.5rem; - font-size: 0.85rem; - font-weight: 600; - color: #64748b; - background-color: #f1f5f9; - border: none; - border-radius: 0.375rem; - cursor: pointer; - box-shadow: none; - transition: all 0.2s; -} - -.people-filter-group button.filter-btn:hover { - background-color: #e2e8f0; - color: #334155; -} - -.people-filter-group button.filter-btn.active { - background-color: #3ba4d7; - color: #ffffff; -} - -.people-left-pane .create-id-container { - padding: 0.75rem 1rem; - border-bottom: 1px solid #e2e8f0; - display: flex; -} - -.people-left-pane .create-id-container button.create-id-btn { - width: 100%; - display: flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - padding: 0.5rem; - font-weight: 600; - font-size: 0.9rem; -} - -/* ===================================================== - CHAT HUB - Two-Pane Layout - ===================================================== */ - -.chat-hub-container { - display: flex; - height: 100%; - width: 100%; - overflow: hidden; - background-color: #f1f5f9; -} - -.chat-hub-left-pane { - width: 320px; - min-width: 300px; - max-width: 350px; - border-right: 1px solid #cbd5e1; - display: flex; - flex-direction: column; - background: #ffffff; - box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05); -} - -.chat-own-profile-card { - padding: 1.25rem; - border-bottom: 1px solid #e2e8f0; - background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); - position: relative; -} - -.chat-create-lobby-btn { - position: absolute; - bottom: 0.5rem; - right: 1.25rem; - background-color: #0084ff; - color: #ffffff; - border: none; - border-radius: 0.375rem; - padding: 0.35rem 0.75rem; - font-size: 0.85rem; - font-weight: 600; - cursor: pointer; - box-shadow: 0 4px 6px -1px rgba(0, 132, 255, 0.2), 0 2px 4px -1px rgba(0, 132, 255, 0.1); - transition: background-color 0.2s, transform 0.2s; - display: flex; - align-items: center; - gap: 0.25rem; -} - -.chat-create-lobby-btn:hover { - background-color: #0073e6; - transform: translateY(-1px); -} - -.chat-create-lobby-btn:active { - transform: translateY(0); -} - -.chat-own-profile-card .profile-header { - display: flex; - align-items: center; - gap: 1rem; -} - -.chat-own-profile-card .profile-info { - display: flex; - flex-direction: column; - flex: 1; - overflow: hidden; -} - -.chat-own-profile-card .profile-info .profile-name { - font-weight: 700; - color: #1e293b; - font-size: 1.1rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.chat-own-profile-card .profile-info .profile-status { - font-size: 0.85rem; - color: #10b981; - font-weight: 500; - display: flex; - align-items: center; - gap: 0.35rem; -} - -.chat-own-profile-card .profile-info .profile-status::before { - content: ''; - display: inline-block; - width: 8px; - height: 8px; - background-color: #10b981; - border-radius: 50%; -} - -.chat-rooms-list-container { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.chat-rooms-list-container .searchbar-container { - padding: 0.75rem 1rem; - border-bottom: 1px solid #e2e8f0; -} - -.chat-rooms-list-container .searchbar-container input.searchbar { - width: 100%; - padding: 0.5rem 0.75rem; - font-size: 0.9rem; - border: 1px solid #cbd5e1; - border-radius: 0.375rem; - background-color: #f8fafc; - outline: none; - transition: all 0.2s; -} - -.chat-rooms-list-container .searchbar-container input.searchbar:focus { - background-color: #ffffff; - border-color: #3ba4d7; - box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); -} - -.chat-rooms-list-container .rooms-scroll { - flex: 1; - overflow-y: auto; - padding: 0.5rem 0; -} - -.rooms-section-title { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1rem 0.375rem; - font-size: 0.75rem; - font-weight: 700; - color: #64748b; - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.rooms-section-title i { - font-size: 0.7rem; - color: #94a3b8; -} - -.chat-room-list-item { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem 1rem; - margin: 0.125rem 0.5rem; - border-radius: 0.5rem; - cursor: pointer; - transition: all 0.2s; -} - -.chat-room-list-item:hover { - background-color: #f1f5f9; -} - -.chat-room-list-item.selected { - background-color: #e0f2fe; -} - -.chat-room-list-item.selected .room-meta .room-name { - color: #0369a1; - font-weight: 600; -} - -.chat-room-list-item .room-icon { - flex-shrink: 0; - width: 36px; - height: 36px; - border-radius: 0.5rem; - background: linear-gradient(135deg, #3ba4d7, #0ea5e9); - display: flex; - align-items: center; - justify-content: center; - color: #ffffff; - font-size: 0.85rem; -} - -.chat-room-list-item.public-room .room-icon { - background: linear-gradient(135deg, #10b981, #059669); -} - -.chat-room-list-item .room-meta { - flex: 1; - min-width: 0; -} - -.chat-room-list-item .room-meta .room-name { - font-size: 0.95rem; - color: #334155; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - transition: color 0.2s; -} - -.chat-room-list-item .room-meta .room-topic { - font-size: 0.8rem; - color: #94a3b8; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.chat-room-list-item .room-badge { - flex-shrink: 0; - min-width: 24px; - height: 24px; - border-radius: 12px; - background-color: #e2e8f0; - color: #475569; - font-size: 0.75rem; - font-weight: 700; - display: flex; - align-items: center; - justify-content: center; - padding: 0 0.375rem; -} - -.chat-hub-right-pane { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - background-color: #f8fafc; -} - -.chat-pane-placeholder { - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - color: #94a3b8; - gap: 1rem; - padding: 2rem; - text-align: center; -} - -.chat-pane-placeholder i { - font-size: 4rem; - color: #cbd5e1; -} - -.chat-pane-placeholder p { - font-size: 1.1rem; - max-width: 400px; -} - -.chat-hub-tab-content { - flex: 1; - overflow-y: auto; - padding: 1.5rem; -} - -.chat-room-detail-view { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.chat-room-detail-view .detail-header { - display: flex; - align-items: flex-start; - gap: 1.5rem; - padding-bottom: 1.5rem; - border-bottom: 1px solid #e2e8f0; - flex-wrap: wrap; -} - -.chat-room-detail-view .detail-header .detail-title { - flex: 1; - min-width: 200px; -} - -.chat-room-detail-view .detail-header .detail-title h2 { - font-size: 1.75rem; - font-weight: 800; - color: #1e293b; - margin-bottom: 0.25rem; -} - -.chat-room-detail-view .detail-header .detail-title .detail-subtitle { - font-size: 0.9rem; - color: #64748b; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.chat-room-detail-view .detail-header .detail-actions { - display: flex; - gap: 0.75rem; - flex-wrap: wrap; -} - -.chat-room-detail-view .detail-header .detail-actions button { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - font-size: 0.9rem; -} - -.chat-room-detail-view .detail-section { - background-color: #ffffff; - border-radius: 0.5rem; - border: 1px solid #e2e8f0; - padding: 1.25rem; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); -} - -.chat-room-detail-view .detail-section h3 { - font-size: 1.1rem; - font-weight: 700; - color: #334155; - margin-bottom: 1rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid #f1f5f9; -} - -.chat-room-detail-view .detail-section .info-grid { - display: grid; - grid-template-columns: 130px 1fr; - row-gap: 0.75rem; - font-size: 0.9rem; -} - -.chat-room-detail-view .detail-section .info-grid .info-label { - font-weight: 600; - color: #64748b; -} - -.chat-room-detail-view .detail-section .info-grid .info-value { - color: #1e293b; - word-break: break-all; -} - -.participants-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); - gap: 0.5rem; -} - -.participant-card { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 0.75rem; - background-color: #f8fafc; - border: 1px solid #e2e8f0; - border-radius: 0.375rem; -} - -.participant-card .participant-name { - font-size: 0.875rem; - color: #334155; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.no-participants { - color: #94a3b8; - font-size: 0.9rem; - font-style: italic; -} - -.detail-actions-footer { - display: flex; - gap: 0.75rem; -} - -.detail-actions-footer button { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - font-size: 0.9rem; -} - -.join-description { - color: #64748b; - font-size: 0.9rem; - margin-bottom: 1rem; -} - -.identities-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 0.75rem; -} - -.identity-card { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.75rem 1rem; - background-color: #f8fafc; - border: 1px solid #e2e8f0; - border-radius: 0.5rem; - cursor: pointer; - transition: all 0.2s; -} - -.identity-card:hover { - background-color: #e0f2fe; - border-color: #3ba4d7; -} - -.identity-card .identity-name { - font-size: 0.95rem; - font-weight: 600; - color: #334155; -} - -.identity-card i { - color: #3ba4d7; - font-size: 0.9rem; -} - -.no-rooms { - padding: 1rem; - color: #94a3b8; - text-align: center; - font-style: italic; -} - -/* Chat Hub Responsive - Mobile */ -@media (max-width: 899px) { - .chat-hub-container { - flex-direction: column; - } - - .chat-hub-left-pane { - width: 100%; - min-width: 0; - max-width: none; - max-height: 45%; - border-right: none; - border-bottom: 1px solid #cbd5e1; - } - - .chat-hub-right-pane { - flex: 1; - min-height: 0; - } -} - -/* ===================================================== - CHAT HUB - Right Pane Conversation & Tabs Styling - ===================================================== */ - -.chat-hub-header-bar { - padding: 0.75rem 1.5rem; - background-color: #ffffff; - border-bottom: 1px solid #e2e8f0; - display: flex; - align-items: center; - justify-content: space-between; - height: 65px; - flex-shrink: 0; -} - -.chat-hub-header-bar .chat-header-info { - display: flex; - flex-direction: column; - overflow: hidden; -} - -.chat-hub-header-bar .chat-header-info .chat-header-name { - font-size: 1.15rem; - font-weight: 800; - color: #1e293b; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.chat-hub-header-bar .chat-header-info .chat-header-topic { - font-size: 0.85rem; - color: #64748b; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin-top: 0.125rem; -} - -.chat-hub-header-bar .chat-header-actions { - display: flex; - gap: 0.5rem; -} - -.chat-hub-header-bar .chat-header-actions button { - display: flex; - align-items: center; - gap: 0.35rem; - padding: 0.375rem 0.75rem; - font-size: 0.85rem; -} - -.chat-hub-tabs-container { - background-color: #ffffff; - border-bottom: 1px solid #cbd5e1; - padding: 0.5rem 1.5rem 0; -} - -.chat-hub-tabs { - display: flex; - gap: 0.5rem; -} - -.chat-hub-tabs .tab-btn { - padding: 0.625rem 1.25rem; - font-size: 0.95rem; - font-weight: 600; - color: #64748b; - background: transparent; - border: none; - border-radius: 0.375rem 0.375rem 0 0; - border-bottom: 3px solid transparent; - cursor: pointer; - box-shadow: none; - transition: all 0.2s; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.chat-hub-tabs .tab-btn:hover { - color: #334155; - background-color: #f1f5f9; -} - -.chat-hub-tabs .tab-btn.active { - color: #3ba4d7; - border-bottom-color: #3ba4d7; - background-color: transparent; -} - -.chat-hub-tab-content { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - background-color: #f8fafc; -} - -.chat-hub-conversation-layout { - display: flex; - flex-direction: row; - height: 100%; - width: 100%; - overflow: hidden; -} - -.chat-hub-conversation-main { - display: flex; - flex-direction: column; - flex: 1; - height: 100%; - overflow: hidden; -} - -.chat-hub-rightbar { - width: 200px; - border-left: 1px solid #cbd5e1; - background-color: #ffffff; - display: flex; - flex-direction: column; - flex-shrink: 0; - position: relative; -} - -.chat-hub-rightbar .rightbar-title { - padding: 0.75rem 1rem; - font-size: 0.85rem; - font-weight: 700; - color: #64748b; - text-transform: uppercase; - letter-spacing: 0.05em; - border-bottom: 1px solid #e2e8f0; -} - -.chat-hub-rightbar .rightbar-users-list { - flex: 1; - overflow-y: auto; - padding: 0.5rem; -} - -.chat-hub-rightbar .user { - padding: 0.5rem 0.75rem; - font-size: 0.9rem; - color: #334155; - border-radius: 0.375rem; - transition: all 0.2s; - display: flex; - align-items: center; - gap: 0.5rem; - position: relative; -} - -.chat-hub-rightbar .user .user-name { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1; -} - -.chat-hub-rightbar .user:hover { - background-color: #f1f5f9; - color: #0f172a; -} - -.chat-hub-rightbar .user-tooltip { - position: absolute; - left: -275px; - transform: translateY(-50%); - width: 260px; - background-color: #ffffe1; - border: 1px solid #7f7f7f; - box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.25); - padding: 0.5rem; - border-radius: 0.25rem; - z-index: 1000; - white-space: normal; - display: flex; - gap: 0.5rem; - align-items: flex-start; -} - -.chat-hub-rightbar .user-tooltip .tooltip-avatar { - flex-shrink: 0; -} - -.chat-hub-rightbar .user-tooltip .tooltip-details { - display: flex; - flex-direction: column; - gap: 0.25rem; - font-size: 0.8rem; - color: #000000; - text-align: left; -} - -.chat-hub-rightbar .user-tooltip .tooltip-row { - line-height: 1.2; -} - -.chat-hub-rightbar .user-tooltip .tooltip-label { - font-weight: bold; -} - -.chat-hub-rightbar .user-tooltip .tooltip-value { - font-weight: normal; - word-break: break-all; -} - -.chat-hub-rightbar .user-tooltip .tooltip-value.tooltip-id { - font-family: monospace; -} - -.chat-hub-rightbar .rightbar-context-menu { - position: absolute; - right: 1rem; - width: 210px; - background-color: #ffffff; - border: 1px solid #e2e8f0; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); - border-radius: 0.375rem; - z-index: 1010; - padding: 0.25rem 0; - display: flex; - flex-direction: column; -} - -.chat-hub-rightbar .rightbar-context-menu .menu-item { - padding: 0.5rem 1rem; - font-size: 0.85rem; - color: #334155; - cursor: pointer; - display: flex; - align-items: center; - transition: background-color 0.2s; -} - -.chat-hub-rightbar .rightbar-context-menu .menu-item:hover { - background-color: #f1f5f9; - color: #0f172a; -} - -.chat-hub-rightbar .user .defaultAvatar { - width: 2rem; - height: 2rem; - font-size: 0.9rem; - flex-shrink: 0; -} - -.chat-hub-rightbar .user img.avatar { - width: 2rem; - height: 2rem; - flex-shrink: 0; -} - -@media (max-width: 899px) { - .chat-hub-rightbar { - display: none; - } -} - -.chat-hub-messages { - flex: 1; - overflow-y: auto; - padding: 1.25rem 1.5rem; - display: flex; - flex-direction: column; - gap: 1rem; -} - -/* Chat bubble overrides for two-pane layout */ -.chat-hub-messages .message { - display: flex; - flex-direction: column; - max-width: 70%; - padding: 0.625rem 0.875rem; - border-radius: 0.75rem; - font-size: 1rem; - line-height: 1.4; - word-break: break-word; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.chat-hub-messages .message.incoming { - align-self: flex-start; - align-items: flex-start; - background-color: #ffffff; - color: #1e293b; - border: 1px solid #e2e8f0; - border-bottom-left-radius: 0.125rem; -} - -.chat-hub-messages .message.outgoing { - align-self: flex-end; - align-items: flex-end; - background-color: #3ba4d7; - color: #ffffff; - border-bottom-right-radius: 0.125rem; -} - -.chat-hub-messages .message .username { - font-size: 0.75rem; - margin-bottom: 0.25rem; - padding: 0 0.125rem; - font-weight: 700; -} - -.chat-hub-messages .message.incoming .username { - color: #0369a1; -} - -.chat-hub-messages .message.outgoing .username { - color: #e0f2fe; -} - -.chat-hub-messages .message .messagetext { - white-space: break-spaces; - margin: 0; -} - -.chat-hub-messages .message .datetime { - font-size: 0.7rem; - margin-top: 0.25rem; - padding: 0 0.125rem; - opacity: 0.8; -} - -.chat-hub-messages .message.incoming .datetime { - color: #64748b; -} - -.chat-hub-messages .message.outgoing .datetime { - color: #f1f5f9; -} - -.chat-hub-input-area { - padding: 1rem 1.5rem; - background-color: #ffffff; - border-top: 1px solid #cbd5e1; - display: flex; - gap: 0.75rem; - align-items: center; - flex-shrink: 0; -} - -.chat-hub-input-area textarea.chat-hub-textarea { - flex: 1; - resize: none; - height: 40px; - padding: 0.5rem 0.75rem; - border: 1px solid #cbd5e1; - border-radius: 0.375rem; - font-size: 0.9rem; - outline: none; - transition: all 0.2s; - background-color: #f8fafc; -} - -.chat-hub-input-area textarea.chat-hub-textarea:focus { - background-color: #ffffff; - border-color: #3ba4d7; - box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); -} - -.chat-hub-input-area button.chat-hub-send-btn { - padding: 0.5rem 1.25rem; - font-size: 0.9rem; - height: 40px; - display: flex; - align-items: center; - gap: 0.5rem; - border-radius: 0.375rem; -} - -/* Compact Room Chat Style (No bubbles, IRC-style single line per message) */ -.chat-hub-messages.compact-container, -.messages.compact-container { - gap: 0 !important; - padding: 0.75rem 1rem !important; - background-color: #ffffff !important; - display: flex !important; - flex-direction: column !important; - flex: 1 !important; - overflow-y: auto !important; - min-height: 0 !important; -} - -.chat-hub-messages.compact-container .message.compact, -.messages.compact-container .message.compact { - display: block !important; - max-width: 100% !important; - padding: 0.15rem 0 !important; - border-radius: 0 !important; - background-color: transparent !important; - border: none !important; - box-shadow: none !important; - align-self: flex-start !important; - font-size: 1rem !important; - line-height: 1.5 !important; - margin: 0 !important; - white-space: nowrap !important; - overflow: hidden !important; - text-overflow: ellipsis !important; - width: 100% !important; -} - -.chat-hub-messages.compact-container .message.compact:hover, -.messages.compact-container .message.compact:hover { - background-color: #f8fafc !important; - overflow: visible !important; - white-space: normal !important; -} - -.chat-hub-messages.compact-container .message.compact .datetime, -.messages.compact-container .message.compact .datetime { - color: #a0a0a0 !important; - margin-right: 0.4rem !important; - font-size: 0.78rem !important; - font-family: monospace !important; - opacity: 1 !important; - display: inline !important; - margin-top: 0 !important; - margin-bottom: 0 !important; - padding: 0 !important; -} - -.chat-hub-messages.compact-container .message.compact .username, -.messages.compact-container .message.compact .username { - font-weight: bold !important; - margin-right: 0.2rem !important; - margin-bottom: 0 !important; - font-size: 0.875rem !important; - display: inline !important; - padding: 0 !important; -} - -.chat-hub-messages.compact-container .message.compact .messagetext, -.messages.compact-container .message.compact .messagetext { - color: #1e293b !important; - white-space: normal !important; - word-break: break-word !important; - display: inline !important; - margin: 0 !important; - font-size: 1rem !important; -} - -/* Make emoji characters render larger than surrounding text in chat */ -.chat-hub-messages .message .messagetext, -.chat-hub-messages.compact-container .message.compact .messagetext, -.messages.compact-container .message.compact .messagetext { - font-family: 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', 'Roboto', Arial, sans-serif; -} - -.chat-emoji { - font-size: 1.45em; - line-height: 1; - vertical-align: -0.15em; - display: inline-block; -} - -/* Fix RetroShare ID textarea - auto-size to content, no scrollbar */ -.homepage .certificate__content .retroshareID .textArea { - min-height: unset !important; - height: auto !important; - overflow: hidden !important; - field-sizing: content !important; -} - -/* Attach file button and modal popup */ -.chat-hub-attach-btn { - background-color: transparent; - border: none; - font-size: 1.25rem; - color: #64748b; - cursor: pointer; - padding: 0.5rem; - margin-right: 0.25rem; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - transition: color 0.2s, transform 0.2s; -} - -.chat-hub-attach-btn:hover { - color: #3b82f6; - transform: scale(1.05); -} - -.attach-modal-overlay { - 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; -} - -.attach-modal { - background-color: #ffffff; - border-radius: 0.5rem; - width: 450px; - max-width: 90%; - padding: 1.5rem; - box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); - display: flex; - flex-direction: column; - gap: 1rem; -} - -.attach-modal .attach-modal-header { - display: flex; - align-items: center; - gap: 0.6rem; - margin-bottom: 0.25rem; -} - -.attach-modal .attach-modal-icon { - font-size: 1.2rem; - color: #3b82f6; -} - -.attach-modal h4 { - margin: 0; - font-size: 1.2rem; - color: #0f172a; -} - -.attach-modal p { - margin: 0; - font-size: 0.9rem; - color: #475569; -} - -.attach-modal .attach-path-row { - display: flex; - gap: 0.5rem; - align-items: center; -} - -.attach-modal .attach-path-row input[type="text"] { - flex: 1; - padding: 0.75rem; - border: 1px solid #cbd5e1; - border-radius: 0.375rem; - font-size: 0.9rem; - outline: none; - transition: border-color 0.2s; - min-width: 0; -} - -.attach-modal .attach-path-row input[type="text"]:focus { - border-color: #3b82f6; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); -} - -.attach-browse-btn { - flex-shrink: 0; - display: flex; - align-items: center; - gap: 0.35rem; - padding: 0.625rem 0.9rem; - font-size: 0.875rem; - background-color: #f1f5f9; - color: #334155; - border: 1px solid #cbd5e1; - border-radius: 0.375rem; - cursor: pointer; - box-shadow: none; - transition: background-color 0.2s, border-color 0.2s; - white-space: nowrap; -} - -.attach-browse-btn:hover { - background-color: #e2e8f0; - border-color: #94a3b8; -} - -.attach-path-hint { - display: flex; - align-items: flex-start; - gap: 0.5rem; - padding: 0.6rem 0.75rem; - background-color: #fffbeb; - border: 1px solid #fcd34d; - border-left: 3px solid #f59e0b; - border-radius: 0.375rem; - font-size: 0.825rem; - color: #92400e; - line-height: 1.45; -} - -.attach-path-hint i { - color: #f59e0b; - margin-top: 0.1rem; - flex-shrink: 0; -} - -.attach-path-hint code { - font-family: monospace; - background-color: rgba(245, 158, 11, 0.15); - padding: 0.05rem 0.25rem; - border-radius: 0.2rem; -} - -.attach-modal .hashing-spinner { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.9rem; - color: #3b82f6; -} - -.attach-modal .error-text { - color: #ef4444; - font-size: 0.85rem; - margin: 0; -} - -.attach-modal .modal-buttons { - display: flex; - justify-content: flex-end; - gap: 0.75rem; - margin-top: 0.5rem; -} - -.attach-modal .modal-buttons button { - padding: 0.5rem 1rem; - font-size: 0.9rem; - border-radius: 0.25rem; - border: none; - cursor: pointer; - transition: opacity 0.2s; -} - -.attach-modal .modal-buttons button:hover { - opacity: 0.9; -} - -/* ========================= Emoji Picker ========================= */ -.chat-hub-emoji-btn { - background-color: transparent; - border: none; - font-size: 1.3rem; - cursor: pointer; - padding: 0.35rem 0.4rem; - margin-right: 0.25rem; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - border-radius: 0.375rem; - line-height: 1; - transition: background-color 0.15s, transform 0.15s; - box-shadow: none; -} - -.chat-hub-emoji-btn:hover { - background-color: #f1f5f9; - transform: scale(1.1); -} - -.emoji-picker-wrapper { - position: relative; - flex-shrink: 0; - display: flex; - align-items: center; -} - -.emoji-picker { - position: absolute; - bottom: calc(100% + 0.5rem); - left: 0; - width: 320px; - background-color: #ffffff; - border: 1px solid #e2e8f0; - border-radius: 0.625rem; - box-shadow: 0 8px 30px -4px rgba(0, 0, 0, 0.18), 0 4px 12px -2px rgba(0, 0, 0, 0.1); - z-index: 3000; - display: flex; - flex-direction: column; - overflow: hidden; - animation: emoji-pop 0.15s ease-out; -} - -@keyframes emoji-pop { - from { opacity: 0; transform: scale(0.92) translateY(6px); } - to { opacity: 1; transform: scale(1) translateY(0); } -} - -.emoji-search-row { - display: flex; - align-items: center; - gap: 0.4rem; - padding: 0.6rem 0.75rem 0.4rem; - border-bottom: 1px solid #f1f5f9; -} - -.emoji-search-icon { - color: #94a3b8; - font-size: 0.8rem; - flex-shrink: 0; -} - -.emoji-search-input { - flex: 1; - border: 1px solid #e2e8f0; - border-radius: 0.375rem; - padding: 0.3rem 0.5rem; - font-size: 0.85rem; - outline: none; - background-color: #f8fafc; - transition: border-color 0.15s; -} - -.emoji-search-input:focus { - border-color: #3ba4d7; - background-color: #fff; -} - -.emoji-search-clear { - background: none; - border: none; - cursor: pointer; - color: #94a3b8; - padding: 0.2rem; - font-size: 0.8rem; - box-shadow: none; - display: flex; - align-items: center; -} - -.emoji-search-clear:hover { - color: #475569; -} - -.emoji-categories { - display: flex; - gap: 0.1rem; - padding: 0.35rem 0.5rem; - border-bottom: 1px solid #f1f5f9; - overflow-x: auto; - scrollbar-width: none; -} - -.emoji-categories::-webkit-scrollbar { - display: none; -} - -.emoji-cat-btn { - background: none; - border: none; - cursor: pointer; - font-size: 1.2rem; - padding: 0.3rem 0.35rem; - border-radius: 0.375rem; - line-height: 1; - box-shadow: none; - transition: background-color 0.1s; - flex-shrink: 0; -} - -.emoji-cat-btn:hover { - background-color: #f1f5f9; -} - -.emoji-cat-btn.active { - background-color: #e0f2fe; - box-shadow: inset 0 -2px 0 #3ba4d7; -} - -.emoji-grid { - display: grid; - grid-template-columns: repeat(7, 1fr); - gap: 0; - padding: 0.4rem 0.35rem; - max-height: 220px; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: #cbd5e1 transparent; -} - -.emoji-grid::-webkit-scrollbar { - width: 4px; -} - -.emoji-grid::-webkit-scrollbar-track { - background: transparent; -} - -.emoji-grid::-webkit-scrollbar-thumb { - background-color: #cbd5e1; - border-radius: 4px; -} - -.emoji-btn { - background: none; - border: none; - cursor: pointer; - font-size: 1.7rem; - padding: 0.25rem; - border-radius: 0.3rem; - line-height: 1; - box-shadow: none; - text-align: center; - transition: background-color 0.1s, transform 0.1s; - display: flex; - align-items: center; - justify-content: center; - aspect-ratio: 1; -} - -.emoji-btn:hover { - background-color: #f1f5f9; - transform: scale(1.2); -} - -table.mails th.sortable-th { - cursor: pointer; - user-select: none; - transition: background-color 0.2s, color 0.2s; -} - -table.mails th.sortable-th:hover { - background-color: #eef3f6; - color: #000; -} - -.compose-mail__from { - display: flex; - justify-content: flex-start; - align-items: center; - gap: 0.5rem; -} - -/* Status Bar Styles */ -.statusbar { - display: flex; - justify-content: space-between; - align-items: center; - height: 28px; - background-color: #14141b; - border-top: 1px solid #2e2e38; - padding: 0 1rem; - font-size: 0.8rem; - color: #94a3b8; - z-index: 100; - box-sizing: border-box; - user-select: none; - flex-shrink: 0; -} - -.statusbar-left { - display: flex; - align-items: center; -} - -.statusbar-right { - display: flex; - align-items: center; - gap: 1.5rem; -} - -.statusbar-item { - display: flex; - align-items: center; -} - -.statusbar-divider { - width: 1px; - height: 14px; - background-color: #2e2e38; -} - -.status-bullet { - width: 8px; - height: 8px; - border-radius: 50%; - display: inline-block; - box-shadow: 0 0 4px rgba(0, 0, 0, 0.5); -} - -/* Hidden Service Configuration layout overrides */ -.proxy-server-container { - width: 100%; - display: flex; - flex-direction: column; - gap: 1rem; -} - -.proxy-description { - color: #334155; - font-size: 0.95rem; - margin-bottom: 0.5rem; -} - -.proxy-rows-container { - display: flex; - flex-direction: column; - gap: 0.75rem; - width: 100%; -} - -.proxy-row { - display: grid; - grid-template-columns: 160px 220px 220px auto; - gap: 0.75rem; - align-items: center; - width: 100%; -} - -.proxy-label { - font-size: 0.95rem; - font-weight: 500; - color: #1e293b; -} - -.proxy-addr-input { - width: 100% !important; - max-width: none !important; -} - -.proxy-port-input { - width: 100% !important; - max-width: none !important; -} - -.proxy-status-container { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.proxy-status-bullet { - width: 14px; - height: 14px; - border-radius: 50%; - display: inline-block; - border: 1px solid #475569; -} - -.proxy-status-text { - font-size: 0.95rem; - color: #1e293b; -} - + */@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:auto;src:url("./webfonts/fa-solid-900.eot");src:url("./webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"),url("./webfonts/fa-solid-900.woff2") format("woff2"),url("./webfonts/fa-solid-900.woff") format("woff"),url("./webfonts/fa-solid-900.ttf") format("truetype"),url("./webfonts/fa-solid-900.svg#fontawesome") format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900}html{font-size:87.5%;box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}body,h1,h2,h3,h4,h5,h6,p,figure,blockquote,dl,dd{margin:0;padding:0}ul[role=list],ol[role=list]{list-style:none}html:focus-within{scroll-behavior:smooth}body{text-rendering:optimizeSpeed;line-height:1.5;font-family:"Roboto",Arial,Helvetica,sans-serif !important;letter-spacing:-0.025ch}a:not([class]){text-decoration-skip-ink:auto}img,picture{max-width:100%;display:block}input,button,textarea,select{font:inherit}@media(prefers-reduced-motion: reduce){html:focus-within{scroll-behavior:auto}*,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important;scroll-behavior:auto !important}}#main{height:100vh}.content{display:flex;height:100%;overflow:hidden}.tab-content{display:flex;height:100%;width:100%;background-color:#eef3f6;animation:fadein .3s;overflow:auto}input[type=text],input[type=password],input[type=number],textarea{box-sizing:border-box;background:#fff;max-width:100%;font-size:1rem;font-weight:400;border:1px solid #ccc;border-radius:.25rem;padding:.25rem .5rem;outline:rgba(0,0,0,0)}input:focus{border:1px solid #3ba4d7;box-shadow:inset 0 0 5px #ccc}input.stretched{width:90%}input.small{max-width:70%;padding:.1rem}input.searchbar{width:40%}a{cursor:pointer}a[title=Back]{width:max-content;height:max-content;padding:.475rem .75rem;border-radius:50%;transition:100ms}a[title=Back]:hover{background:#eef3f6}table{padding:20px;table-layout:fixed;width:100%;border-collapse:collapse;text-align:center;color:#333;font-size:1.125rem}table th{font-size:1.125rem;color:#000;border-bottom:2px solid #eee}table tr{border-bottom:1px solid #eee}h3{color:#444}hr{margin-left:0;color:#aaa}.grid-2col{display:grid;grid-template-columns:auto auto;gap:1rem;justify-content:start}.grid-2col input[type=checkbox]{margin-top:20px}.error{color:red}.tooltip{color:#333;position:relative;display:inline-block;margin:0 .25rem}.tooltiptext{visibility:hidden;position:absolute;top:100%;left:50%;min-width:250px;margin-left:-120px;z-index:1;color:#ccc;background-color:#333;font-size:.875rem;text-align:center;padding:.25rem;border-radius:.5rem}.tooltip:hover .tooltiptext{visibility:visible;animation:fadein .5s}blockquote{color:#14141b;padding:.75rem 1rem .75rem 2rem;border-radius:.25rem}blockquote.info{position:relative;line-height:1.2;color:rgba(20,20,27,.8);border:1px solid rgba(17,143,204,.8)}blockquote.info::before{font-family:"Font Awesome 5 Free";position:absolute;top:.5rem;left:.5rem;content:"";color:#019dff}@keyframes fadein{from{opacity:0}to{opacity:1}}.fadein{animation:fadein .5s}@keyframes swipe-from-left{from{margin-left:100%}to{margin-left:0}}button{width:max-content;height:max-content;color:#fff;background:#019dff;font-size:1rem;padding:.4rem 1rem;border:0;border-radius:5px;cursor:pointer;box-shadow:inset -3px -3px 0 rgb(0,94.5826771654,154)}button:active{outline:none;box-shadow:inset 3px 3px 0 rgb(0,94.5826771654,154)}button.red{width:max-content;height:max-content;color:#fff;background:#ff3a4a;font-size:1rem;padding:.4rem 1rem;border:0;border-radius:5px;cursor:pointer;box-shadow:inset -3px -3px 0 rgb(211,0,17.1370558376)}button.red:active{outline:none;box-shadow:inset 3px 3px 0 rgb(211,0,17.1370558376)}.media-item{display:flex;margin-top:.5rem;padding:1rem;border:1px solid rgba(20,20,27,.1);border-radius:4px}.media-item__details{flex-basis:40%;display:flex;align-items:start;gap:.5rem}.media-item__details img{width:6rem;object-fit:contain}.media-item__desc{flex-basis:60%}.active-link{background:hsla(0,0%,100%,.1) !important}.nav-menu{background-color:#14141b;box-shadow:0 5px 5px #222;display:flex;flex-direction:column;align-items:center;height:100%;padding:.5rem .25rem;margin-right:0rem}.nav-menu__logo{padding:1.2rem 0;display:flex;align-items:center;gap:.3rem}.nav-menu__logo img{width:1.6rem}.nav-menu__logo h5{line-height:1;color:#fff}.nav-menu__box{padding:2rem .125rem;display:flex;flex-direction:column;gap:.5rem;position:relative}.nav-menu__box .item{margin:0;padding:.675rem .5rem;width:10rem;display:flex;align-items:center;line-height:1;border-radius:.5rem;text-decoration:none;color:#ccc;text-transform:capitalize;transition:0ms}.nav-menu__box .item:hover{background-color:rgba(238,243,246,.15)}.nav-menu__box .item i.sidenav-icon{width:2.5rem;height:1.4rem;display:grid;place-items:center}.nav-menu__box .item.item-selected{color:#9bdaff;background-color:rgba(155,218,255,.15);font-weight:medium}.nav-menu__box button.toggle-nav{display:none;position:absolute;padding:0;top:0;right:-1rem;background:rgb(77.5,186.5157480315,255);width:1.5rem;height:1.5rem;aspect-ratio:1;justify-content:center;align-items:center;border-radius:50%;box-shadow:none}.nav-menu.collapsed .nav-menu__logo .logo-container{display:flex;flex-direction:column;align-items:center;gap:.5rem}.nav-menu.collapsed .nav-menu__logo .logo-container>*:not(img){display:block}.nav-menu.collapsed .nav-menu__logo .nav-menu__logo-text{display:none !important}.nav-menu.collapsed .nav-menu__box .item{padding:.675rem 0;width:2.5rem;justify-content:center;transition:300ms}.nav-menu.collapsed .nav-menu__box .item span,.nav-menu.collapsed .nav-menu__box .item p{display:none !important}.nav-menu.collapsed button i{rotate:180deg}.nav-menu:hover button.toggle-nav{display:flex}.sidebar{width:13rem;background-color:#fff;display:flex;flex-direction:column}.sidebar a{text-decoration:none;text-transform:capitalize;padding:1rem;cursor:pointer;color:#999}.sidebar a:hover{color:#222}.sidebar .selected-sidebar-link{font-weight:bold;color:#222;border-left:5px solid #3ba4d7;animation:expand-left-border .1s}.sidebarquickview>h6{padding:.5rem}.sidebarquickview a{text-decoration:none;text-transform:capitalize;padding:.5rem 1rem;display:block;color:#999}.sidebarquickview a a:hover{color:#222}.sidebarquickview .selected-sidebarquickview-link{font-weight:bold;color:#222;border-left:5px solid #3ba4d7;animation:expand-left-border .1s}.node-panel{width:100%;padding:.5rem;animation:fadein .5s}@keyframes expand-left-border{from{border-left:0}to{border-left:5px solid #3ba4d7}}@media(max-width: 700px){.tab-content{flex-direction:column}.sidebar{width:100% !important;flex-direction:row !important;overflow-x:auto !important;overflow-y:hidden !important;white-space:nowrap !important;border-bottom:1px solid rgba(20,20,27,.1) !important;background:#fff !important;z-index:50 !important;flex-shrink:0 !important;height:auto !important;padding:0 !important}.sidebar a{display:inline-block !important;padding:.8rem 1.2rem !important;border-bottom:3px solid rgba(0,0,0,0) !important;border-left:none !important}.sidebar .selected-sidebar-link{border-left:none !important;border-bottom:3px solid #3ba4d7 !important;animation:none !important}.sidebarquickview>h4,.sidebarquickview>h6{display:none !important}}.posts{height:100%;margin-top:1rem;flex-direction:column;overflow:auto}.posts__heading{display:flex;flex-direction:column;justify-content:space-between}.posts-container{height:100%;padding:1rem;display:grid;grid-template-columns:repeat(auto-fill, minmax(150px, 1fr));gap:2rem;border:1px solid rgba(20,20,27,.1);border-radius:4px;overflow:auto}.posts-container-card{min-height:240px;flex-direction:column;border:1px solid rgba(20,20,27,.5);border-radius:4px;cursor:pointer;text-align:center}.posts-container-card img{flex-basis:90%;object-fit:cover}.posts-container-card p{padding:0 .125rem;flex-basis:10%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.progress-bar{width:100%;height:2rem;position:relative;text-align:center;background-color:#eef3f6;border-radius:20px;overflow:hidden}.progress-bar__status{position:absolute;top:0;left:0;height:100%;color:#14141b;background-color:#019dff}.progress-bar__percent{position:absolute;inset:0;margin:auto;width:fit-content;height:fit-content}.progress-bar-chunks{position:relative;margin-top:.5rem;width:100%;height:2rem;display:flex;border-radius:.25rem;overflow:hidden;background-color:#eef3f6}.progress-bar-chunks .chunk{width:100%}.progress-bar-chunks .chunk[data-chunkVal="0"]{background-color:rgba(155,218,255,.2)}.progress-bar-chunks .chunk[data-chunkVal="1"]{background-color:#ff3a4a}.progress-bar-chunks .chunk[data-chunkVal="2"]{background-color:#019dff}.progress-bar-chunks .chunk[data-chunkVal="3"]{background-color:#fcba03}.progress-bar-chunks__percent{position:absolute;inset:0;margin:auto;width:fit-content;height:fit-content}.widget{height:100%;padding:1rem;display:flex;flex-direction:column;gap:.5rem;background-color:#fff;border-radius:.5rem;overflow:auto}.widget .top-heading{display:flex;justify-content:space-between}.widget__heading{display:flex;justify-content:space-between;align-items:center;border-bottom:2px solid #999}.widget__body{height:100%;display:flex;flex-direction:column;overflow:auto}.widget__body-heading{display:flex;justify-content:space-between;align-items:center}.widget__body-heading .action{display:flex;gap:.5rem}.widget__body-content{height:100%;overflow:auto}.widget__body-box{display:flex;flex-direction:column;gap:.5rem}.widget-half{max-width:50%}#modal-container{display:none;position:fixed;z-index:1;height:100%;top:0;left:0;width:100%;background-color:rgba(0,0,0,.2)}.modal-content{position:absolute;color:#555;width:40%;min-height:10rem;height:max-content;padding:1.5rem;inset:0;margin:auto;background-color:#fff;border-radius:.5rem;animation:fadein .5s;display:flex;flex-direction:column}.modal-content button:last-child{margin-top:auto}.modal-content .close-btn{position:absolute;right:1.5rem}.modal-content .widget{padding:0}#notification-container{position:absolute;bottom:0;right:0}.login-page{background-image:linear-gradient(-45deg, rgba(1, 157, 255, 0.75), rgba(17, 143, 204, 0.75));height:100%;animation:fadein .5s}.login-page .login-container{background-color:#fff;box-shadow:3px 3px 5px rgba(20,20,27,.4);margin:auto;position:relative;top:100px;max-width:400px;max-height:500px;border-radius:5px;display:flex;flex-direction:column;align-items:center}.login-page .login-container input{padding:.375rem .75rem;border-radius:.275rem}.login-page .login-container *{margin-bottom:1rem}.login-page .login-container>img{margin:1rem 0 2rem}.login-page .login-container extra{margin:0}.login-page .login-container>a{text-decoration:underline;cursor:pointer}.login-page .extra>label,.login-page .extra>br,.login-page .extra>input{margin-bottom:0}.homepage{margin:2rem auto 0;display:flex;flex-direction:column;gap:4rem}.homepage .logo{display:flex;justify-content:center;align-items:center}.homepage .logo img{width:90px}.homepage .logo .retroshareText{display:flex;flex-direction:column;align-items:center}.homepage .logo .retroshareText .retrotext{font-size:36px;font-weight:600;line-height:1.125}.homepage .logo .retroshareText .retrotext>span{color:#118fcc}.homepage .logo .retroshareText>b{font-size:14px;line-height:1}.homepage .certificate{display:flex;flex-direction:column;gap:4rem}.homepage .certificate__heading{text-align:center}.homepage .certificate__heading>h1{margin-bottom:1rem}.homepage .certificate__content{display:flex;flex-direction:column;gap:2rem;padding:2rem;text-align:center;border:1.5px solid rgba(17,143,204,.2);border-radius:6px;box-shadow:0px 0px 8px 2px rgba(20,20,27,.05)}.homepage .certificate__content .rsId>p{margin-bottom:.5rem;color:#118fcc}.homepage .certificate__content .retroshareID{padding:.25rem;display:flex;align-items:center;justify-self:start;font-size:1.25rem;border-radius:4px;background:rgba(20,20,27,.05)}.homepage .certificate__content .retroshareID .textArea{padding:0;width:100%;height:auto;font-size:1rem;font-family:monospace;background:rgba(0,0,0,0);border:none;resize:none;overflow:hidden;field-sizing:content}.homepage .certificate__content .retroshareID i{color:#118fcc}.homepage .certificate__content .retroshareID>i{margin:0 .5rem;cursor:pointer}.homepage .certificate__content .webhelp{padding:.5rem;background:#f5f5f5;display:flex;justify-content:center;align-items:center;gap:.5rem;border-radius:4px;border:1px solid rgba(20,20,27,.5);width:fit-content;cursor:pointer}.homepage .certificate__content .webhelp-container{display:grid;place-items:center}.homepage .certificate__content .webhelp:hover{background:#eef3f6;border:1px solid #14141b}.homepage .certificate__content .webhelp>i{font-size:1.2rem;color:green}.homepage .certificate__content .add-friend>h6,.homepage .certificate__content .webhelp-container>h6{font-weight:normal;margin-bottom:.5rem}.network-container{display:flex;height:100%;width:100%;overflow:hidden;background-color:#f1f5f9}.network-left-pane{width:320px;min-width:300px;max-width:350px;border-right:1px solid #cbd5e1;display:flex;flex-direction:column;background:#fff;box-shadow:2px 0 5px rgba(0,0,0,.05)}.own-profile-card{padding:1.25rem;border-bottom:1px solid #e2e8f0;background:linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);display:flex;flex-direction:column;gap:.75rem}.own-profile-card .profile-header{display:flex;align-items:center;gap:1rem}.own-profile-card .profile-info{display:flex;flex-direction:column;flex:1;overflow:hidden}.own-profile-card .profile-info .profile-name{font-weight:700;color:#1e293b;font-size:1.1rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.own-profile-card .profile-info .profile-status{font-size:.85rem;color:#10b981;font-weight:500;display:flex;align-items:center;gap:.35rem}.own-profile-card .profile-info .profile-status::before{content:"";display:inline-block;width:8px;height:8px;background-color:#10b981;border-radius:50%}.own-profile-card .own-identity-select-container{display:flex;flex-direction:column;gap:.25rem}.own-profile-card .own-identity-select-container label{font-size:.75rem;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:.05em}.own-profile-card .own-identity-select-container select.own-identity-select{width:100%;padding:.375rem .5rem;font-size:.85rem;border:1px solid #cbd5e1;border-radius:.375rem;background-color:#fff;color:#334155;outline:none;cursor:pointer;transition:border-color .2s}.own-profile-card .own-identity-select-container select.own-identity-select:focus{border-color:#3ba4d7}.friends-list-container{flex:1;display:flex;flex-direction:column;overflow:hidden}.friends-list-container .searchbar-container{padding:.75rem 1rem;border-bottom:1px solid #e2e8f0}.friends-list-container .searchbar-container input.searchbar{width:100%;padding:.5rem .75rem;font-size:.9rem;border:1px solid #cbd5e1;border-radius:.375rem;background-color:#f8fafc;outline:none;transition:all .2s}.friends-list-container .searchbar-container input.searchbar:focus{background-color:#fff;border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.friends-list-container .friends-scroll{flex:1;overflow-y:auto;padding:.5rem 0}.friend-list-item{display:flex;align-items:center;gap:.75rem;padding:.75rem 1rem;margin:.125rem .5rem;border-radius:.5rem;cursor:pointer;transition:all .2s}.friend-list-item:hover{background-color:#f1f5f9}.friend-list-item.selected{background-color:#e0f2fe}.friend-list-item.selected .friend-name{color:#0369a1;font-weight:600}.friend-list-item .friend-avatar{flex-shrink:0}.friend-list-item .friend-meta{flex:1;min-width:0}.friend-list-item .friend-meta .friend-name{font-size:.95rem;color:#334155;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .2s}.friend-list-item .friend-meta .friend-status{font-size:.8rem;color:#94a3b8}.friend-list-item .friend-meta .friend-status.online{color:#10b981;font-weight:500}.network-right-pane{flex:1;display:flex;flex-direction:column;overflow:hidden;background-color:#f8fafc}.network-pane-placeholder{flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;color:#94a3b8;gap:1rem;padding:2rem;text-align:center}.network-pane-placeholder i{font-size:4rem;color:#cbd5e1}.network-pane-placeholder p{font-size:1.1rem;max-width:400px}.network-tabs{display:flex;background-color:#fff;border-bottom:1px solid #cbd5e1;padding:.5rem 1rem 0;gap:.5rem}.network-tabs .tab-btn{padding:.625rem 1.25rem;font-size:.95rem;font-weight:600;color:#64748b;background:rgba(0,0,0,0);border:none;border-radius:.375rem .375rem 0 0;border-bottom:3px solid rgba(0,0,0,0);cursor:pointer;box-shadow:none;transition:all .2s}.network-tabs .tab-btn:hover{color:#334155;background-color:#f1f5f9}.network-tabs .tab-btn.active{color:#3ba4d7;border-bottom-color:#3ba4d7;background-color:rgba(0,0,0,0)}.network-tab-content{flex:1;overflow-y:auto;padding:1.5rem}.network-detail-view{display:flex;flex-direction:column;gap:1.5rem}.network-detail-view .detail-header{display:flex;align-items:center;gap:1.5rem;padding-bottom:1.5rem;border-bottom:1px solid #e2e8f0}.network-detail-view .detail-header .detail-title{flex:1}.network-detail-view .detail-header .detail-title h2{font-size:1.75rem;font-weight:800;color:#1e293b;margin-bottom:.25rem}.network-detail-view .detail-header .detail-title .detail-subtitle{font-size:.9rem;color:#64748b;display:flex;align-items:center;gap:.5rem}.network-detail-view .detail-header .detail-actions{display:flex;gap:.75rem}.network-detail-view .detail-header .detail-actions button{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.9rem}.network-detail-view .detail-section{background-color:#fff;border-radius:.5rem;border:1px solid #e2e8f0;padding:1.25rem;box-shadow:0 1px 3px rgba(0,0,0,.02)}.network-detail-view .detail-section h3{font-size:1.1rem;font-weight:700;color:#334155;margin-bottom:1rem;padding-bottom:.5rem;border-bottom:1px solid #f1f5f9}.network-detail-view .detail-section .info-grid{display:grid;grid-template-columns:120px 1fr;row-gap:.75rem;font-size:.9rem}.network-detail-view .detail-section .info-grid .info-label{font-weight:600;color:#64748b}.network-detail-view .detail-section .info-grid .info-value{color:#1e293b;word-break:break-all}.network-detail-view .locations-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));gap:1rem}.network-detail-view .location-card{background-color:#fff;border:1px solid #e2e8f0;border-radius:.5rem;padding:1rem;display:flex;flex-direction:column;gap:.5rem;box-shadow:0 1px 3px rgba(0,0,0,.02)}.network-detail-view .location-card .loc-header{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #f1f5f9;padding-bottom:.5rem;margin-bottom:.25rem}.network-detail-view .location-card .loc-header .loc-name{font-weight:700;color:#334155;font-size:.95rem}.network-detail-view .location-card .loc-header .loc-status{font-size:.75rem;font-weight:600;padding:.125rem .5rem;border-radius:.25rem}.network-detail-view .location-card .loc-header .loc-status.online{background-color:#d1fae5;color:#065f46}.network-detail-view .location-card .loc-header .loc-status.offline{background-color:#f1f5f9;color:#475569}.network-detail-view .location-card .loc-body{font-size:.85rem;display:grid;grid-template-columns:80px 1fr;row-gap:.25rem}.network-detail-view .location-card .loc-body .loc-label{color:#64748b}.network-detail-view .location-card .loc-body .loc-val{color:#334155;word-break:break-all}.network-detail-view .location-card .loc-footer{margin-top:.5rem;display:flex;justify-content:flex-end}.network-detail-view .location-card .loc-footer button{font-size:.8rem;padding:.25rem .75rem}.network-chat-view{display:flex;flex-direction:column;height:100%;overflow:hidden;background-color:#f8fafc}.network-chat-view .chat-messages{flex:1;overflow-y:auto;padding:1.25rem;display:flex;flex-direction:column;gap:1rem}.network-chat-view .chat-bubble-container{display:flex;flex-direction:column;max-width:70%}.network-chat-view .chat-bubble-container.outgoing{align-self:flex-end;align-items:flex-end}.network-chat-view .chat-bubble-container.outgoing .chat-bubble{background-color:#3ba4d7;color:#fff;border-bottom-right-radius:.125rem}.network-chat-view .chat-bubble-container.incoming{align-self:flex-start;align-items:flex-start}.network-chat-view .chat-bubble-container.incoming .chat-bubble{background-color:#fff;color:#1e293b;border:1px solid #e2e8f0;border-bottom-left-radius:.125rem}.network-chat-view .chat-bubble-container .chat-sender{font-size:.75rem;color:#64748b;margin-bottom:.25rem;padding:0 .25rem}.network-chat-view .chat-bubble-container .chat-bubble{padding:.625rem .875rem;border-radius:.75rem;font-size:.925rem;line-height:1.4;white-space:break-spaces;word-break:break-word;box-shadow:0 1px 2px rgba(0,0,0,.05)}.network-chat-view .chat-bubble-container .chat-time{font-size:.7rem;color:#94a3b8;margin-top:.25rem;padding:0 .25rem}.network-chat-view .chat-input-area{padding:1rem;background-color:#fff;border-top:1px solid #cbd5e1;display:flex;gap:.75rem;align-items:center}.network-chat-view .chat-input-area textarea.chat-textarea{flex:1;resize:none;height:40px;padding:.5rem .75rem;border:1px solid #cbd5e1;border-radius:.375rem;font-size:.9rem;outline:none;transition:all .2s}.network-chat-view .chat-input-area textarea.chat-textarea:focus{border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.network-chat-view .chat-input-area button.send-btn{padding:.5rem 1.25rem;font-size:.9rem;height:40px;display:flex;align-items:center;gap:.5rem}.network-chat-view .chat-warning{flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;color:#64748b;text-align:center;padding:2rem;gap:1rem}.network-chat-view .chat-warning i{font-size:3rem;color:#cbd5e1}.network-chat-view .chat-warning h4{font-weight:700;color:#334155}.network-chat-view .chat-warning p{max-width:350px;font-size:.9rem}.identity{color:#444;font-size:1.1em;margin:20px;padding:10px;border:1px solid #aaa;border-radius:20px}.identity>h4{margin:5px;font-size:1.3em}.identity button{font-size:.9em}.identity .details{display:grid;grid-template-columns:140px auto;grid-row-gap:5px;justify-content:left}.defaultAvatar{width:3rem;height:3rem;aspect-ratio:1;background:#b0c4de;border-radius:50%;display:grid;place-items:center}.defaultAvatar p{font-weight:900;color:#666f7f;transform:translateY(1px)}img.avatar{display:block;width:3rem;height:max-content;aspect-ratio:1;margin-right:.3em;border-radius:50%}.counter{margin-left:.5em}.counter:before{content:"("}.counter:after{content:")"}.chatInit{margin-left:.5em;color:green;cursor:pointer}.lobby{margin:10px;border:1px solid #aaa;border-radius:20px}.lobby .mainname{margin:20px;font-weight:100;font-size:1.2em}.topic{color:#666}.lobby>.topic{font-size:.95em;margin-left:25px;margin-bottom:5px}.lefttitle{margin-top:15px;margin-bottom:0;font-weight:100;font-size:1.2em}.leftname{margin-top:5px;margin-bottom:5px;padding:5px;font-weight:100;font-size:1em}.leftlobby>.topic{font-size:.75em;margin-left:15px;margin-bottom:5px}.subscribed,.public{cursor:pointer}.leftlobby{border:1px solid #aaa;border-radius:10px;margin-top:5px;background-color:#fff}.leftlobby.selected-lobby,.selectedidentity{color:#fff;background-color:#3ba4d7}.rightbar{position:absolute;width:185px;background-color:#fff;overflow:auto;top:130px;bottom:15px;right:15px}.user{padding:5px}.lobbyName{padding:15px;margin-top:2rem}.lobbies{position:absolute;width:185px;left:165px;bottom:15px;top:130px;overflow:auto}.messages,.setup{position:absolute;background-color:#fff;top:130px;left:360px;right:215px;overflow:auto}.messages{bottom:115px}.messagetext{white-space:break-spaces;margin-right:5px}.message>*{margin-left:5px}.username{color:#006400;font-weight:bolder}.chatMessage{position:absolute;background-color:#fff;height:85px;bottom:15px;right:215px;left:360px}textarea.chatMsg{height:100%;width:100%}.chatatchar{margin-left:.2em;margin-right:.2em;color:silver}.setupicon{margin-left:1em;cursor:pointer}.leaveicon{margin-left:1em;cursor:pointer;color:#d40000}.selectidentity{margin:15px;font-size:1.2em}.setup>.identity{cursor:pointer}.setup{bottom:15px}.createDistantChat{margin-top:1em}.no-lobbies .messages,.no-lobbies .chatMessage,.no-lobbies .setup{left:165px}@media(min-width: 900px){.node-panel.chat-room{display:grid !important;grid-template-columns:250px 1fr 200px !important;grid-template-rows:auto 1fr auto !important;grid-template-areas:"lobbies header rightbar" "lobbies messages rightbar" "lobbies input rightbar" !important;padding:0 !important;height:100% !important}.node-panel.chat-room .lobbyName{grid-area:header;padding:10px;border-bottom:1px solid #eee;margin:0;z-index:10;background:#fff}.node-panel.chat-room .lobbies{grid-area:lobbies;position:static !important;width:auto !important;height:auto !important;border-right:1px solid #ccc;overflow-y:auto;display:block !important;top:auto !important;bottom:auto !important;left:auto !important}.node-panel.chat-room .messages{grid-area:messages;position:static !important;width:auto !important;height:auto !important;overflow-y:auto;padding:10px;left:auto !important;right:auto !important;top:auto !important;bottom:auto !important;margin:0 !important}.node-panel.chat-room .rightbar{grid-area:rightbar;position:static !important;width:auto !important;border-left:1px solid #ccc;overflow-y:auto;display:block !important}.node-panel.chat-room .chatMessage{grid-area:input;position:static !important;width:auto !important;height:auto !important;border-top:1px solid #eee;left:auto !important;right:auto !important;bottom:auto !important;flex:0 0 auto;padding:10px !important;background:#fff;z-index:10}}@media(max-width: 899px){.node-panel.chat-room{display:flex !important;flex-direction:column !important;height:100% !important;position:relative !important}.node-panel.chat-room .lobbyName{flex:0 0 auto}.node-panel.chat-room .messages{flex:1 !important;overflow-y:auto !important;position:relative !important;top:0 !important;bottom:0 !important;left:0 !important;right:0 !important;width:100% !important;height:auto !important;margin:0 !important}.node-panel.chat-room .chatMessage{flex:0 0 auto !important;position:relative !important;bottom:0 !important;left:0 !important;right:0 !important;width:100% !important;height:auto !important;z-index:100}.node-panel.chat-room .rightbar,.node-panel.chat-room .lobbies{display:none !important;position:fixed !important;top:60px !important;bottom:0 !important;width:80% !important;background:#fff !important;z-index:200 !important;box-shadow:2px 0 10px rgba(0,0,0,.2) !important}.node-panel.chat-room.show-lobbies .lobbies{display:block !important;left:0 !important}.node-panel.chat-room.show-users .rightbar{display:block !important;right:0 !important}.chat-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.4);z-index:150}.show-lobbies .chat-overlay,.show-users .chat-overlay{display:block}.mobile-menu-icons{display:flex;gap:15px;font-size:1.2rem}.mobile-menu-icons i{cursor:pointer;padding:5px}}@media(min-width: 900px){.mobile-menu-icons{display:none}}.chat-hub-container{display:flex;height:100%;width:100%;overflow:hidden;background-color:#f1f5f9}.chat-hub-left-pane{width:320px;min-width:300px;max-width:350px;border-right:1px solid #cbd5e1;display:flex;flex-direction:column;background:#fff;box-shadow:2px 0 5px rgba(0,0,0,.05)}.chat-own-profile-card{padding:1.25rem;border-bottom:1px solid #e2e8f0;background:linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)}.chat-own-profile-card .profile-header{display:flex;align-items:center;gap:1rem}.chat-own-profile-card .profile-info{display:flex;flex-direction:column;flex:1;overflow:hidden}.chat-own-profile-card .profile-info .profile-name{font-weight:700;color:#1e293b;font-size:1.1rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-own-profile-card .profile-info .profile-status{font-size:.85rem;color:#10b981;font-weight:500;display:flex;align-items:center;gap:.35rem}.chat-own-profile-card .profile-info .profile-status::before{content:"";display:inline-block;width:8px;height:8px;background-color:#10b981;border-radius:50%}.chat-rooms-list-container{flex:1;display:flex;flex-direction:column;overflow:hidden}.chat-rooms-list-container .searchbar-container{padding:.75rem 1rem;border-bottom:1px solid #e2e8f0}.chat-rooms-list-container .searchbar-container input.searchbar{width:100%;padding:.5rem .75rem;font-size:.9rem;border:1px solid #cbd5e1;border-radius:.375rem;background-color:#f8fafc;outline:none;transition:all .2s}.chat-rooms-list-container .searchbar-container input.searchbar:focus{background-color:#fff;border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.chat-rooms-list-container .rooms-scroll{flex:1;overflow-y:auto;padding:.5rem 0}.rooms-section-title{display:flex;align-items:center;gap:.5rem;padding:.75rem 1rem .375rem;font-size:.75rem;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.05em}.rooms-section-title i{font-size:.7rem;color:#94a3b8}.chat-room-list-item{display:flex;align-items:center;gap:.75rem;padding:.75rem 1rem;margin:.125rem .5rem;border-radius:.5rem;cursor:pointer;transition:all .2s}.chat-room-list-item:hover{background-color:#f1f5f9}.chat-room-list-item.selected{background-color:#e0f2fe}.chat-room-list-item.selected .room-name{color:#0369a1;font-weight:600}.chat-room-list-item .room-icon{flex-shrink:0;width:36px;height:36px;border-radius:.5rem;background:linear-gradient(135deg, #3ba4d7, #0ea5e9);display:flex;align-items:center;justify-content:center;color:#fff;font-size:.85rem}.chat-room-list-item.public-room .room-icon{background:linear-gradient(135deg, #10b981, #059669)}.chat-room-list-item .room-meta{flex:1;min-width:0}.chat-room-list-item .room-meta .room-name{font-size:.95rem;color:#334155;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .2s}.chat-room-list-item .room-meta .room-topic{font-size:.8rem;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-room-list-item .room-badge{flex-shrink:0;min-width:24px;height:24px;border-radius:12px;background-color:#e2e8f0;color:#475569;font-size:.75rem;font-weight:700;display:flex;align-items:center;justify-content:center;padding:0 .375rem}.chat-hub-right-pane{flex:1;display:flex;flex-direction:column;overflow:hidden;background-color:#f8fafc}.chat-pane-placeholder{flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;color:#94a3b8;gap:1rem;padding:2rem;text-align:center}.chat-pane-placeholder i{font-size:4rem;color:#cbd5e1}.chat-pane-placeholder p{font-size:1.1rem;max-width:400px}.chat-hub-tab-content{flex:1;overflow-y:auto;padding:1.5rem}.chat-room-detail-view{display:flex;flex-direction:column;gap:1.5rem}.chat-room-detail-view .detail-header{display:flex;align-items:flex-start;gap:1.5rem;padding-bottom:1.5rem;border-bottom:1px solid #e2e8f0;flex-wrap:wrap}.chat-room-detail-view .detail-header .detail-title{flex:1;min-width:200px}.chat-room-detail-view .detail-header .detail-title h2{font-size:1.75rem;font-weight:800;color:#1e293b;margin-bottom:.25rem}.chat-room-detail-view .detail-header .detail-title .detail-subtitle{font-size:.9rem;color:#64748b;display:flex;align-items:center;gap:.5rem}.chat-room-detail-view .detail-header .detail-actions{display:flex;gap:.75rem;flex-wrap:wrap}.chat-room-detail-view .detail-header .detail-actions button{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.9rem}.chat-room-detail-view .detail-section{background-color:#fff;border-radius:.5rem;border:1px solid #e2e8f0;padding:1.25rem;box-shadow:0 1px 3px rgba(0,0,0,.02)}.chat-room-detail-view .detail-section h3{font-size:1.1rem;font-weight:700;color:#334155;margin-bottom:1rem;padding-bottom:.5rem;border-bottom:1px solid #f1f5f9}.chat-room-detail-view .detail-section .info-grid{display:grid;grid-template-columns:130px 1fr;row-gap:.75rem;font-size:.9rem}.chat-room-detail-view .detail-section .info-grid .info-label{font-weight:600;color:#64748b}.chat-room-detail-view .detail-section .info-grid .info-value{color:#1e293b;word-break:break-all}.participants-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(180px, 1fr));gap:.5rem}.participant-card{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;background-color:#f8fafc;border:1px solid #e2e8f0;border-radius:.375rem}.participant-card .participant-name{font-size:.875rem;color:#334155;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.no-participants{color:#94a3b8;font-size:.9rem;font-style:italic}.detail-actions-footer{display:flex;gap:.75rem}.detail-actions-footer button{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.9rem}.join-description{color:#64748b;font-size:.9rem;margin-bottom:1rem}.identities-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(200px, 1fr));gap:.75rem}.identity-card{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1rem;background-color:#f8fafc;border:1px solid #e2e8f0;border-radius:.5rem;cursor:pointer;transition:all .2s}.identity-card:hover{background-color:#e0f2fe;border-color:#3ba4d7}.identity-card .identity-name{font-size:.95rem;font-weight:600;color:#334155}.identity-card i{color:#3ba4d7;font-size:.9rem}.no-rooms{padding:1rem;color:#94a3b8;text-align:center;font-style:italic}@media(max-width: 899px){.chat-hub-container{flex-direction:column}.chat-hub-left-pane{width:100%;min-width:0;max-width:none;max-height:45%;border-right:none;border-bottom:1px solid #cbd5e1}.chat-hub-right-pane{flex:1;min-height:0}}.chat-hub-header-bar{padding:.75rem 1.5rem;background-color:#fff;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;justify-content:space-between;height:65px;flex-shrink:0}.chat-hub-header-bar .chat-header-info{display:flex;flex-direction:column;overflow:hidden}.chat-hub-header-bar .chat-header-info .chat-header-name{font-size:1.15rem;font-weight:800;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-hub-header-bar .chat-header-info .chat-header-topic{font-size:.85rem;color:#64748b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:.125rem}.chat-hub-header-bar .chat-header-actions{display:flex;gap:.5rem}.chat-hub-header-bar .chat-header-actions button{display:flex;align-items:center;gap:.35rem;padding:.375rem .75rem;font-size:.85rem}.chat-hub-tabs-container{background-color:#fff;border-bottom:1px solid #cbd5e1;padding:.5rem 1.5rem 0}.chat-hub-tabs{display:flex;gap:.5rem}.chat-hub-tabs .tab-btn{padding:.625rem 1.25rem;font-size:.95rem;font-weight:600;color:#64748b;background:rgba(0,0,0,0);border:none;border-radius:.375rem .375rem 0 0;border-bottom:3px solid rgba(0,0,0,0);cursor:pointer;box-shadow:none;transition:all .2s;display:flex;align-items:center;gap:.5rem}.chat-hub-tabs .tab-btn:hover{color:#334155;background-color:#f1f5f9}.chat-hub-tabs .tab-btn.active{color:#3ba4d7;border-bottom-color:#3ba4d7;background-color:rgba(0,0,0,0)}.chat-hub-tab-content{flex:1;display:flex;flex-direction:column;overflow:hidden;background-color:#f8fafc}.chat-hub-conversation-layout{display:flex;flex-direction:row;height:100%;width:100%;overflow:hidden}.chat-hub-conversation-main{display:flex;flex-direction:column;flex:1;height:100%;overflow:hidden}.chat-hub-rightbar{width:200px;border-left:1px solid #cbd5e1;background-color:#fff;display:flex;flex-direction:column;flex-shrink:0}.chat-hub-rightbar .rightbar-title{padding:.75rem 1rem;font-size:.85rem;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid #e2e8f0}.chat-hub-rightbar .rightbar-users-list{flex:1;overflow-y:auto;padding:.5rem}.chat-hub-rightbar .user{padding:.5rem .75rem;font-size:.9rem;color:#334155;border-radius:.375rem;transition:all .2s;display:flex;align-items:center;gap:.5rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-hub-rightbar .user:hover{background-color:#f1f5f9;color:#0f172a}.chat-hub-rightbar .user .defaultAvatar{width:2rem;height:2rem;font-size:.9rem;flex-shrink:0}.chat-hub-rightbar .user img.avatar{width:2rem;height:2rem;flex-shrink:0}@media(max-width: 899px){.chat-hub-rightbar{display:none}}.chat-hub-messages{flex:1;overflow-y:auto;padding:1.25rem 1.5rem;display:flex;flex-direction:column;gap:1rem}.chat-hub-messages .message{display:flex;flex-direction:column;max-width:70%;padding:.625rem .875rem;border-radius:.75rem;font-size:.925rem;line-height:1.4;word-break:break-word;box-shadow:0 1px 2px rgba(0,0,0,.05)}.chat-hub-messages .message.incoming{align-self:flex-start;align-items:flex-start;background-color:#fff;color:#1e293b;border:1px solid #e2e8f0;border-bottom-left-radius:.125rem}.chat-hub-messages .message.outgoing{align-self:flex-end;align-items:flex-end;background-color:#3ba4d7;color:#fff;border-bottom-right-radius:.125rem}.chat-hub-messages .message .username{font-size:.75rem;margin-bottom:.25rem;padding:0 .125rem;font-weight:700}.chat-hub-messages .message.incoming .username{color:#0369a1}.chat-hub-messages .message.outgoing .username{color:#e0f2fe}.chat-hub-messages .message .messagetext{white-space:break-spaces;margin:0}.chat-hub-messages .message .datetime{font-size:.7rem;margin-top:.25rem;padding:0 .125rem;opacity:.8}.chat-hub-messages .message.incoming .datetime{color:#64748b}.chat-hub-messages .message.outgoing .datetime{color:#f1f5f9}.chat-hub-input-area{padding:1rem 1.5rem;background-color:#fff;border-top:1px solid #cbd5e1;display:flex;gap:.75rem;align-items:center;flex-shrink:0}.chat-hub-input-area textarea.chat-hub-textarea{flex:1;resize:none;height:40px;padding:.5rem .75rem;border:1px solid #cbd5e1;border-radius:.375rem;font-size:.9rem;outline:none;transition:all .2s;background-color:#f8fafc}.chat-hub-input-area textarea.chat-hub-textarea:focus{background-color:#fff;border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.chat-hub-input-area button.chat-hub-send-btn{padding:.5rem 1.25rem;font-size:.9rem;height:40px;display:flex;align-items:center;gap:.5rem;border-radius:.375rem}.chat-hub-messages.compact-container,.messages.compact-container{gap:0 !important;padding:.75rem 1rem !important;background-color:#fff !important;display:flex !important;flex-direction:column !important}.chat-hub-messages.compact-container .message.compact,.messages.compact-container .message.compact{display:block !important;max-width:100% !important;padding:.1rem 0 !important;border-radius:0 !important;background-color:rgba(0,0,0,0) !important;border:none !important;box-shadow:none !important;align-self:flex-start !important;font-size:.875rem !important;line-height:1.45 !important;margin:0 !important;white-space:nowrap !important}.chat-hub-messages.compact-container .message.compact:hover,.messages.compact-container .message.compact:hover{background-color:#f8fafc !important;overflow:visible !important;white-space:normal !important}.chat-hub-messages.compact-container .message.compact .datetime,.messages.compact-container .message.compact .datetime{color:#a0a0a0 !important;margin-right:.4rem !important;font-size:.78rem !important;font-family:monospace !important;opacity:1 !important;display:inline !important}.chat-hub-messages.compact-container .message.compact .username,.messages.compact-container .message.compact .username{font-weight:bold !important;margin-right:.2rem !important;font-size:.875rem !important;display:inline !important}.chat-hub-messages.compact-container .message.compact .messagetext,.messages.compact-container .message.compact .messagetext{color:#1e293b !important;white-space:normal !important;word-break:break-word !important;display:inline !important;margin:0 !important}.side-bar{display:flex;flex-direction:column;background:#fff}.side-bar .mail-compose-btn{width:96%;margin:.25rem;padding:.75rem 0}.compose-mail__from{display:flex;justify-content:flex-start;align-items:center;gap:.5rem;padding-bottom:.5rem;border-bottom:2px solid #eef3f6}.compose-mail__recipients{padding:.5rem 0;display:flex;flex-direction:column;gap:.5rem;border-bottom:2px solid #eef3f6}.compose-mail__recipients__container{display:flex;gap:.5rem}.compose-mail__recipients__container>label{text-transform:capitalize}.compose-mail__recipients__container .recipients{width:100%;display:flex;gap:.5rem;flex-wrap:wrap}.compose-mail__recipients__container .recipients__selected{padding:.125rem .5rem;display:flex;align-items:center;gap:.5rem;border:1px solid #eef3f6;border-radius:3px;cursor:default}.compose-mail__recipients__container .recipients__selected i{cursor:pointer;padding:.25rem}.compose-mail__recipients__container .recipients__input{display:flex;position:relative;flex-grow:1}.compose-mail__recipients__container .recipients__input-field{flex-grow:1;min-width:200px;padding:0;border:none;box-shadow:none}.compose-mail__recipients__container .recipients__input-field:focus+.recipients__input-list{display:flex}.compose-mail__recipients__container .recipients__input-list{z-index:1;position:absolute;top:1rem;padding:0;width:100%;max-height:15rem;flex-direction:column;overflow:auto;display:none;background:#fff;border-top:1px solid #eef3f6;border-bottom:1px solid #eef3f6}.compose-mail__recipients__container .recipients__input-list:hover{display:flex}.compose-mail__recipients__container .recipients__input-list li{list-style:none;padding:.25rem .5rem;cursor:pointer;background:#fff;border:1px solid #eef3f6;border-top:0px}.compose-mail__recipients__container .recipients__input-list li:hover{background:#eef3f6}.compose-mail__recipients__container .recipients__input-list li:last-child{border-bottom:0px}.compose-mail__recipients .remove-recipient{padding:.125rem .5rem}.compose-mail input[type=text].compose-mail__subject{padding:.5rem 0;border:none;box-shadow:none;border-bottom:2px solid #eef3f6;border-radius:0}.compose-mail__message{margin:.5rem 0;height:100%;display:flex;flex-direction:column;overflow:auto}.compose-mail__message-body{height:100%;outline:rgba(0,0,0,0)}.compose-mail__send-btn{display:flex;align-items:center;gap:.5rem}.compose-mail__send-btn i{transform:translateY(-1px)}.msg-view{height:100%;display:flex;flex-direction:column;gap:1rem;overflow:auto}.msg-view-nav{display:flex;justify-content:space-between;align-items:column}.msg-view-nav__action{display:flex;gap:.5rem}.msg-view__header{display:flex;flex-direction:column;gap:1rem}.msg-view__header>h3{line-height:1}.msg-view__header .msg-details{display:flex;gap:1rem}.msg-view__header .msg-details__avatar{height:max-content}.msg-view__header .msg-details__info{display:flex;flex-direction:column}.msg-view__header .msg-details__info-item{display:flex;gap:.5rem}.msg-view__body{height:100%;overflow:auto;font-size:14px !important}.msg-view__attachment{height:50%;overflow:auto;display:flex;flex-direction:column}.msg-view__attachment-items{height:100%;overflow:auto}.mail-tag{width:8rem;padding:.5rem}.msgHeader{display:flex}.msgHeaderDetails{display:flex;flex-direction:column}table.mails th:nth-child(1){width:5%;color:#fcba03}table.mails th:nth-child(2){width:5%;color:hsl(202.5,30.7692307692%,44.9019607843%)}table.mails th:nth-child(3){width:50%;text-align:start}table.mails th:nth-child(4),table.mails th:nth-child(5){width:20%;text-align:start}table.mails td:nth-child(3){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.mails td:nth-child(4),table.mails td:nth-child(5){text-align:start}table.mails tr:hover{background-color:#eef3f6;cursor:pointer}table.mails tr.unread{color:#000;background-color:#eef3f6}table.mails>tr:hover{cursor:auto;background-color:#fff}table.mails th.sortable-th{cursor:pointer;user-select:none;transition:background-color .2s,color .2s}table.mails th.sortable-th:hover{background-color:#eef3f6;color:hsl(202.5,30.7692307692%,14.9019607843%)}input.star-check{display:none}input.star-check+label.star-check{color:gray}input.star-check:checked+label.star-check{color:#fcba03}#truncate{height:6rem;overflow:auto}#truncate.truncated-view{height:1.75rem;overflow:hidden}.toggle-truncate{font-size:.75rem;padding:0 .25rem;background:#999;color:#14141b;box-shadow:none;border-radius:2px}table.attachment-container{padding:0}table.attachment-container>tr{border:0}table.attachment-container .attachment-header{width:100%;display:flex;justify-content:space-between}table.attachment-container .attachment-header th{text-align:start}table.attachment-container .attachment-header th:nth-child(1){flex-basis:45%}table.attachment-container .attachment-header th:nth-child(2){flex-basis:15%}table.attachment-container .attachment-header th:nth-child(3){flex-basis:10%}table.attachment-container .attachment-header th:nth-child(4){flex-basis:20%}table.attachment-container .attachment-header th:nth-child(5){text-align:center;flex-basis:10%}table.attachment-container .attachment{width:100%;display:flex;justify-content:space-between;text-align:start}table.attachment-container .attachment__name{flex-basis:45%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}table.attachment-container .attachment__name span{margin-left:8px}table.attachment-container .attachment__from{flex-basis:15%}table.attachment-container .attachment__size{flex-basis:10%}table.attachment-container .attachment__date{flex-basis:20%}table.attachment-container .attachment td:nth-child(5){display:flex;justify-content:center;align-items:center;flex-basis:10%}table.attachment-container .attachment td:nth-child(5) button{font-size:.875rem}.view-toggle{height:max-content;border:1px solid #019dff;border-radius:4px;display:flex}.view-toggle *{padding:4px 12px;border-radius:4px}.composePopupOverlay{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1;background-color:rgba(0,0,0,.2)}.composePopupOverlay .composePopup{position:absolute;inset:0;margin:auto;width:80%;height:90%}.composePopupOverlay .composePopup>.widget{padding:2rem}.composePopupOverlay .composePopup .close-btn{position:absolute;top:1.5rem;right:1.5rem}.file-view{width:100%;padding:1rem;margin-top:1.5rem;border-radius:8px;border:1px solid #ccc;animation:fadein .5s}.file-view__heading{display:flex;justify-content:space-between;margin-bottom:.5rem}.file-view__heading-chunk{display:flex;gap:1rem}.file-view__body{display:flex;flex-direction:column;gap:1rem}.file-view__body-details{display:flex;align-items:center}.file-view__body-details-stat{width:100%;display:grid;grid-template-columns:repeat(5, 1fr)}.file-view__body-details-stat span>i{margin-right:.5rem}.file-view__body-details-action{display:flex;gap:1rem;height:100%}.file-view__body-details-action button,.file-view__body-details-action button.red{padding:.25rem .75rem}table.myfiles td{word-wrap:break-word}table.myfiles th:nth-child(1){width:2%}table.myfiles th:nth-child(2){width:50%}table.myfiles td:nth-child(2){text-align:start}table.friendsfiles td{word-wrap:break-word}table.friendsfiles th:nth-child(1){width:2%}table.friendsfiles th:nth-child(2){width:50%}table.friendsfiles th:nth-child(4){width:40%}table.friendsfiles td:nth-child(2){text-align:start}.file-search-container{margin-top:1rem;padding:8px;display:flex;gap:8px;border:1px solid rgba(20,20,27,.2);border-radius:6px;height:100%;overflow:auto}.file-search-container__keywords{flex-basis:15%;padding-right:.25rem;border-right:1px solid rgba(20,20,27,.1)}.file-search-container__keywords .keywords-container{display:flex;flex-direction:column;border-top:2.5px solid rgba(20,20,27,.08);margin-top:.125rem;padding-top:.25rem}.file-search-container__keywords .keywords-container a{font-size:1.2rem;text-decoration:none;color:#14141b}.file-search-container__keywords .keywords-container a.selected{color:#019dff}.file-search-container__results{flex-basis:85%;height:100%;overflow:auto}.file-search-container__results .results-container .results-header tr{display:flex}.file-search-container__results .results-container .results-header tr th{font-size:1.25rem;font-weight:bold;text-align:left}.file-search-container__results .results-container .results-header tr th:nth-child(1){flex-basis:40%}.file-search-container__results .results-container .results-header tr th:nth-child(2){flex-basis:10%;text-align:center}.file-search-container__results .results-container .results-header tr th:nth-child(3){flex-basis:40%}.file-search-container__results .results-container .results-header tr th:nth-child(4){flex-basis:10%}.file-search-container__results .results-container .results{height:100%;overflow:auto}.file-search-container__results .results-container .results tr{display:flex}.file-search-container__results .results-container .results tr .results__hash,.file-search-container__results .results-container .results tr .results__name{text-align:left;flex-basis:40%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.file-search-container__results .results-container .results tr .results__hash span,.file-search-container__results .results-container .results tr .results__name span{margin-left:8px}.file-search-container__results .results-container .results tr .results__size{flex-basis:10%}.file-search-container__results .results-container .results tr .results__download{flex-basis:10%;display:flex;justify-content:start;align-items:center}.search-form{display:flex;width:40%}.search-form input{width:100%}.search-form button{margin-left:.5rem}.shareManagerPopupOverlay{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1;background-color:rgba(0,0,0,.2)}.shareManagerPopupOverlay .shareManagerPopup{position:absolute;inset:0;margin:auto;width:80%;height:90%}.shareManagerPopupOverlay .shareManagerPopup>.widget{padding:1.5rem}.shareManagerPopupOverlay .shareManagerPopup .close-btn{position:absolute;top:1.5rem;right:1.5rem}.share-manager{display:flex;flex-direction:column;justify-content:space-between}.share-manager__table{margin:1rem 0 auto}.share-manager__table thead{font-weight:bold;text-align:left}.share-manager__table thead td:nth-child(1),.share-manager__table thead td:nth-child(2){padding-left:.5rem}.share-manager__table thead td:nth-child(3) .tooltip,.share-manager__table thead td:nth-child(4) .tooltip{font-weight:normal;font-size:1rem}.share-manager__table tbody{text-align:left}.share-manager__table tbody td:nth-child(4){font-size:1rem}.share-manager__table td input{border:0 !important}.share-manager__table td input[type=text]{width:100%}.share-manager__table td:nth-child(1){width:45%}.share-manager__table td:nth-child(2){width:20%}.share-manager__table td:nth-child(3){width:10%}.share-manager__table td:nth-child(4){width:25%}.share-manager__actions{display:flex;justify-content:space-between}.share-manager__form{display:flex;flex-direction:column;gap:.5rem}.share-manager__form_input{display:flex;flex-direction:column;gap:.5rem}.share-manager__form_input input{flex-grow:1}.share-manager .share-flags input.share-flags-check{display:none}.share-manager .share-flags input.share-flags-check+label.share-flags-label{color:gray;margin-right:.25rem;padding:.25rem .25rem .125rem;border:1px solid #6d6d6d;border-radius:.5rem}.share-manager .share-flags input.share-flags-check:checked+label.share-flags-label{color:#118fcc}.share-manager label span{display:inline-block;width:1.125rem}.manage-visibility label{width:100%;cursor:pointer}.manage-visibility{display:flex;justify-content:space-between}@media(max-width: 700px){.file-view__body-details{flex-direction:column;align-items:flex-start;gap:1rem}.file-view__body-details-stat{grid-template-columns:1fr;gap:.5rem}.file-view__body-details-stat span{display:flex;align-items:center}.share-manager__table,.share-manager__table thead,.share-manager__table tbody,.share-manager__table tr,.share-manager__table td{display:block;width:100% !important}.share-manager__table thead{display:none}.share-manager__table tr{border:1px solid #ccc;border-radius:8px;margin-bottom:1rem;padding:.5rem;background:#fff}.share-manager__table td{margin-bottom:.5rem;border:none !important;padding-left:0 !important}table.myfiles,table.myfiles tr,table.myfiles td,table.friendsfiles,table.friendsfiles tr,table.friendsfiles td{display:block;width:100% !important}table.myfiles th,table.friendsfiles th{display:none}table.myfiles tr,table.friendsfiles tr{border:1px solid #ccc;border-radius:8px;margin-bottom:1rem;padding:.5rem;background:#fff}.file-search-container{flex-direction:column}.file-search-container__keywords{flex-basis:auto;width:100%;border-right:none;border-bottom:1px solid rgba(20,20,27,.1);padding-bottom:1rem;margin-bottom:1rem}.results-container,.results-container thead,.results-container tbody,.results-container tr,.results-container td{display:block;width:100% !important}.results-container thead{display:none}.results-container tr{border-bottom:1px solid #eee;padding:1rem 0}.results-container td{margin-bottom:.5rem;word-break:break-all}}.file-section{margin-top:2rem;display:flex;flex-direction:column}.comments-section{margin-top:2rem;display:flex;justify-content:space-between}.comments-section__menu{display:flex;gap:1rem}.comments-section__menu-id{display:flex;align-items:center;gap:.25rem}#toggleunsub{position:relative;background:gray}table.channels th:nth-child(1){width:50%;text-align:start}table.channels td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.channels tr:hover{background-color:#eef3f6;cursor:pointer}table.channels tr.hidden{display:none}table{padding:.5rem}table.comments{border:1px solid #eee}table.comments th{height:40px}table.comments th:nth-child(1){width:2%}table.comments th:nth-child(2){width:40%}table.comments td{word-wrap:break-word}table.comments td:nth-child(2){text-align:start}table.files th:first-child{text-align:start;width:60%}table.files tr td:first-child{text-align:start}table.files td{word-wrap:break-word}#mtags{width:160px;text-align:center;font-size:medium;margin-left:10px;height:40px}.forums-node-panel{position:relative;bottom:200px;margin-left:200px;animation:fadein .5s}table.forums th:nth-child(1){width:50%;text-align:start}table.forums td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.forums tr:hover{background-color:#eef3f6;cursor:pointer}table.forums tr.hidden{display:none}#searchforum{position:relative;margin-left:250px}#forumdetails{position:relative;padding:10px}.p{margin:0}#toggleunsub{position:relative;background:gray}table.threads tr:hover{background-color:#eef3f6;cursor:pointer}table.threads td{word-wrap:break-word}table.threadreply th:nth-child(2){width:50%}table.threadreply th:nth-child(1){width:2%}table.threadreply td:nth-child(2){width:50%;text-align:start}table.threadreply td{word-wrap:break-word}table.threadreply tr:hover{background-color:#eef3f6;cursor:pointer}table.boards th:nth-child(1){width:50%;text-align:start}table.boards td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.boards tr:hover{background-color:#eef3f6;cursor:pointer}table.boards tr.hidden{display:none}#toggleunsub{position:relative;background:gray}#options{width:100px;text-align:center;font-size:medium;margin-left:20px;height:40px}#composepopup{height:80%;width:70%}#mtags{width:160px;text-align:center;font-size:medium;margin-left:10px;height:40px}.mail .permission-flag{margin-bottom:1rem;display:flex;gap:1rem}.mail-tags{padding:.5rem;border:1px solid rgba(20,20,27,.2);border-radius:6px}.mail-tags__container{display:flex;flex-direction:column}.mail-tags__container .tag-item{display:flex;align-items:center;gap:4px;border-bottom:1px solid rgba(20,20,27,.1);padding:2px 0}.mail-tags__container .tag-item:last-child{border:none}.mail-tags__container .tag-item__color{width:1.25rem;height:1.25rem;aspect-ratio:1}.mail-tags__container .tag-item__name{font-size:1.125rem}.mail-tags__container .tag-item__modify{margin-left:auto;font-size:.75rem;display:flex;gap:4px}.mail-tags__container .tag-item:hover{background-color:#eef3f6}.mail-tags__container .tag-item button,.mail-tags__container .tag-item button.red{padding:.25rem .6rem}.mail-tags-form .input-field{margin-bottom:.5rem}.mail-tags-form .input-field label{margin-right:.5rem}.external-address{margin:0;padding-left:1rem;height:100px;overflow:hidden auto}.external-address::-webkit-scrollbar{display:none}.proxy-server{display:flex;flex-direction:column;gap:4px}.proxy-server__tor>h4,.proxy-server__i2p>h4{margin-bottom:.25rem}.proxy-server__tor>input,.proxy-server__i2p>input{margin-right:.5rem}.proxy-server__tor .proxy-outgoing,.proxy-server__i2p .proxy-outgoing{display:inline-flex;align-items:center;gap:.5rem}.proxy-server__tor .proxy-outgoing__status,.proxy-server__i2p .proxy-outgoing__status{width:1rem;height:1rem;aspect-ratio:1;border:1px solid #000;border-radius:50%}.config-files{display:flex;flex-direction:column;gap:1rem} From 42429641391b1203489f109f92eb95c59b325bd9 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:19:34 +0200 Subject: [PATCH 14/40] restore back broken styles --- webui-src/styles.css | 2061 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 2059 insertions(+), 2 deletions(-) diff --git a/webui-src/styles.css b/webui-src/styles.css index eb029fc..cb79ce8 100644 --- a/webui-src/styles.css +++ b/webui-src/styles.css @@ -1,7 +1,2064 @@ -h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem}h5{font-size:1.25rem}h6{font-size:1.125rem}p{font-size:1rem}.small{font-size:.75rem}.bold{font-weight:bold}h1,h2,h3,h4,h5,h6,p{font-weight:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Bold.woff2") format("woff2"),url("./webfonts/Roboto-Bold.woff") format("woff"),url("./webfonts/Roboto-Bold.ttf") format("truetype");font-weight:700;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Bold.woff2") format("woff2"),url("./webfonts/Roboto-Bold.woff") format("woff"),url("./webfonts/Roboto-Bold.ttf") format("truetype");font-weight:bold;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-BoldItalic.woff2") format("woff2"),url("./webfonts/Roboto-BoldItalic.woff") format("woff"),url("./webfonts/Roboto-BoldItalic.ttf") format("truetype");font-weight:700;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-BoldItalic.woff2") format("woff2"),url("./webfonts/Roboto-BoldItalic.woff") format("woff"),url("./webfonts/Roboto-BoldItalic.ttf") format("truetype");font-weight:bold;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Medium.woff2") format("woff2"),url("./webfonts/Roboto-Medium.woff") format("woff"),url("./webfonts/Roboto-Medium.ttf") format("truetype");font-weight:500;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-MediumItalic.woff2") format("woff2"),url("./webfonts/Roboto-MediumItalic.woff") format("woff"),url("./webfonts/Roboto-MediumItalic.ttf") format("truetype");font-weight:500;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Regular.woff2") format("woff2"),url("./webfonts/Roboto-Regular.woff") format("woff"),url("./webfonts/Roboto-Regular.ttf") format("truetype");font-weight:400;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Regular.woff2") format("woff2"),url("./webfonts/Roboto-Regular.woff") format("woff"),url("./webfonts/Roboto-Regular.ttf") format("truetype");font-weight:normal;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Italic.woff2") format("woff2"),url("./webfonts/Roboto-Italic.woff") format("woff"),url("./webfonts/Roboto-Italic.ttf") format("truetype");font-weight:400;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Italic.woff2") format("woff2"),url("./webfonts/Roboto-Italic.woff") format("woff"),url("./webfonts/Roboto-Italic.ttf") format("truetype");font-weight:normal;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Light.woff2") format("woff2"),url("./webfonts/Roboto-Light.woff") format("woff"),url("./webfonts/Roboto-Light.ttf") format("truetype");font-weight:300;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-LightItalic.woff2") format("woff2"),url("./webfonts/Roboto-LightItalic.woff") format("woff"),url("./webfonts/Roboto-LightItalic.ttf") format("truetype");font-weight:300;font-style:italic}/*! +h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem}h5{font-size:1.25rem}h6{font-size:1.125rem}p{font-size:1rem}.small{font-size:.75rem}.bold{font-weight:bold}h1,h2,h3,h4,h5,h6,p{font-weight:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Bold.woff2") format("woff2"),url("./webfonts/Roboto-Bold.woff") format("woff"),url("./webfonts/Roboto-Bold.ttf") format("truetype");font-weight:700;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Bold.woff2") format("woff2"),url("./webfonts/Roboto-Bold.woff") format("woff"),url("./webfonts/Roboto-Bold.ttf") format("truetype");font-weight:bold;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-BoldItalic.woff2") format("woff2"),url("./webfonts/Roboto-BoldItalic.woff") format("woff"),url("./webfonts/Roboto-BoldItalic.ttf") format("truetype");font-weight:700;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-BoldItalic.woff2") format("woff2"),url("./webfonts/Roboto-BoldItalic.woff") format("woff"),url("./webfonts/Roboto-BoldItalic.ttf") format("truetype");font-weight:bold;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Medium.woff2") format("woff2"),url("./webfonts/Roboto-Medium.woff") format("woff"),url("./webfonts/Roboto-Medium.ttf") format("truetype");font-weight:500;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-MediumItalic.woff2") format("woff2"),url("./webfonts/Roboto-MediumItalic.woff") format("woff"),url("./webfonts/Roboto-MediumItalic.ttf") format("truetype");font-weight:500;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Regular.woff2") format("woff2"),url("./webfonts/Roboto-Regular.woff") format("woff"),url("./webfonts/Roboto-Regular.ttf") format("truetype");font-weight:400;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Regular.woff2") format("woff2"),url("./webfonts/Roboto-Regular.woff") format("woff"),url("./webfonts/Roboto-Regular.ttf") format("truetype");font-weight:normal;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Italic.woff2") format("woff2"),url("./webfonts/Roboto-Italic.woff") format("woff"),url("./webfonts/Roboto-Italic.ttf") format("truetype");font-weight:400;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Italic.woff2") format("woff2"),url("./webfonts/Roboto-Italic.woff") format("woff"),url("./webfonts/Roboto-Italic.ttf") format("truetype");font-weight:normal;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Light.woff2") format("woff2"),url("./webfonts/Roboto-Light.woff") format("woff"),url("./webfonts/Roboto-Light.ttf") format("truetype");font-weight:300;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-LightItalic.woff2") format("woff2"),url("./webfonts/Roboto-LightItalic.woff") format("woff"),url("./webfonts/Roboto-LightItalic.ttf") format("truetype");font-weight:300;font-style:italic}/*! * Font Awesome Free 5.9.0 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) */.fa,.fas,.far,.fal,.fab{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-0.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:solid .08em #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fas.fa-pull-left,.far.fa-pull-left,.fal.fa-pull-left,.fab.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fas.fa-pull-right,.far.fa-pull-right,.fal.fa-pull-right,.fab.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);transform:scale(1, -1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(-1, -1);transform:scale(-1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-flip-both{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:""}.fa-accessible-icon:before{content:""}.fa-accusoft:before{content:""}.fa-acquisitions-incorporated:before{content:""}.fa-ad:before{content:""}.fa-address-book:before{content:""}.fa-address-card:before{content:""}.fa-adjust:before{content:""}.fa-adn:before{content:""}.fa-adobe:before{content:""}.fa-adversal:before{content:""}.fa-affiliatetheme:before{content:""}.fa-air-freshener:before{content:""}.fa-airbnb:before{content:""}.fa-algolia:before{content:""}.fa-align-center:before{content:""}.fa-align-justify:before{content:""}.fa-align-left:before{content:""}.fa-align-right:before{content:""}.fa-alipay:before{content:""}.fa-allergies:before{content:""}.fa-amazon:before{content:""}.fa-amazon-pay:before{content:""}.fa-ambulance:before{content:""}.fa-american-sign-language-interpreting:before{content:""}.fa-amilia:before{content:""}.fa-anchor:before{content:""}.fa-android:before{content:""}.fa-angellist:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angry:before{content:""}.fa-angrycreative:before{content:""}.fa-angular:before{content:""}.fa-ankh:before{content:""}.fa-app-store:before{content:""}.fa-app-store-ios:before{content:""}.fa-apper:before{content:""}.fa-apple:before{content:""}.fa-apple-alt:before{content:""}.fa-apple-pay:before{content:""}.fa-archive:before{content:""}.fa-archway:before{content:""}.fa-arrow-alt-circle-down:before{content:""}.fa-arrow-alt-circle-left:before{content:""}.fa-arrow-alt-circle-right:before{content:""}.fa-arrow-alt-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-arrow-circle-left:before{content:""}.fa-arrow-circle-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-down:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrows-alt:before{content:""}.fa-arrows-alt-h:before{content:""}.fa-arrows-alt-v:before{content:""}.fa-artstation:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-asterisk:before{content:""}.fa-asymmetrik:before{content:""}.fa-at:before{content:""}.fa-atlas:before{content:""}.fa-atlassian:before{content:""}.fa-atom:before{content:""}.fa-audible:before{content:""}.fa-audio-description:before{content:""}.fa-autoprefixer:before{content:""}.fa-avianex:before{content:""}.fa-aviato:before{content:""}.fa-award:before{content:""}.fa-aws:before{content:""}.fa-baby:before{content:""}.fa-baby-carriage:before{content:""}.fa-backspace:before{content:""}.fa-backward:before{content:""}.fa-bacon:before{content:""}.fa-balance-scale:before{content:""}.fa-balance-scale-left:before{content:""}.fa-balance-scale-right:before{content:""}.fa-ban:before{content:""}.fa-band-aid:before{content:""}.fa-bandcamp:before{content:""}.fa-barcode:before{content:""}.fa-bars:before{content:""}.fa-baseball-ball:before{content:""}.fa-basketball-ball:before{content:""}.fa-bath:before{content:""}.fa-battery-empty:before{content:""}.fa-battery-full:before{content:""}.fa-battery-half:before{content:""}.fa-battery-quarter:before{content:""}.fa-battery-three-quarters:before{content:""}.fa-battle-net:before{content:""}.fa-bed:before{content:""}.fa-beer:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-bell:before{content:""}.fa-bell-slash:before{content:""}.fa-bezier-curve:before{content:""}.fa-bible:before{content:""}.fa-bicycle:before{content:""}.fa-biking:before{content:""}.fa-bimobject:before{content:""}.fa-binoculars:before{content:""}.fa-biohazard:before{content:""}.fa-birthday-cake:before{content:""}.fa-bitbucket:before{content:""}.fa-bitcoin:before{content:""}.fa-bity:before{content:""}.fa-black-tie:before{content:""}.fa-blackberry:before{content:""}.fa-blender:before{content:""}.fa-blender-phone:before{content:""}.fa-blind:before{content:""}.fa-blog:before{content:""}.fa-blogger:before{content:""}.fa-blogger-b:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-bold:before{content:""}.fa-bolt:before{content:""}.fa-bomb:before{content:""}.fa-bone:before{content:""}.fa-bong:before{content:""}.fa-book:before{content:""}.fa-book-dead:before{content:""}.fa-book-medical:before{content:""}.fa-book-open:before{content:""}.fa-book-reader:before{content:""}.fa-bookmark:before{content:""}.fa-bootstrap:before{content:""}.fa-border-all:before{content:""}.fa-border-none:before{content:""}.fa-border-style:before{content:""}.fa-bowling-ball:before{content:""}.fa-box:before{content:""}.fa-box-open:before{content:""}.fa-boxes:before{content:""}.fa-braille:before{content:""}.fa-brain:before{content:""}.fa-bread-slice:before{content:""}.fa-briefcase:before{content:""}.fa-briefcase-medical:before{content:""}.fa-broadcast-tower:before{content:""}.fa-broom:before{content:""}.fa-brush:before{content:""}.fa-btc:before{content:""}.fa-buffer:before{content:""}.fa-bug:before{content:""}.fa-building:before{content:""}.fa-bullhorn:before{content:""}.fa-bullseye:before{content:""}.fa-burn:before{content:""}.fa-buromobelexperte:before{content:""}.fa-bus:before{content:""}.fa-bus-alt:before{content:""}.fa-business-time:before{content:""}.fa-buysellads:before{content:""}.fa-calculator:before{content:""}.fa-calendar:before{content:""}.fa-calendar-alt:before{content:""}.fa-calendar-check:before{content:""}.fa-calendar-day:before{content:""}.fa-calendar-minus:before{content:""}.fa-calendar-plus:before{content:""}.fa-calendar-times:before{content:""}.fa-calendar-week:before{content:""}.fa-camera:before{content:""}.fa-camera-retro:before{content:""}.fa-campground:before{content:""}.fa-canadian-maple-leaf:before{content:""}.fa-candy-cane:before{content:""}.fa-cannabis:before{content:""}.fa-capsules:before{content:""}.fa-car:before{content:""}.fa-car-alt:before{content:""}.fa-car-battery:before{content:""}.fa-car-crash:before{content:""}.fa-car-side:before{content:""}.fa-caret-down:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-caret-square-down:before{content:""}.fa-caret-square-left:before{content:""}.fa-caret-square-right:before{content:""}.fa-caret-square-up:before{content:""}.fa-caret-up:before{content:""}.fa-carrot:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-cart-plus:before{content:""}.fa-cash-register:before{content:""}.fa-cat:before{content:""}.fa-cc-amazon-pay:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-apple-pay:before{content:""}.fa-cc-diners-club:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-cc-visa:before{content:""}.fa-centercode:before{content:""}.fa-centos:before{content:""}.fa-certificate:before{content:""}.fa-chair:before{content:""}.fa-chalkboard:before{content:""}.fa-chalkboard-teacher:before{content:""}.fa-charging-station:before{content:""}.fa-chart-area:before{content:""}.fa-chart-bar:before{content:""}.fa-chart-line:before{content:""}.fa-chart-pie:before{content:""}.fa-check:before{content:""}.fa-check-circle:before{content:""}.fa-check-double:before{content:""}.fa-check-square:before{content:""}.fa-cheese:before{content:""}.fa-chess:before{content:""}.fa-chess-bishop:before{content:""}.fa-chess-board:before{content:""}.fa-chess-king:before{content:""}.fa-chess-knight:before{content:""}.fa-chess-pawn:before{content:""}.fa-chess-queen:before{content:""}.fa-chess-rook:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-down:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-chevron-up:before{content:""}.fa-child:before{content:""}.fa-chrome:before{content:""}.fa-chromecast:before{content:""}.fa-church:before{content:""}.fa-circle:before{content:""}.fa-circle-notch:before{content:""}.fa-city:before{content:""}.fa-clinic-medical:before{content:""}.fa-clipboard:before{content:""}.fa-clipboard-check:before{content:""}.fa-clipboard-list:before{content:""}.fa-clock:before{content:""}.fa-clone:before{content:""}.fa-closed-captioning:before{content:""}.fa-cloud:before{content:""}.fa-cloud-download-alt:before{content:""}.fa-cloud-meatball:before{content:""}.fa-cloud-moon:before{content:""}.fa-cloud-moon-rain:before{content:""}.fa-cloud-rain:before{content:""}.fa-cloud-showers-heavy:before{content:""}.fa-cloud-sun:before{content:""}.fa-cloud-sun-rain:before{content:""}.fa-cloud-upload-alt:before{content:""}.fa-cloudscale:before{content:""}.fa-cloudsmith:before{content:""}.fa-cloudversify:before{content:""}.fa-cocktail:before{content:""}.fa-code:before{content:""}.fa-code-branch:before{content:""}.fa-codepen:before{content:""}.fa-codiepie:before{content:""}.fa-coffee:before{content:""}.fa-cog:before{content:""}.fa-cogs:before{content:""}.fa-coins:before{content:""}.fa-columns:before{content:""}.fa-comment:before{content:""}.fa-comment-alt:before{content:""}.fa-comment-dollar:before{content:""}.fa-comment-dots:before{content:""}.fa-comment-medical:before{content:""}.fa-comment-slash:before{content:""}.fa-comments:before{content:""}.fa-comments-dollar:before{content:""}.fa-compact-disc:before{content:""}.fa-compass:before{content:""}.fa-compress:before{content:""}.fa-compress-arrows-alt:before{content:""}.fa-concierge-bell:before{content:""}.fa-confluence:before{content:""}.fa-connectdevelop:before{content:""}.fa-contao:before{content:""}.fa-cookie:before{content:""}.fa-cookie-bite:before{content:""}.fa-copy:before{content:""}.fa-copyright:before{content:""}.fa-couch:before{content:""}.fa-cpanel:before{content:""}.fa-creative-commons:before{content:""}.fa-creative-commons-by:before{content:""}.fa-creative-commons-nc:before{content:""}.fa-creative-commons-nc-eu:before{content:""}.fa-creative-commons-nc-jp:before{content:""}.fa-creative-commons-nd:before{content:""}.fa-creative-commons-pd:before{content:""}.fa-creative-commons-pd-alt:before{content:""}.fa-creative-commons-remix:before{content:""}.fa-creative-commons-sa:before{content:""}.fa-creative-commons-sampling:before{content:""}.fa-creative-commons-sampling-plus:before{content:""}.fa-creative-commons-share:before{content:""}.fa-creative-commons-zero:before{content:""}.fa-credit-card:before{content:""}.fa-critical-role:before{content:""}.fa-crop:before{content:""}.fa-crop-alt:before{content:""}.fa-cross:before{content:""}.fa-crosshairs:before{content:""}.fa-crow:before{content:""}.fa-crown:before{content:""}.fa-crutch:before{content:""}.fa-css3:before{content:""}.fa-css3-alt:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-cut:before{content:""}.fa-cuttlefish:before{content:""}.fa-d-and-d:before{content:""}.fa-d-and-d-beyond:before{content:""}.fa-dashcube:before{content:""}.fa-database:before{content:""}.fa-deaf:before{content:""}.fa-delicious:before{content:""}.fa-democrat:before{content:""}.fa-deploydog:before{content:""}.fa-deskpro:before{content:""}.fa-desktop:before{content:""}.fa-dev:before{content:""}.fa-deviantart:before{content:""}.fa-dharmachakra:before{content:""}.fa-dhl:before{content:""}.fa-diagnoses:before{content:""}.fa-diaspora:before{content:""}.fa-dice:before{content:""}.fa-dice-d20:before{content:""}.fa-dice-d6:before{content:""}.fa-dice-five:before{content:""}.fa-dice-four:before{content:""}.fa-dice-one:before{content:""}.fa-dice-six:before{content:""}.fa-dice-three:before{content:""}.fa-dice-two:before{content:""}.fa-digg:before{content:""}.fa-digital-ocean:before{content:""}.fa-digital-tachograph:before{content:""}.fa-directions:before{content:""}.fa-discord:before{content:""}.fa-discourse:before{content:""}.fa-divide:before{content:""}.fa-dizzy:before{content:""}.fa-dna:before{content:""}.fa-dochub:before{content:""}.fa-docker:before{content:""}.fa-dog:before{content:""}.fa-dollar-sign:before{content:""}.fa-dolly:before{content:""}.fa-dolly-flatbed:before{content:""}.fa-donate:before{content:""}.fa-door-closed:before{content:""}.fa-door-open:before{content:""}.fa-dot-circle:before{content:""}.fa-dove:before{content:""}.fa-download:before{content:""}.fa-draft2digital:before{content:""}.fa-drafting-compass:before{content:""}.fa-dragon:before{content:""}.fa-draw-polygon:before{content:""}.fa-dribbble:before{content:""}.fa-dribbble-square:before{content:""}.fa-dropbox:before{content:""}.fa-drum:before{content:""}.fa-drum-steelpan:before{content:""}.fa-drumstick-bite:before{content:""}.fa-drupal:before{content:""}.fa-dumbbell:before{content:""}.fa-dumpster:before{content:""}.fa-dumpster-fire:before{content:""}.fa-dungeon:before{content:""}.fa-dyalog:before{content:""}.fa-earlybirds:before{content:""}.fa-ebay:before{content:""}.fa-edge:before{content:""}.fa-edit:before{content:""}.fa-egg:before{content:""}.fa-eject:before{content:""}.fa-elementor:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-ello:before{content:""}.fa-ember:before{content:""}.fa-empire:before{content:""}.fa-envelope:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-text:before{content:""}.fa-envelope-square:before{content:""}.fa-envira:before{content:""}.fa-equals:before{content:""}.fa-eraser:before{content:""}.fa-erlang:before{content:""}.fa-ethereum:before{content:""}.fa-ethernet:before{content:""}.fa-etsy:before{content:""}.fa-euro-sign:before{content:""}.fa-evernote:before{content:""}.fa-exchange-alt:before{content:""}.fa-exclamation:before{content:""}.fa-exclamation-circle:before{content:""}.fa-exclamation-triangle:before{content:""}.fa-expand:before{content:""}.fa-expand-arrows-alt:before{content:""}.fa-expeditedssl:before{content:""}.fa-external-link-alt:before{content:""}.fa-external-link-square-alt:before{content:""}.fa-eye:before{content:""}.fa-eye-dropper:before{content:""}.fa-eye-slash:before{content:""}.fa-facebook:before{content:""}.fa-facebook-f:before{content:""}.fa-facebook-messenger:before{content:""}.fa-facebook-square:before{content:""}.fa-fan:before{content:""}.fa-fantasy-flight-games:before{content:""}.fa-fast-backward:before{content:""}.fa-fast-forward:before{content:""}.fa-fax:before{content:""}.fa-feather:before{content:""}.fa-feather-alt:before{content:""}.fa-fedex:before{content:""}.fa-fedora:before{content:""}.fa-female:before{content:""}.fa-fighter-jet:before{content:""}.fa-figma:before{content:""}.fa-file:before{content:""}.fa-file-alt:before{content:""}.fa-file-archive:before{content:""}.fa-file-audio:before{content:""}.fa-file-code:before{content:""}.fa-file-contract:before{content:""}.fa-file-csv:before{content:""}.fa-file-download:before{content:""}.fa-file-excel:before{content:""}.fa-file-export:before{content:""}.fa-file-image:before{content:""}.fa-file-import:before{content:""}.fa-file-invoice:before{content:""}.fa-file-invoice-dollar:before{content:""}.fa-file-medical:before{content:""}.fa-file-medical-alt:before{content:""}.fa-file-pdf:before{content:""}.fa-file-powerpoint:before{content:""}.fa-file-prescription:before{content:""}.fa-file-signature:before{content:""}.fa-file-upload:before{content:""}.fa-file-video:before{content:""}.fa-file-word:before{content:""}.fa-fill:before{content:""}.fa-fill-drip:before{content:""}.fa-film:before{content:""}.fa-filter:before{content:""}.fa-fingerprint:before{content:""}.fa-fire:before{content:""}.fa-fire-alt:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-firefox:before{content:""}.fa-first-aid:before{content:""}.fa-first-order:before{content:""}.fa-first-order-alt:before{content:""}.fa-firstdraft:before{content:""}.fa-fish:before{content:""}.fa-fist-raised:before{content:""}.fa-flag:before{content:""}.fa-flag-checkered:before{content:""}.fa-flag-usa:before{content:""}.fa-flask:before{content:""}.fa-flickr:before{content:""}.fa-flipboard:before{content:""}.fa-flushed:before{content:""}.fa-fly:before{content:""}.fa-folder:before{content:""}.fa-folder-minus:before{content:""}.fa-folder-open:before{content:""}.fa-folder-plus:before{content:""}.fa-font:before{content:""}.fa-font-awesome:before{content:""}.fa-font-awesome-alt:before{content:""}.fa-font-awesome-flag:before{content:""}.fa-font-awesome-logo-full:before{content:""}.fa-fonticons:before{content:""}.fa-fonticons-fi:before{content:""}.fa-football-ball:before{content:""}.fa-fort-awesome:before{content:""}.fa-fort-awesome-alt:before{content:""}.fa-forumbee:before{content:""}.fa-forward:before{content:""}.fa-foursquare:before{content:""}.fa-free-code-camp:before{content:""}.fa-freebsd:before{content:""}.fa-frog:before{content:""}.fa-frown:before{content:""}.fa-frown-open:before{content:""}.fa-fulcrum:before{content:""}.fa-funnel-dollar:before{content:""}.fa-futbol:before{content:""}.fa-galactic-republic:before{content:""}.fa-galactic-senate:before{content:""}.fa-gamepad:before{content:""}.fa-gas-pump:before{content:""}.fa-gavel:before{content:""}.fa-gem:before{content:""}.fa-genderless:before{content:""}.fa-get-pocket:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-ghost:before{content:""}.fa-gift:before{content:""}.fa-gifts:before{content:""}.fa-git:before{content:""}.fa-git-alt:before{content:""}.fa-git-square:before{content:""}.fa-github:before{content:""}.fa-github-alt:before{content:""}.fa-github-square:before{content:""}.fa-gitkraken:before{content:""}.fa-gitlab:before{content:""}.fa-gitter:before{content:""}.fa-glass-cheers:before{content:""}.fa-glass-martini:before{content:""}.fa-glass-martini-alt:before{content:""}.fa-glass-whiskey:before{content:""}.fa-glasses:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-globe:before{content:""}.fa-globe-africa:before{content:""}.fa-globe-americas:before{content:""}.fa-globe-asia:before{content:""}.fa-globe-europe:before{content:""}.fa-gofore:before{content:""}.fa-golf-ball:before{content:""}.fa-goodreads:before{content:""}.fa-goodreads-g:before{content:""}.fa-google:before{content:""}.fa-google-drive:before{content:""}.fa-google-play:before{content:""}.fa-google-plus:before{content:""}.fa-google-plus-g:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-wallet:before{content:""}.fa-gopuram:before{content:""}.fa-graduation-cap:before{content:""}.fa-gratipay:before{content:""}.fa-grav:before{content:""}.fa-greater-than:before{content:""}.fa-greater-than-equal:before{content:""}.fa-grimace:before{content:""}.fa-grin:before{content:""}.fa-grin-alt:before{content:""}.fa-grin-beam:before{content:""}.fa-grin-beam-sweat:before{content:""}.fa-grin-hearts:before{content:""}.fa-grin-squint:before{content:""}.fa-grin-squint-tears:before{content:""}.fa-grin-stars:before{content:""}.fa-grin-tears:before{content:""}.fa-grin-tongue:before{content:""}.fa-grin-tongue-squint:before{content:""}.fa-grin-tongue-wink:before{content:""}.fa-grin-wink:before{content:""}.fa-grip-horizontal:before{content:""}.fa-grip-lines:before{content:""}.fa-grip-lines-vertical:before{content:""}.fa-grip-vertical:before{content:""}.fa-gripfire:before{content:""}.fa-grunt:before{content:""}.fa-guitar:before{content:""}.fa-gulp:before{content:""}.fa-h-square:before{content:""}.fa-hacker-news:before{content:""}.fa-hacker-news-square:before{content:""}.fa-hackerrank:before{content:""}.fa-hamburger:before{content:""}.fa-hammer:before{content:""}.fa-hamsa:before{content:""}.fa-hand-holding:before{content:""}.fa-hand-holding-heart:before{content:""}.fa-hand-holding-usd:before{content:""}.fa-hand-lizard:before{content:""}.fa-hand-middle-finger:before{content:""}.fa-hand-paper:before{content:""}.fa-hand-peace:before{content:""}.fa-hand-point-down:before{content:""}.fa-hand-point-left:before{content:""}.fa-hand-point-right:before{content:""}.fa-hand-point-up:before{content:""}.fa-hand-pointer:before{content:""}.fa-hand-rock:before{content:""}.fa-hand-scissors:before{content:""}.fa-hand-spock:before{content:""}.fa-hands:before{content:""}.fa-hands-helping:before{content:""}.fa-handshake:before{content:""}.fa-hanukiah:before{content:""}.fa-hard-hat:before{content:""}.fa-hashtag:before{content:""}.fa-hat-wizard:before{content:""}.fa-haykal:before{content:""}.fa-hdd:before{content:""}.fa-heading:before{content:""}.fa-headphones:before{content:""}.fa-headphones-alt:before{content:""}.fa-headset:before{content:""}.fa-heart:before{content:""}.fa-heart-broken:before{content:""}.fa-heartbeat:before{content:""}.fa-helicopter:before{content:""}.fa-highlighter:before{content:""}.fa-hiking:before{content:""}.fa-hippo:before{content:""}.fa-hips:before{content:""}.fa-hire-a-helper:before{content:""}.fa-history:before{content:""}.fa-hockey-puck:before{content:""}.fa-holly-berry:before{content:""}.fa-home:before{content:""}.fa-hooli:before{content:""}.fa-hornbill:before{content:""}.fa-horse:before{content:""}.fa-horse-head:before{content:""}.fa-hospital:before{content:""}.fa-hospital-alt:before{content:""}.fa-hospital-symbol:before{content:""}.fa-hot-tub:before{content:""}.fa-hotdog:before{content:""}.fa-hotel:before{content:""}.fa-hotjar:before{content:""}.fa-hourglass:before{content:""}.fa-hourglass-end:before{content:""}.fa-hourglass-half:before{content:""}.fa-hourglass-start:before{content:""}.fa-house-damage:before{content:""}.fa-houzz:before{content:""}.fa-hryvnia:before{content:""}.fa-html5:before{content:""}.fa-hubspot:before{content:""}.fa-i-cursor:before{content:""}.fa-ice-cream:before{content:""}.fa-icicles:before{content:""}.fa-icons:before{content:""}.fa-id-badge:before{content:""}.fa-id-card:before{content:""}.fa-id-card-alt:before{content:""}.fa-igloo:before{content:""}.fa-image:before{content:""}.fa-images:before{content:""}.fa-imdb:before{content:""}.fa-inbox:before{content:""}.fa-indent:before{content:""}.fa-industry:before{content:""}.fa-infinity:before{content:""}.fa-info:before{content:""}.fa-info-circle:before{content:""}.fa-instagram:before{content:""}.fa-intercom:before{content:""}.fa-internet-explorer:before{content:""}.fa-invision:before{content:""}.fa-ioxhost:before{content:""}.fa-italic:before{content:""}.fa-itch-io:before{content:""}.fa-itunes:before{content:""}.fa-itunes-note:before{content:""}.fa-java:before{content:""}.fa-jedi:before{content:""}.fa-jedi-order:before{content:""}.fa-jenkins:before{content:""}.fa-jira:before{content:""}.fa-joget:before{content:""}.fa-joint:before{content:""}.fa-joomla:before{content:""}.fa-journal-whills:before{content:""}.fa-js:before{content:""}.fa-js-square:before{content:""}.fa-jsfiddle:before{content:""}.fa-kaaba:before{content:""}.fa-kaggle:before{content:""}.fa-key:before{content:""}.fa-keybase:before{content:""}.fa-keyboard:before{content:""}.fa-keycdn:before{content:""}.fa-khanda:before{content:""}.fa-kickstarter:before{content:""}.fa-kickstarter-k:before{content:""}.fa-kiss:before{content:""}.fa-kiss-beam:before{content:""}.fa-kiss-wink-heart:before{content:""}.fa-kiwi-bird:before{content:""}.fa-korvue:before{content:""}.fa-landmark:before{content:""}.fa-language:before{content:""}.fa-laptop:before{content:""}.fa-laptop-code:before{content:""}.fa-laptop-medical:before{content:""}.fa-laravel:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-laugh:before{content:""}.fa-laugh-beam:before{content:""}.fa-laugh-squint:before{content:""}.fa-laugh-wink:before{content:""}.fa-layer-group:before{content:""}.fa-leaf:before{content:""}.fa-leanpub:before{content:""}.fa-lemon:before{content:""}.fa-less:before{content:""}.fa-less-than:before{content:""}.fa-less-than-equal:before{content:""}.fa-level-down-alt:before{content:""}.fa-level-up-alt:before{content:""}.fa-life-ring:before{content:""}.fa-lightbulb:before{content:""}.fa-line:before{content:""}.fa-link:before{content:""}.fa-linkedin:before{content:""}.fa-linkedin-in:before{content:""}.fa-linode:before{content:""}.fa-linux:before{content:""}.fa-lira-sign:before{content:""}.fa-list:before{content:""}.fa-list-alt:before{content:""}.fa-list-ol:before{content:""}.fa-list-ul:before{content:""}.fa-location-arrow:before{content:""}.fa-lock:before{content:""}.fa-lock-open:before{content:""}.fa-long-arrow-alt-down:before{content:""}.fa-long-arrow-alt-left:before{content:""}.fa-long-arrow-alt-right:before{content:""}.fa-long-arrow-alt-up:before{content:""}.fa-low-vision:before{content:""}.fa-luggage-cart:before{content:""}.fa-lyft:before{content:""}.fa-magento:before{content:""}.fa-magic:before{content:""}.fa-magnet:before{content:""}.fa-mail-bulk:before{content:""}.fa-mailchimp:before{content:""}.fa-male:before{content:""}.fa-mandalorian:before{content:""}.fa-map:before{content:""}.fa-map-marked:before{content:""}.fa-map-marked-alt:before{content:""}.fa-map-marker:before{content:""}.fa-map-marker-alt:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-markdown:before{content:""}.fa-marker:before{content:""}.fa-mars:before{content:""}.fa-mars-double:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mask:before{content:""}.fa-mastodon:before{content:""}.fa-maxcdn:before{content:""}.fa-medal:before{content:""}.fa-medapps:before{content:""}.fa-medium:before{content:""}.fa-medium-m:before{content:""}.fa-medkit:before{content:""}.fa-medrt:before{content:""}.fa-meetup:before{content:""}.fa-megaport:before{content:""}.fa-meh:before{content:""}.fa-meh-blank:before{content:""}.fa-meh-rolling-eyes:before{content:""}.fa-memory:before{content:""}.fa-mendeley:before{content:""}.fa-menorah:before{content:""}.fa-mercury:before{content:""}.fa-meteor:before{content:""}.fa-microchip:before{content:""}.fa-microphone:before{content:""}.fa-microphone-alt:before{content:""}.fa-microphone-alt-slash:before{content:""}.fa-microphone-slash:before{content:""}.fa-microscope:before{content:""}.fa-microsoft:before{content:""}.fa-minus:before{content:""}.fa-minus-circle:before{content:""}.fa-minus-square:before{content:""}.fa-mitten:before{content:""}.fa-mix:before{content:""}.fa-mixcloud:before{content:""}.fa-mizuni:before{content:""}.fa-mobile:before{content:""}.fa-mobile-alt:before{content:""}.fa-modx:before{content:""}.fa-monero:before{content:""}.fa-money-bill:before{content:""}.fa-money-bill-alt:before{content:""}.fa-money-bill-wave:before{content:""}.fa-money-bill-wave-alt:before{content:""}.fa-money-check:before{content:""}.fa-money-check-alt:before{content:""}.fa-monument:before{content:""}.fa-moon:before{content:""}.fa-mortar-pestle:before{content:""}.fa-mosque:before{content:""}.fa-motorcycle:before{content:""}.fa-mountain:before{content:""}.fa-mouse-pointer:before{content:""}.fa-mug-hot:before{content:""}.fa-music:before{content:""}.fa-napster:before{content:""}.fa-neos:before{content:""}.fa-network-wired:before{content:""}.fa-neuter:before{content:""}.fa-newspaper:before{content:""}.fa-nimblr:before{content:""}.fa-node:before{content:""}.fa-node-js:before{content:""}.fa-not-equal:before{content:""}.fa-notes-medical:before{content:""}.fa-npm:before{content:""}.fa-ns8:before{content:""}.fa-nutritionix:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-oil-can:before{content:""}.fa-old-republic:before{content:""}.fa-om:before{content:""}.fa-opencart:before{content:""}.fa-openid:before{content:""}.fa-opera:before{content:""}.fa-optin-monster:before{content:""}.fa-osi:before{content:""}.fa-otter:before{content:""}.fa-outdent:before{content:""}.fa-page4:before{content:""}.fa-pagelines:before{content:""}.fa-pager:before{content:""}.fa-paint-brush:before{content:""}.fa-paint-roller:before{content:""}.fa-palette:before{content:""}.fa-palfed:before{content:""}.fa-pallet:before{content:""}.fa-paper-plane:before{content:""}.fa-paperclip:before{content:""}.fa-parachute-box:before{content:""}.fa-paragraph:before{content:""}.fa-parking:before{content:""}.fa-passport:before{content:""}.fa-pastafarianism:before{content:""}.fa-paste:before{content:""}.fa-patreon:before{content:""}.fa-pause:before{content:""}.fa-pause-circle:before{content:""}.fa-paw:before{content:""}.fa-paypal:before{content:""}.fa-peace:before{content:""}.fa-pen:before{content:""}.fa-pen-alt:before{content:""}.fa-pen-fancy:before{content:""}.fa-pen-nib:before{content:""}.fa-pen-square:before{content:""}.fa-pencil-alt:before{content:""}.fa-pencil-ruler:before{content:""}.fa-penny-arcade:before{content:""}.fa-people-carry:before{content:""}.fa-pepper-hot:before{content:""}.fa-percent:before{content:""}.fa-percentage:before{content:""}.fa-periscope:before{content:""}.fa-person-booth:before{content:""}.fa-phabricator:before{content:""}.fa-phoenix-framework:before{content:""}.fa-phoenix-squadron:before{content:""}.fa-phone:before{content:""}.fa-phone-alt:before{content:""}.fa-phone-slash:before{content:""}.fa-phone-square:before{content:""}.fa-phone-square-alt:before{content:""}.fa-phone-volume:before{content:""}.fa-photo-video:before{content:""}.fa-php:before{content:""}.fa-pied-piper:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-pied-piper-hat:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-piggy-bank:before{content:""}.fa-pills:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-p:before{content:""}.fa-pinterest-square:before{content:""}.fa-pizza-slice:before{content:""}.fa-place-of-worship:before{content:""}.fa-plane:before{content:""}.fa-plane-arrival:before{content:""}.fa-plane-departure:before{content:""}.fa-play:before{content:""}.fa-play-circle:before{content:""}.fa-playstation:before{content:""}.fa-plug:before{content:""}.fa-plus:before{content:""}.fa-plus-circle:before{content:""}.fa-plus-square:before{content:""}.fa-podcast:before{content:""}.fa-poll:before{content:""}.fa-poll-h:before{content:""}.fa-poo:before{content:""}.fa-poo-storm:before{content:""}.fa-poop:before{content:""}.fa-portrait:before{content:""}.fa-pound-sign:before{content:""}.fa-power-off:before{content:""}.fa-pray:before{content:""}.fa-praying-hands:before{content:""}.fa-prescription:before{content:""}.fa-prescription-bottle:before{content:""}.fa-prescription-bottle-alt:before{content:""}.fa-print:before{content:""}.fa-procedures:before{content:""}.fa-product-hunt:before{content:""}.fa-project-diagram:before{content:""}.fa-pushed:before{content:""}.fa-puzzle-piece:before{content:""}.fa-python:before{content:""}.fa-qq:before{content:""}.fa-qrcode:before{content:""}.fa-question:before{content:""}.fa-question-circle:before{content:""}.fa-quidditch:before{content:""}.fa-quinscape:before{content:""}.fa-quora:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-quran:before{content:""}.fa-r-project:before{content:""}.fa-radiation:before{content:""}.fa-radiation-alt:before{content:""}.fa-rainbow:before{content:""}.fa-random:before{content:""}.fa-raspberry-pi:before{content:""}.fa-ravelry:before{content:""}.fa-react:before{content:""}.fa-reacteurope:before{content:""}.fa-readme:before{content:""}.fa-rebel:before{content:""}.fa-receipt:before{content:""}.fa-recycle:before{content:""}.fa-red-river:before{content:""}.fa-reddit:before{content:""}.fa-reddit-alien:before{content:""}.fa-reddit-square:before{content:""}.fa-redhat:before{content:""}.fa-redo:before{content:""}.fa-redo-alt:before{content:""}.fa-registered:before{content:""}.fa-remove-format:before{content:""}.fa-renren:before{content:""}.fa-reply:before{content:""}.fa-reply-all:before{content:""}.fa-replyd:before{content:""}.fa-republican:before{content:""}.fa-researchgate:before{content:""}.fa-resolving:before{content:""}.fa-restroom:before{content:""}.fa-retweet:before{content:""}.fa-rev:before{content:""}.fa-ribbon:before{content:""}.fa-ring:before{content:""}.fa-road:before{content:""}.fa-robot:before{content:""}.fa-rocket:before{content:""}.fa-rocketchat:before{content:""}.fa-rockrms:before{content:""}.fa-route:before{content:""}.fa-rss:before{content:""}.fa-rss-square:before{content:""}.fa-ruble-sign:before{content:""}.fa-ruler:before{content:""}.fa-ruler-combined:before{content:""}.fa-ruler-horizontal:before{content:""}.fa-ruler-vertical:before{content:""}.fa-running:before{content:""}.fa-rupee-sign:before{content:""}.fa-sad-cry:before{content:""}.fa-sad-tear:before{content:""}.fa-safari:before{content:""}.fa-salesforce:before{content:""}.fa-sass:before{content:""}.fa-satellite:before{content:""}.fa-satellite-dish:before{content:""}.fa-save:before{content:""}.fa-schlix:before{content:""}.fa-school:before{content:""}.fa-screwdriver:before{content:""}.fa-scribd:before{content:""}.fa-scroll:before{content:""}.fa-sd-card:before{content:""}.fa-search:before{content:""}.fa-search-dollar:before{content:""}.fa-search-location:before{content:""}.fa-search-minus:before{content:""}.fa-search-plus:before{content:""}.fa-searchengin:before{content:""}.fa-seedling:before{content:""}.fa-sellcast:before{content:""}.fa-sellsy:before{content:""}.fa-server:before{content:""}.fa-servicestack:before{content:""}.fa-shapes:before{content:""}.fa-share:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-share-square:before{content:""}.fa-shekel-sign:before{content:""}.fa-shield-alt:before{content:""}.fa-ship:before{content:""}.fa-shipping-fast:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-shoe-prints:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-shopping-cart:before{content:""}.fa-shopware:before{content:""}.fa-shower:before{content:""}.fa-shuttle-van:before{content:""}.fa-sign:before{content:""}.fa-sign-in-alt:before{content:""}.fa-sign-language:before{content:""}.fa-sign-out-alt:before{content:""}.fa-signal:before{content:""}.fa-signature:before{content:""}.fa-sim-card:before{content:""}.fa-simplybuilt:before{content:""}.fa-sistrix:before{content:""}.fa-sitemap:before{content:""}.fa-sith:before{content:""}.fa-skating:before{content:""}.fa-sketch:before{content:""}.fa-skiing:before{content:""}.fa-skiing-nordic:before{content:""}.fa-skull:before{content:""}.fa-skull-crossbones:before{content:""}.fa-skyatlas:before{content:""}.fa-skype:before{content:""}.fa-slack:before{content:""}.fa-slack-hash:before{content:""}.fa-slash:before{content:""}.fa-sleigh:before{content:""}.fa-sliders-h:before{content:""}.fa-slideshare:before{content:""}.fa-smile:before{content:""}.fa-smile-beam:before{content:""}.fa-smile-wink:before{content:""}.fa-smog:before{content:""}.fa-smoking:before{content:""}.fa-smoking-ban:before{content:""}.fa-sms:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-snowboarding:before{content:""}.fa-snowflake:before{content:""}.fa-snowman:before{content:""}.fa-snowplow:before{content:""}.fa-socks:before{content:""}.fa-solar-panel:before{content:""}.fa-sort:before{content:""}.fa-sort-alpha-down:before{content:""}.fa-sort-alpha-down-alt:before{content:""}.fa-sort-alpha-up:before{content:""}.fa-sort-alpha-up-alt:before{content:""}.fa-sort-amount-down:before{content:""}.fa-sort-amount-down-alt:before{content:""}.fa-sort-amount-up:before{content:""}.fa-sort-amount-up-alt:before{content:""}.fa-sort-down:before{content:""}.fa-sort-numeric-down:before{content:""}.fa-sort-numeric-down-alt:before{content:""}.fa-sort-numeric-up:before{content:""}.fa-sort-numeric-up-alt:before{content:""}.fa-sort-up:before{content:""}.fa-soundcloud:before{content:""}.fa-sourcetree:before{content:""}.fa-spa:before{content:""}.fa-space-shuttle:before{content:""}.fa-speakap:before{content:""}.fa-speaker-deck:before{content:""}.fa-spell-check:before{content:""}.fa-spider:before{content:""}.fa-spinner:before{content:""}.fa-splotch:before{content:""}.fa-spotify:before{content:""}.fa-spray-can:before{content:""}.fa-square:before{content:""}.fa-square-full:before{content:""}.fa-square-root-alt:before{content:""}.fa-squarespace:before{content:""}.fa-stack-exchange:before{content:""}.fa-stack-overflow:before{content:""}.fa-stackpath:before{content:""}.fa-stamp:before{content:""}.fa-star:before{content:""}.fa-star-and-crescent:before{content:""}.fa-star-half:before{content:""}.fa-star-half-alt:before{content:""}.fa-star-of-david:before{content:""}.fa-star-of-life:before{content:""}.fa-staylinked:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-steam-symbol:before{content:""}.fa-step-backward:before{content:""}.fa-step-forward:before{content:""}.fa-stethoscope:before{content:""}.fa-sticker-mule:before{content:""}.fa-sticky-note:before{content:""}.fa-stop:before{content:""}.fa-stop-circle:before{content:""}.fa-stopwatch:before{content:""}.fa-store:before{content:""}.fa-store-alt:before{content:""}.fa-strava:before{content:""}.fa-stream:before{content:""}.fa-street-view:before{content:""}.fa-strikethrough:before{content:""}.fa-stripe:before{content:""}.fa-stripe-s:before{content:""}.fa-stroopwafel:before{content:""}.fa-studiovinari:before{content:""}.fa-stumbleupon:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-subscript:before{content:""}.fa-subway:before{content:""}.fa-suitcase:before{content:""}.fa-suitcase-rolling:before{content:""}.fa-sun:before{content:""}.fa-superpowers:before{content:""}.fa-superscript:before{content:""}.fa-supple:before{content:""}.fa-surprise:before{content:""}.fa-suse:before{content:""}.fa-swatchbook:before{content:""}.fa-swimmer:before{content:""}.fa-swimming-pool:before{content:""}.fa-symfony:before{content:""}.fa-synagogue:before{content:""}.fa-sync:before{content:""}.fa-sync-alt:before{content:""}.fa-syringe:before{content:""}.fa-table:before{content:""}.fa-table-tennis:before{content:""}.fa-tablet:before{content:""}.fa-tablet-alt:before{content:""}.fa-tablets:before{content:""}.fa-tachometer-alt:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-tape:before{content:""}.fa-tasks:before{content:""}.fa-taxi:before{content:""}.fa-teamspeak:before{content:""}.fa-teeth:before{content:""}.fa-teeth-open:before{content:""}.fa-telegram:before{content:""}.fa-telegram-plane:before{content:""}.fa-temperature-high:before{content:""}.fa-temperature-low:before{content:""}.fa-tencent-weibo:before{content:""}.fa-tenge:before{content:""}.fa-terminal:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-th:before{content:""}.fa-th-large:before{content:""}.fa-th-list:before{content:""}.fa-the-red-yeti:before{content:""}.fa-theater-masks:before{content:""}.fa-themeco:before{content:""}.fa-themeisle:before{content:""}.fa-thermometer:before{content:""}.fa-thermometer-empty:before{content:""}.fa-thermometer-full:before{content:""}.fa-thermometer-half:before{content:""}.fa-thermometer-quarter:before{content:""}.fa-thermometer-three-quarters:before{content:""}.fa-think-peaks:before{content:""}.fa-thumbs-down:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbtack:before{content:""}.fa-ticket-alt:before{content:""}.fa-times:before{content:""}.fa-times-circle:before{content:""}.fa-tint:before{content:""}.fa-tint-slash:before{content:""}.fa-tired:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-toilet:before{content:""}.fa-toilet-paper:before{content:""}.fa-toolbox:before{content:""}.fa-tools:before{content:""}.fa-tooth:before{content:""}.fa-torah:before{content:""}.fa-torii-gate:before{content:""}.fa-tractor:before{content:""}.fa-trade-federation:before{content:""}.fa-trademark:before{content:""}.fa-traffic-light:before{content:""}.fa-train:before{content:""}.fa-tram:before{content:""}.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-trash:before{content:""}.fa-trash-alt:before{content:""}.fa-trash-restore:before{content:""}.fa-trash-restore-alt:before{content:""}.fa-tree:before{content:""}.fa-trello:before{content:""}.fa-tripadvisor:before{content:""}.fa-trophy:before{content:""}.fa-truck:before{content:""}.fa-truck-loading:before{content:""}.fa-truck-monster:before{content:""}.fa-truck-moving:before{content:""}.fa-truck-pickup:before{content:""}.fa-tshirt:before{content:""}.fa-tty:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-tv:before{content:""}.fa-twitch:before{content:""}.fa-twitter:before{content:""}.fa-twitter-square:before{content:""}.fa-typo3:before{content:""}.fa-uber:before{content:""}.fa-ubuntu:before{content:""}.fa-uikit:before{content:""}.fa-umbrella:before{content:""}.fa-umbrella-beach:before{content:""}.fa-underline:before{content:""}.fa-undo:before{content:""}.fa-undo-alt:before{content:""}.fa-uniregistry:before{content:""}.fa-universal-access:before{content:""}.fa-university:before{content:""}.fa-unlink:before{content:""}.fa-unlock:before{content:""}.fa-unlock-alt:before{content:""}.fa-untappd:before{content:""}.fa-upload:before{content:""}.fa-ups:before{content:""}.fa-usb:before{content:""}.fa-user:before{content:""}.fa-user-alt:before{content:""}.fa-user-alt-slash:before{content:""}.fa-user-astronaut:before{content:""}.fa-user-check:before{content:""}.fa-user-circle:before{content:""}.fa-user-clock:before{content:""}.fa-user-cog:before{content:""}.fa-user-edit:before{content:""}.fa-user-friends:before{content:""}.fa-user-graduate:before{content:""}.fa-user-injured:before{content:""}.fa-user-lock:before{content:""}.fa-user-md:before{content:""}.fa-user-minus:before{content:""}.fa-user-ninja:before{content:""}.fa-user-nurse:before{content:""}.fa-user-plus:before{content:""}.fa-user-secret:before{content:""}.fa-user-shield:before{content:""}.fa-user-slash:before{content:""}.fa-user-tag:before{content:""}.fa-user-tie:before{content:""}.fa-user-times:before{content:""}.fa-users:before{content:""}.fa-users-cog:before{content:""}.fa-usps:before{content:""}.fa-ussunnah:before{content:""}.fa-utensil-spoon:before{content:""}.fa-utensils:before{content:""}.fa-vaadin:before{content:""}.fa-vector-square:before{content:""}.fa-venus:before{content:""}.fa-venus-double:before{content:""}.fa-venus-mars:before{content:""}.fa-viacoin:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-vial:before{content:""}.fa-vials:before{content:""}.fa-viber:before{content:""}.fa-video:before{content:""}.fa-video-slash:before{content:""}.fa-vihara:before{content:""}.fa-vimeo:before{content:""}.fa-vimeo-square:before{content:""}.fa-vimeo-v:before{content:""}.fa-vine:before{content:""}.fa-vk:before{content:""}.fa-vnv:before{content:""}.fa-voicemail:before{content:""}.fa-volleyball-ball:before{content:""}.fa-volume-down:before{content:""}.fa-volume-mute:before{content:""}.fa-volume-off:before{content:""}.fa-volume-up:before{content:""}.fa-vote-yea:before{content:""}.fa-vr-cardboard:before{content:""}.fa-vuejs:before{content:""}.fa-walking:before{content:""}.fa-wallet:before{content:""}.fa-warehouse:before{content:""}.fa-water:before{content:""}.fa-wave-square:before{content:""}.fa-waze:before{content:""}.fa-weebly:before{content:""}.fa-weibo:before{content:""}.fa-weight:before{content:""}.fa-weight-hanging:before{content:""}.fa-weixin:before{content:""}.fa-whatsapp:before{content:""}.fa-whatsapp-square:before{content:""}.fa-wheelchair:before{content:""}.fa-whmcs:before{content:""}.fa-wifi:before{content:""}.fa-wikipedia-w:before{content:""}.fa-wind:before{content:""}.fa-window-close:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-windows:before{content:""}.fa-wine-bottle:before{content:""}.fa-wine-glass:before{content:""}.fa-wine-glass-alt:before{content:""}.fa-wix:before{content:""}.fa-wizards-of-the-coast:before{content:""}.fa-wolf-pack-battalion:before{content:""}.fa-won-sign:before{content:""}.fa-wordpress:before{content:""}.fa-wordpress-simple:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpexplorer:before{content:""}.fa-wpforms:before{content:""}.fa-wpressr:before{content:""}.fa-wrench:before{content:""}.fa-x-ray:before{content:""}.fa-xbox:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-y-combinator:before{content:""}.fa-yahoo:before{content:""}.fa-yammer:before{content:""}.fa-yandex:before{content:""}.fa-yandex-international:before{content:""}.fa-yarn:before{content:""}.fa-yelp:before{content:""}.fa-yen-sign:before{content:""}.fa-yin-yang:before{content:""}.fa-yoast:before{content:""}.fa-youtube:before{content:""}.fa-youtube-square:before{content:""}.fa-zhihu:before{content:""}.sr-only{border:0;clip:rect(0, 0, 0, 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}/*! * Font Awesome Free 5.9.0 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - */@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:auto;src:url("./webfonts/fa-solid-900.eot");src:url("./webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"),url("./webfonts/fa-solid-900.woff2") format("woff2"),url("./webfonts/fa-solid-900.woff") format("woff"),url("./webfonts/fa-solid-900.ttf") format("truetype"),url("./webfonts/fa-solid-900.svg#fontawesome") format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900}html{font-size:87.5%;box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}body,h1,h2,h3,h4,h5,h6,p,figure,blockquote,dl,dd{margin:0;padding:0}ul[role=list],ol[role=list]{list-style:none}html:focus-within{scroll-behavior:smooth}body{text-rendering:optimizeSpeed;line-height:1.5;font-family:"Roboto",Arial,Helvetica,sans-serif !important;letter-spacing:-0.025ch}a:not([class]){text-decoration-skip-ink:auto}img,picture{max-width:100%;display:block}input,button,textarea,select{font:inherit}@media(prefers-reduced-motion: reduce){html:focus-within{scroll-behavior:auto}*,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important;scroll-behavior:auto !important}}#main{height:100vh}.content{display:flex;height:100%;overflow:hidden}.tab-content{display:flex;height:100%;width:100%;background-color:#eef3f6;animation:fadein .3s;overflow:auto}input[type=text],input[type=password],input[type=number],textarea{box-sizing:border-box;background:#fff;max-width:100%;font-size:1rem;font-weight:400;border:1px solid #ccc;border-radius:.25rem;padding:.25rem .5rem;outline:rgba(0,0,0,0)}input:focus{border:1px solid #3ba4d7;box-shadow:inset 0 0 5px #ccc}input.stretched{width:90%}input.small{max-width:70%;padding:.1rem}input.searchbar{width:40%}a{cursor:pointer}a[title=Back]{width:max-content;height:max-content;padding:.475rem .75rem;border-radius:50%;transition:100ms}a[title=Back]:hover{background:#eef3f6}table{padding:20px;table-layout:fixed;width:100%;border-collapse:collapse;text-align:center;color:#333;font-size:1.125rem}table th{font-size:1.125rem;color:#000;border-bottom:2px solid #eee}table tr{border-bottom:1px solid #eee}h3{color:#444}hr{margin-left:0;color:#aaa}.grid-2col{display:grid;grid-template-columns:auto auto;gap:1rem;justify-content:start}.grid-2col input[type=checkbox]{margin-top:20px}.error{color:red}.tooltip{color:#333;position:relative;display:inline-block;margin:0 .25rem}.tooltiptext{visibility:hidden;position:absolute;top:100%;left:50%;min-width:250px;margin-left:-120px;z-index:1;color:#ccc;background-color:#333;font-size:.875rem;text-align:center;padding:.25rem;border-radius:.5rem}.tooltip:hover .tooltiptext{visibility:visible;animation:fadein .5s}blockquote{color:#14141b;padding:.75rem 1rem .75rem 2rem;border-radius:.25rem}blockquote.info{position:relative;line-height:1.2;color:rgba(20,20,27,.8);border:1px solid rgba(17,143,204,.8)}blockquote.info::before{font-family:"Font Awesome 5 Free";position:absolute;top:.5rem;left:.5rem;content:"";color:#019dff}@keyframes fadein{from{opacity:0}to{opacity:1}}.fadein{animation:fadein .5s}@keyframes swipe-from-left{from{margin-left:100%}to{margin-left:0}}button{width:max-content;height:max-content;color:#fff;background:#019dff;font-size:1rem;padding:.4rem 1rem;border:0;border-radius:5px;cursor:pointer;box-shadow:inset -3px -3px 0 rgb(0,94.5826771654,154)}button:active{outline:none;box-shadow:inset 3px 3px 0 rgb(0,94.5826771654,154)}button.red{width:max-content;height:max-content;color:#fff;background:#ff3a4a;font-size:1rem;padding:.4rem 1rem;border:0;border-radius:5px;cursor:pointer;box-shadow:inset -3px -3px 0 rgb(211,0,17.1370558376)}button.red:active{outline:none;box-shadow:inset 3px 3px 0 rgb(211,0,17.1370558376)}.media-item{display:flex;margin-top:.5rem;padding:1rem;border:1px solid rgba(20,20,27,.1);border-radius:4px}.media-item__details{flex-basis:40%;display:flex;align-items:start;gap:.5rem}.media-item__details img{width:6rem;object-fit:contain}.media-item__desc{flex-basis:60%}.active-link{background:hsla(0,0%,100%,.1) !important}.nav-menu{background-color:#14141b;box-shadow:0 5px 5px #222;display:flex;flex-direction:column;align-items:center;height:100%;padding:.5rem .25rem;margin-right:0rem}.nav-menu__logo{padding:1.2rem 0;display:flex;align-items:center;gap:.3rem}.nav-menu__logo img{width:1.6rem}.nav-menu__logo h5{line-height:1;color:#fff}.nav-menu__box{padding:2rem .125rem;display:flex;flex-direction:column;gap:.5rem;position:relative}.nav-menu__box .item{margin:0;padding:.675rem .5rem;width:10rem;display:flex;align-items:center;line-height:1;border-radius:.5rem;text-decoration:none;color:#ccc;text-transform:capitalize;transition:0ms}.nav-menu__box .item:hover{background-color:rgba(238,243,246,.15)}.nav-menu__box .item i.sidenav-icon{width:2.5rem;height:1.4rem;display:grid;place-items:center}.nav-menu__box .item.item-selected{color:#9bdaff;background-color:rgba(155,218,255,.15);font-weight:medium}.nav-menu__box button.toggle-nav{display:none;position:absolute;padding:0;top:0;right:-1rem;background:rgb(77.5,186.5157480315,255);width:1.5rem;height:1.5rem;aspect-ratio:1;justify-content:center;align-items:center;border-radius:50%;box-shadow:none}.nav-menu.collapsed .nav-menu__logo .logo-container{display:flex;flex-direction:column;align-items:center;gap:.5rem}.nav-menu.collapsed .nav-menu__logo .logo-container>*:not(img){display:block}.nav-menu.collapsed .nav-menu__logo .nav-menu__logo-text{display:none !important}.nav-menu.collapsed .nav-menu__box .item{padding:.675rem 0;width:2.5rem;justify-content:center;transition:300ms}.nav-menu.collapsed .nav-menu__box .item span,.nav-menu.collapsed .nav-menu__box .item p{display:none !important}.nav-menu.collapsed button i{rotate:180deg}.nav-menu:hover button.toggle-nav{display:flex}.sidebar{width:13rem;background-color:#fff;display:flex;flex-direction:column}.sidebar a{text-decoration:none;text-transform:capitalize;padding:1rem;cursor:pointer;color:#999}.sidebar a:hover{color:#222}.sidebar .selected-sidebar-link{font-weight:bold;color:#222;border-left:5px solid #3ba4d7;animation:expand-left-border .1s}.sidebarquickview>h6{padding:.5rem}.sidebarquickview a{text-decoration:none;text-transform:capitalize;padding:.5rem 1rem;display:block;color:#999}.sidebarquickview a a:hover{color:#222}.sidebarquickview .selected-sidebarquickview-link{font-weight:bold;color:#222;border-left:5px solid #3ba4d7;animation:expand-left-border .1s}.node-panel{width:100%;padding:.5rem;animation:fadein .5s}@keyframes expand-left-border{from{border-left:0}to{border-left:5px solid #3ba4d7}}@media(max-width: 700px){.tab-content{flex-direction:column}.sidebar{width:100% !important;flex-direction:row !important;overflow-x:auto !important;overflow-y:hidden !important;white-space:nowrap !important;border-bottom:1px solid rgba(20,20,27,.1) !important;background:#fff !important;z-index:50 !important;flex-shrink:0 !important;height:auto !important;padding:0 !important}.sidebar a{display:inline-block !important;padding:.8rem 1.2rem !important;border-bottom:3px solid rgba(0,0,0,0) !important;border-left:none !important}.sidebar .selected-sidebar-link{border-left:none !important;border-bottom:3px solid #3ba4d7 !important;animation:none !important}.sidebarquickview>h4,.sidebarquickview>h6{display:none !important}}.posts{height:100%;margin-top:1rem;flex-direction:column;overflow:auto}.posts__heading{display:flex;flex-direction:column;justify-content:space-between}.posts-container{height:100%;padding:1rem;display:grid;grid-template-columns:repeat(auto-fill, minmax(150px, 1fr));gap:2rem;border:1px solid rgba(20,20,27,.1);border-radius:4px;overflow:auto}.posts-container-card{min-height:240px;flex-direction:column;border:1px solid rgba(20,20,27,.5);border-radius:4px;cursor:pointer;text-align:center}.posts-container-card img{flex-basis:90%;object-fit:cover}.posts-container-card p{padding:0 .125rem;flex-basis:10%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.progress-bar{width:100%;height:2rem;position:relative;text-align:center;background-color:#eef3f6;border-radius:20px;overflow:hidden}.progress-bar__status{position:absolute;top:0;left:0;height:100%;color:#14141b;background-color:#019dff}.progress-bar__percent{position:absolute;inset:0;margin:auto;width:fit-content;height:fit-content}.progress-bar-chunks{position:relative;margin-top:.5rem;width:100%;height:2rem;display:flex;border-radius:.25rem;overflow:hidden;background-color:#eef3f6}.progress-bar-chunks .chunk{width:100%}.progress-bar-chunks .chunk[data-chunkVal="0"]{background-color:rgba(155,218,255,.2)}.progress-bar-chunks .chunk[data-chunkVal="1"]{background-color:#ff3a4a}.progress-bar-chunks .chunk[data-chunkVal="2"]{background-color:#019dff}.progress-bar-chunks .chunk[data-chunkVal="3"]{background-color:#fcba03}.progress-bar-chunks__percent{position:absolute;inset:0;margin:auto;width:fit-content;height:fit-content}.widget{height:100%;padding:1rem;display:flex;flex-direction:column;gap:.5rem;background-color:#fff;border-radius:.5rem;overflow:auto}.widget .top-heading{display:flex;justify-content:space-between}.widget__heading{display:flex;justify-content:space-between;align-items:center;border-bottom:2px solid #999}.widget__body{height:100%;display:flex;flex-direction:column;overflow:auto}.widget__body-heading{display:flex;justify-content:space-between;align-items:center}.widget__body-heading .action{display:flex;gap:.5rem}.widget__body-content{height:100%;overflow:auto}.widget__body-box{display:flex;flex-direction:column;gap:.5rem}.widget-half{max-width:50%}#modal-container{display:none;position:fixed;z-index:1;height:100%;top:0;left:0;width:100%;background-color:rgba(0,0,0,.2)}.modal-content{position:absolute;color:#555;width:40%;min-height:10rem;height:max-content;padding:1.5rem;inset:0;margin:auto;background-color:#fff;border-radius:.5rem;animation:fadein .5s;display:flex;flex-direction:column}.modal-content button:last-child{margin-top:auto}.modal-content .close-btn{position:absolute;right:1.5rem}.modal-content .widget{padding:0}#notification-container{position:absolute;bottom:0;right:0}.login-page{background-image:linear-gradient(-45deg, rgba(1, 157, 255, 0.75), rgba(17, 143, 204, 0.75));height:100%;animation:fadein .5s}.login-page .login-container{background-color:#fff;box-shadow:3px 3px 5px rgba(20,20,27,.4);margin:auto;position:relative;top:100px;max-width:400px;max-height:500px;border-radius:5px;display:flex;flex-direction:column;align-items:center}.login-page .login-container input{padding:.375rem .75rem;border-radius:.275rem}.login-page .login-container *{margin-bottom:1rem}.login-page .login-container>img{margin:1rem 0 2rem}.login-page .login-container extra{margin:0}.login-page .login-container>a{text-decoration:underline;cursor:pointer}.login-page .extra>label,.login-page .extra>br,.login-page .extra>input{margin-bottom:0}.homepage{margin:2rem auto 0;display:flex;flex-direction:column;gap:4rem}.homepage .logo{display:flex;justify-content:center;align-items:center}.homepage .logo img{width:90px}.homepage .logo .retroshareText{display:flex;flex-direction:column;align-items:center}.homepage .logo .retroshareText .retrotext{font-size:36px;font-weight:600;line-height:1.125}.homepage .logo .retroshareText .retrotext>span{color:#118fcc}.homepage .logo .retroshareText>b{font-size:14px;line-height:1}.homepage .certificate{display:flex;flex-direction:column;gap:4rem}.homepage .certificate__heading{text-align:center}.homepage .certificate__heading>h1{margin-bottom:1rem}.homepage .certificate__content{display:flex;flex-direction:column;gap:2rem;padding:2rem;text-align:center;border:1.5px solid rgba(17,143,204,.2);border-radius:6px;box-shadow:0px 0px 8px 2px rgba(20,20,27,.05)}.homepage .certificate__content .rsId>p{margin-bottom:.5rem;color:#118fcc}.homepage .certificate__content .retroshareID{padding:.25rem;display:flex;align-items:center;justify-self:start;font-size:1.25rem;border-radius:4px;background:rgba(20,20,27,.05)}.homepage .certificate__content .retroshareID .textArea{padding:0;width:100%;height:auto;font-size:1rem;font-family:monospace;background:rgba(0,0,0,0);border:none;resize:none;overflow:hidden;field-sizing:content}.homepage .certificate__content .retroshareID i{color:#118fcc}.homepage .certificate__content .retroshareID>i{margin:0 .5rem;cursor:pointer}.homepage .certificate__content .webhelp{padding:.5rem;background:#f5f5f5;display:flex;justify-content:center;align-items:center;gap:.5rem;border-radius:4px;border:1px solid rgba(20,20,27,.5);width:fit-content;cursor:pointer}.homepage .certificate__content .webhelp-container{display:grid;place-items:center}.homepage .certificate__content .webhelp:hover{background:#eef3f6;border:1px solid #14141b}.homepage .certificate__content .webhelp>i{font-size:1.2rem;color:green}.homepage .certificate__content .add-friend>h6,.homepage .certificate__content .webhelp-container>h6{font-weight:normal;margin-bottom:.5rem}.network-container{display:flex;height:100%;width:100%;overflow:hidden;background-color:#f1f5f9}.network-left-pane{width:320px;min-width:300px;max-width:350px;border-right:1px solid #cbd5e1;display:flex;flex-direction:column;background:#fff;box-shadow:2px 0 5px rgba(0,0,0,.05)}.own-profile-card{padding:1.25rem;border-bottom:1px solid #e2e8f0;background:linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);display:flex;flex-direction:column;gap:.75rem}.own-profile-card .profile-header{display:flex;align-items:center;gap:1rem}.own-profile-card .profile-info{display:flex;flex-direction:column;flex:1;overflow:hidden}.own-profile-card .profile-info .profile-name{font-weight:700;color:#1e293b;font-size:1.1rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.own-profile-card .profile-info .profile-status{font-size:.85rem;color:#10b981;font-weight:500;display:flex;align-items:center;gap:.35rem}.own-profile-card .profile-info .profile-status::before{content:"";display:inline-block;width:8px;height:8px;background-color:#10b981;border-radius:50%}.own-profile-card .own-identity-select-container{display:flex;flex-direction:column;gap:.25rem}.own-profile-card .own-identity-select-container label{font-size:.75rem;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:.05em}.own-profile-card .own-identity-select-container select.own-identity-select{width:100%;padding:.375rem .5rem;font-size:.85rem;border:1px solid #cbd5e1;border-radius:.375rem;background-color:#fff;color:#334155;outline:none;cursor:pointer;transition:border-color .2s}.own-profile-card .own-identity-select-container select.own-identity-select:focus{border-color:#3ba4d7}.friends-list-container{flex:1;display:flex;flex-direction:column;overflow:hidden}.friends-list-container .searchbar-container{padding:.75rem 1rem;border-bottom:1px solid #e2e8f0}.friends-list-container .searchbar-container input.searchbar{width:100%;padding:.5rem .75rem;font-size:.9rem;border:1px solid #cbd5e1;border-radius:.375rem;background-color:#f8fafc;outline:none;transition:all .2s}.friends-list-container .searchbar-container input.searchbar:focus{background-color:#fff;border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.friends-list-container .friends-scroll{flex:1;overflow-y:auto;padding:.5rem 0}.friend-list-item{display:flex;align-items:center;gap:.75rem;padding:.75rem 1rem;margin:.125rem .5rem;border-radius:.5rem;cursor:pointer;transition:all .2s}.friend-list-item:hover{background-color:#f1f5f9}.friend-list-item.selected{background-color:#e0f2fe}.friend-list-item.selected .friend-name{color:#0369a1;font-weight:600}.friend-list-item .friend-avatar{flex-shrink:0}.friend-list-item .friend-meta{flex:1;min-width:0}.friend-list-item .friend-meta .friend-name{font-size:.95rem;color:#334155;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .2s}.friend-list-item .friend-meta .friend-status{font-size:.8rem;color:#94a3b8}.friend-list-item .friend-meta .friend-status.online{color:#10b981;font-weight:500}.network-right-pane{flex:1;display:flex;flex-direction:column;overflow:hidden;background-color:#f8fafc}.network-pane-placeholder{flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;color:#94a3b8;gap:1rem;padding:2rem;text-align:center}.network-pane-placeholder i{font-size:4rem;color:#cbd5e1}.network-pane-placeholder p{font-size:1.1rem;max-width:400px}.network-tabs{display:flex;background-color:#fff;border-bottom:1px solid #cbd5e1;padding:.5rem 1rem 0;gap:.5rem}.network-tabs .tab-btn{padding:.625rem 1.25rem;font-size:.95rem;font-weight:600;color:#64748b;background:rgba(0,0,0,0);border:none;border-radius:.375rem .375rem 0 0;border-bottom:3px solid rgba(0,0,0,0);cursor:pointer;box-shadow:none;transition:all .2s}.network-tabs .tab-btn:hover{color:#334155;background-color:#f1f5f9}.network-tabs .tab-btn.active{color:#3ba4d7;border-bottom-color:#3ba4d7;background-color:rgba(0,0,0,0)}.network-tab-content{flex:1;overflow-y:auto;padding:1.5rem}.network-detail-view{display:flex;flex-direction:column;gap:1.5rem}.network-detail-view .detail-header{display:flex;align-items:center;gap:1.5rem;padding-bottom:1.5rem;border-bottom:1px solid #e2e8f0}.network-detail-view .detail-header .detail-title{flex:1}.network-detail-view .detail-header .detail-title h2{font-size:1.75rem;font-weight:800;color:#1e293b;margin-bottom:.25rem}.network-detail-view .detail-header .detail-title .detail-subtitle{font-size:.9rem;color:#64748b;display:flex;align-items:center;gap:.5rem}.network-detail-view .detail-header .detail-actions{display:flex;gap:.75rem}.network-detail-view .detail-header .detail-actions button{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.9rem}.network-detail-view .detail-section{background-color:#fff;border-radius:.5rem;border:1px solid #e2e8f0;padding:1.25rem;box-shadow:0 1px 3px rgba(0,0,0,.02)}.network-detail-view .detail-section h3{font-size:1.1rem;font-weight:700;color:#334155;margin-bottom:1rem;padding-bottom:.5rem;border-bottom:1px solid #f1f5f9}.network-detail-view .detail-section .info-grid{display:grid;grid-template-columns:120px 1fr;row-gap:.75rem;font-size:.9rem}.network-detail-view .detail-section .info-grid .info-label{font-weight:600;color:#64748b}.network-detail-view .detail-section .info-grid .info-value{color:#1e293b;word-break:break-all}.network-detail-view .locations-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));gap:1rem}.network-detail-view .location-card{background-color:#fff;border:1px solid #e2e8f0;border-radius:.5rem;padding:1rem;display:flex;flex-direction:column;gap:.5rem;box-shadow:0 1px 3px rgba(0,0,0,.02)}.network-detail-view .location-card .loc-header{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #f1f5f9;padding-bottom:.5rem;margin-bottom:.25rem}.network-detail-view .location-card .loc-header .loc-name{font-weight:700;color:#334155;font-size:.95rem}.network-detail-view .location-card .loc-header .loc-status{font-size:.75rem;font-weight:600;padding:.125rem .5rem;border-radius:.25rem}.network-detail-view .location-card .loc-header .loc-status.online{background-color:#d1fae5;color:#065f46}.network-detail-view .location-card .loc-header .loc-status.offline{background-color:#f1f5f9;color:#475569}.network-detail-view .location-card .loc-body{font-size:.85rem;display:grid;grid-template-columns:80px 1fr;row-gap:.25rem}.network-detail-view .location-card .loc-body .loc-label{color:#64748b}.network-detail-view .location-card .loc-body .loc-val{color:#334155;word-break:break-all}.network-detail-view .location-card .loc-footer{margin-top:.5rem;display:flex;justify-content:flex-end}.network-detail-view .location-card .loc-footer button{font-size:.8rem;padding:.25rem .75rem}.network-chat-view{display:flex;flex-direction:column;height:100%;overflow:hidden;background-color:#f8fafc}.network-chat-view .chat-messages{flex:1;overflow-y:auto;padding:1.25rem;display:flex;flex-direction:column;gap:1rem}.network-chat-view .chat-bubble-container{display:flex;flex-direction:column;max-width:70%}.network-chat-view .chat-bubble-container.outgoing{align-self:flex-end;align-items:flex-end}.network-chat-view .chat-bubble-container.outgoing .chat-bubble{background-color:#3ba4d7;color:#fff;border-bottom-right-radius:.125rem}.network-chat-view .chat-bubble-container.incoming{align-self:flex-start;align-items:flex-start}.network-chat-view .chat-bubble-container.incoming .chat-bubble{background-color:#fff;color:#1e293b;border:1px solid #e2e8f0;border-bottom-left-radius:.125rem}.network-chat-view .chat-bubble-container .chat-sender{font-size:.75rem;color:#64748b;margin-bottom:.25rem;padding:0 .25rem}.network-chat-view .chat-bubble-container .chat-bubble{padding:.625rem .875rem;border-radius:.75rem;font-size:.925rem;line-height:1.4;white-space:break-spaces;word-break:break-word;box-shadow:0 1px 2px rgba(0,0,0,.05)}.network-chat-view .chat-bubble-container .chat-time{font-size:.7rem;color:#94a3b8;margin-top:.25rem;padding:0 .25rem}.network-chat-view .chat-input-area{padding:1rem;background-color:#fff;border-top:1px solid #cbd5e1;display:flex;gap:.75rem;align-items:center}.network-chat-view .chat-input-area textarea.chat-textarea{flex:1;resize:none;height:40px;padding:.5rem .75rem;border:1px solid #cbd5e1;border-radius:.375rem;font-size:.9rem;outline:none;transition:all .2s}.network-chat-view .chat-input-area textarea.chat-textarea:focus{border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.network-chat-view .chat-input-area button.send-btn{padding:.5rem 1.25rem;font-size:.9rem;height:40px;display:flex;align-items:center;gap:.5rem}.network-chat-view .chat-warning{flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;color:#64748b;text-align:center;padding:2rem;gap:1rem}.network-chat-view .chat-warning i{font-size:3rem;color:#cbd5e1}.network-chat-view .chat-warning h4{font-weight:700;color:#334155}.network-chat-view .chat-warning p{max-width:350px;font-size:.9rem}.identity{color:#444;font-size:1.1em;margin:20px;padding:10px;border:1px solid #aaa;border-radius:20px}.identity>h4{margin:5px;font-size:1.3em}.identity button{font-size:.9em}.identity .details{display:grid;grid-template-columns:140px auto;grid-row-gap:5px;justify-content:left}.defaultAvatar{width:3rem;height:3rem;aspect-ratio:1;background:#b0c4de;border-radius:50%;display:grid;place-items:center}.defaultAvatar p{font-weight:900;color:#666f7f;transform:translateY(1px)}img.avatar{display:block;width:3rem;height:max-content;aspect-ratio:1;margin-right:.3em;border-radius:50%}.counter{margin-left:.5em}.counter:before{content:"("}.counter:after{content:")"}.chatInit{margin-left:.5em;color:green;cursor:pointer}.lobby{margin:10px;border:1px solid #aaa;border-radius:20px}.lobby .mainname{margin:20px;font-weight:100;font-size:1.2em}.topic{color:#666}.lobby>.topic{font-size:.95em;margin-left:25px;margin-bottom:5px}.lefttitle{margin-top:15px;margin-bottom:0;font-weight:100;font-size:1.2em}.leftname{margin-top:5px;margin-bottom:5px;padding:5px;font-weight:100;font-size:1em}.leftlobby>.topic{font-size:.75em;margin-left:15px;margin-bottom:5px}.subscribed,.public{cursor:pointer}.leftlobby{border:1px solid #aaa;border-radius:10px;margin-top:5px;background-color:#fff}.leftlobby.selected-lobby,.selectedidentity{color:#fff;background-color:#3ba4d7}.rightbar{position:absolute;width:185px;background-color:#fff;overflow:auto;top:130px;bottom:15px;right:15px}.user{padding:5px}.lobbyName{padding:15px;margin-top:2rem}.lobbies{position:absolute;width:185px;left:165px;bottom:15px;top:130px;overflow:auto}.messages,.setup{position:absolute;background-color:#fff;top:130px;left:360px;right:215px;overflow:auto}.messages{bottom:115px}.messagetext{white-space:break-spaces;margin-right:5px}.message>*{margin-left:5px}.username{color:#006400;font-weight:bolder}.chatMessage{position:absolute;background-color:#fff;height:85px;bottom:15px;right:215px;left:360px}textarea.chatMsg{height:100%;width:100%}.chatatchar{margin-left:.2em;margin-right:.2em;color:silver}.setupicon{margin-left:1em;cursor:pointer}.leaveicon{margin-left:1em;cursor:pointer;color:#d40000}.selectidentity{margin:15px;font-size:1.2em}.setup>.identity{cursor:pointer}.setup{bottom:15px}.createDistantChat{margin-top:1em}.no-lobbies .messages,.no-lobbies .chatMessage,.no-lobbies .setup{left:165px}@media(min-width: 900px){.node-panel.chat-room{display:grid !important;grid-template-columns:250px 1fr 200px !important;grid-template-rows:auto 1fr auto !important;grid-template-areas:"lobbies header rightbar" "lobbies messages rightbar" "lobbies input rightbar" !important;padding:0 !important;height:100% !important}.node-panel.chat-room .lobbyName{grid-area:header;padding:10px;border-bottom:1px solid #eee;margin:0;z-index:10;background:#fff}.node-panel.chat-room .lobbies{grid-area:lobbies;position:static !important;width:auto !important;height:auto !important;border-right:1px solid #ccc;overflow-y:auto;display:block !important;top:auto !important;bottom:auto !important;left:auto !important}.node-panel.chat-room .messages{grid-area:messages;position:static !important;width:auto !important;height:auto !important;overflow-y:auto;padding:10px;left:auto !important;right:auto !important;top:auto !important;bottom:auto !important;margin:0 !important}.node-panel.chat-room .rightbar{grid-area:rightbar;position:static !important;width:auto !important;border-left:1px solid #ccc;overflow-y:auto;display:block !important}.node-panel.chat-room .chatMessage{grid-area:input;position:static !important;width:auto !important;height:auto !important;border-top:1px solid #eee;left:auto !important;right:auto !important;bottom:auto !important;flex:0 0 auto;padding:10px !important;background:#fff;z-index:10}}@media(max-width: 899px){.node-panel.chat-room{display:flex !important;flex-direction:column !important;height:100% !important;position:relative !important}.node-panel.chat-room .lobbyName{flex:0 0 auto}.node-panel.chat-room .messages{flex:1 !important;overflow-y:auto !important;position:relative !important;top:0 !important;bottom:0 !important;left:0 !important;right:0 !important;width:100% !important;height:auto !important;margin:0 !important}.node-panel.chat-room .chatMessage{flex:0 0 auto !important;position:relative !important;bottom:0 !important;left:0 !important;right:0 !important;width:100% !important;height:auto !important;z-index:100}.node-panel.chat-room .rightbar,.node-panel.chat-room .lobbies{display:none !important;position:fixed !important;top:60px !important;bottom:0 !important;width:80% !important;background:#fff !important;z-index:200 !important;box-shadow:2px 0 10px rgba(0,0,0,.2) !important}.node-panel.chat-room.show-lobbies .lobbies{display:block !important;left:0 !important}.node-panel.chat-room.show-users .rightbar{display:block !important;right:0 !important}.chat-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.4);z-index:150}.show-lobbies .chat-overlay,.show-users .chat-overlay{display:block}.mobile-menu-icons{display:flex;gap:15px;font-size:1.2rem}.mobile-menu-icons i{cursor:pointer;padding:5px}}@media(min-width: 900px){.mobile-menu-icons{display:none}}.chat-hub-container{display:flex;height:100%;width:100%;overflow:hidden;background-color:#f1f5f9}.chat-hub-left-pane{width:320px;min-width:300px;max-width:350px;border-right:1px solid #cbd5e1;display:flex;flex-direction:column;background:#fff;box-shadow:2px 0 5px rgba(0,0,0,.05)}.chat-own-profile-card{padding:1.25rem;border-bottom:1px solid #e2e8f0;background:linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)}.chat-own-profile-card .profile-header{display:flex;align-items:center;gap:1rem}.chat-own-profile-card .profile-info{display:flex;flex-direction:column;flex:1;overflow:hidden}.chat-own-profile-card .profile-info .profile-name{font-weight:700;color:#1e293b;font-size:1.1rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-own-profile-card .profile-info .profile-status{font-size:.85rem;color:#10b981;font-weight:500;display:flex;align-items:center;gap:.35rem}.chat-own-profile-card .profile-info .profile-status::before{content:"";display:inline-block;width:8px;height:8px;background-color:#10b981;border-radius:50%}.chat-rooms-list-container{flex:1;display:flex;flex-direction:column;overflow:hidden}.chat-rooms-list-container .searchbar-container{padding:.75rem 1rem;border-bottom:1px solid #e2e8f0}.chat-rooms-list-container .searchbar-container input.searchbar{width:100%;padding:.5rem .75rem;font-size:.9rem;border:1px solid #cbd5e1;border-radius:.375rem;background-color:#f8fafc;outline:none;transition:all .2s}.chat-rooms-list-container .searchbar-container input.searchbar:focus{background-color:#fff;border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.chat-rooms-list-container .rooms-scroll{flex:1;overflow-y:auto;padding:.5rem 0}.rooms-section-title{display:flex;align-items:center;gap:.5rem;padding:.75rem 1rem .375rem;font-size:.75rem;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.05em}.rooms-section-title i{font-size:.7rem;color:#94a3b8}.chat-room-list-item{display:flex;align-items:center;gap:.75rem;padding:.75rem 1rem;margin:.125rem .5rem;border-radius:.5rem;cursor:pointer;transition:all .2s}.chat-room-list-item:hover{background-color:#f1f5f9}.chat-room-list-item.selected{background-color:#e0f2fe}.chat-room-list-item.selected .room-name{color:#0369a1;font-weight:600}.chat-room-list-item .room-icon{flex-shrink:0;width:36px;height:36px;border-radius:.5rem;background:linear-gradient(135deg, #3ba4d7, #0ea5e9);display:flex;align-items:center;justify-content:center;color:#fff;font-size:.85rem}.chat-room-list-item.public-room .room-icon{background:linear-gradient(135deg, #10b981, #059669)}.chat-room-list-item .room-meta{flex:1;min-width:0}.chat-room-list-item .room-meta .room-name{font-size:.95rem;color:#334155;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .2s}.chat-room-list-item .room-meta .room-topic{font-size:.8rem;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-room-list-item .room-badge{flex-shrink:0;min-width:24px;height:24px;border-radius:12px;background-color:#e2e8f0;color:#475569;font-size:.75rem;font-weight:700;display:flex;align-items:center;justify-content:center;padding:0 .375rem}.chat-hub-right-pane{flex:1;display:flex;flex-direction:column;overflow:hidden;background-color:#f8fafc}.chat-pane-placeholder{flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;color:#94a3b8;gap:1rem;padding:2rem;text-align:center}.chat-pane-placeholder i{font-size:4rem;color:#cbd5e1}.chat-pane-placeholder p{font-size:1.1rem;max-width:400px}.chat-hub-tab-content{flex:1;overflow-y:auto;padding:1.5rem}.chat-room-detail-view{display:flex;flex-direction:column;gap:1.5rem}.chat-room-detail-view .detail-header{display:flex;align-items:flex-start;gap:1.5rem;padding-bottom:1.5rem;border-bottom:1px solid #e2e8f0;flex-wrap:wrap}.chat-room-detail-view .detail-header .detail-title{flex:1;min-width:200px}.chat-room-detail-view .detail-header .detail-title h2{font-size:1.75rem;font-weight:800;color:#1e293b;margin-bottom:.25rem}.chat-room-detail-view .detail-header .detail-title .detail-subtitle{font-size:.9rem;color:#64748b;display:flex;align-items:center;gap:.5rem}.chat-room-detail-view .detail-header .detail-actions{display:flex;gap:.75rem;flex-wrap:wrap}.chat-room-detail-view .detail-header .detail-actions button{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.9rem}.chat-room-detail-view .detail-section{background-color:#fff;border-radius:.5rem;border:1px solid #e2e8f0;padding:1.25rem;box-shadow:0 1px 3px rgba(0,0,0,.02)}.chat-room-detail-view .detail-section h3{font-size:1.1rem;font-weight:700;color:#334155;margin-bottom:1rem;padding-bottom:.5rem;border-bottom:1px solid #f1f5f9}.chat-room-detail-view .detail-section .info-grid{display:grid;grid-template-columns:130px 1fr;row-gap:.75rem;font-size:.9rem}.chat-room-detail-view .detail-section .info-grid .info-label{font-weight:600;color:#64748b}.chat-room-detail-view .detail-section .info-grid .info-value{color:#1e293b;word-break:break-all}.participants-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(180px, 1fr));gap:.5rem}.participant-card{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;background-color:#f8fafc;border:1px solid #e2e8f0;border-radius:.375rem}.participant-card .participant-name{font-size:.875rem;color:#334155;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.no-participants{color:#94a3b8;font-size:.9rem;font-style:italic}.detail-actions-footer{display:flex;gap:.75rem}.detail-actions-footer button{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.9rem}.join-description{color:#64748b;font-size:.9rem;margin-bottom:1rem}.identities-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(200px, 1fr));gap:.75rem}.identity-card{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1rem;background-color:#f8fafc;border:1px solid #e2e8f0;border-radius:.5rem;cursor:pointer;transition:all .2s}.identity-card:hover{background-color:#e0f2fe;border-color:#3ba4d7}.identity-card .identity-name{font-size:.95rem;font-weight:600;color:#334155}.identity-card i{color:#3ba4d7;font-size:.9rem}.no-rooms{padding:1rem;color:#94a3b8;text-align:center;font-style:italic}@media(max-width: 899px){.chat-hub-container{flex-direction:column}.chat-hub-left-pane{width:100%;min-width:0;max-width:none;max-height:45%;border-right:none;border-bottom:1px solid #cbd5e1}.chat-hub-right-pane{flex:1;min-height:0}}.chat-hub-header-bar{padding:.75rem 1.5rem;background-color:#fff;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;justify-content:space-between;height:65px;flex-shrink:0}.chat-hub-header-bar .chat-header-info{display:flex;flex-direction:column;overflow:hidden}.chat-hub-header-bar .chat-header-info .chat-header-name{font-size:1.15rem;font-weight:800;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-hub-header-bar .chat-header-info .chat-header-topic{font-size:.85rem;color:#64748b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:.125rem}.chat-hub-header-bar .chat-header-actions{display:flex;gap:.5rem}.chat-hub-header-bar .chat-header-actions button{display:flex;align-items:center;gap:.35rem;padding:.375rem .75rem;font-size:.85rem}.chat-hub-tabs-container{background-color:#fff;border-bottom:1px solid #cbd5e1;padding:.5rem 1.5rem 0}.chat-hub-tabs{display:flex;gap:.5rem}.chat-hub-tabs .tab-btn{padding:.625rem 1.25rem;font-size:.95rem;font-weight:600;color:#64748b;background:rgba(0,0,0,0);border:none;border-radius:.375rem .375rem 0 0;border-bottom:3px solid rgba(0,0,0,0);cursor:pointer;box-shadow:none;transition:all .2s;display:flex;align-items:center;gap:.5rem}.chat-hub-tabs .tab-btn:hover{color:#334155;background-color:#f1f5f9}.chat-hub-tabs .tab-btn.active{color:#3ba4d7;border-bottom-color:#3ba4d7;background-color:rgba(0,0,0,0)}.chat-hub-tab-content{flex:1;display:flex;flex-direction:column;overflow:hidden;background-color:#f8fafc}.chat-hub-conversation-layout{display:flex;flex-direction:row;height:100%;width:100%;overflow:hidden}.chat-hub-conversation-main{display:flex;flex-direction:column;flex:1;height:100%;overflow:hidden}.chat-hub-rightbar{width:200px;border-left:1px solid #cbd5e1;background-color:#fff;display:flex;flex-direction:column;flex-shrink:0}.chat-hub-rightbar .rightbar-title{padding:.75rem 1rem;font-size:.85rem;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid #e2e8f0}.chat-hub-rightbar .rightbar-users-list{flex:1;overflow-y:auto;padding:.5rem}.chat-hub-rightbar .user{padding:.5rem .75rem;font-size:.9rem;color:#334155;border-radius:.375rem;transition:all .2s;display:flex;align-items:center;gap:.5rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-hub-rightbar .user:hover{background-color:#f1f5f9;color:#0f172a}.chat-hub-rightbar .user .defaultAvatar{width:2rem;height:2rem;font-size:.9rem;flex-shrink:0}.chat-hub-rightbar .user img.avatar{width:2rem;height:2rem;flex-shrink:0}@media(max-width: 899px){.chat-hub-rightbar{display:none}}.chat-hub-messages{flex:1;overflow-y:auto;padding:1.25rem 1.5rem;display:flex;flex-direction:column;gap:1rem}.chat-hub-messages .message{display:flex;flex-direction:column;max-width:70%;padding:.625rem .875rem;border-radius:.75rem;font-size:.925rem;line-height:1.4;word-break:break-word;box-shadow:0 1px 2px rgba(0,0,0,.05)}.chat-hub-messages .message.incoming{align-self:flex-start;align-items:flex-start;background-color:#fff;color:#1e293b;border:1px solid #e2e8f0;border-bottom-left-radius:.125rem}.chat-hub-messages .message.outgoing{align-self:flex-end;align-items:flex-end;background-color:#3ba4d7;color:#fff;border-bottom-right-radius:.125rem}.chat-hub-messages .message .username{font-size:.75rem;margin-bottom:.25rem;padding:0 .125rem;font-weight:700}.chat-hub-messages .message.incoming .username{color:#0369a1}.chat-hub-messages .message.outgoing .username{color:#e0f2fe}.chat-hub-messages .message .messagetext{white-space:break-spaces;margin:0}.chat-hub-messages .message .datetime{font-size:.7rem;margin-top:.25rem;padding:0 .125rem;opacity:.8}.chat-hub-messages .message.incoming .datetime{color:#64748b}.chat-hub-messages .message.outgoing .datetime{color:#f1f5f9}.chat-hub-input-area{padding:1rem 1.5rem;background-color:#fff;border-top:1px solid #cbd5e1;display:flex;gap:.75rem;align-items:center;flex-shrink:0}.chat-hub-input-area textarea.chat-hub-textarea{flex:1;resize:none;height:40px;padding:.5rem .75rem;border:1px solid #cbd5e1;border-radius:.375rem;font-size:.9rem;outline:none;transition:all .2s;background-color:#f8fafc}.chat-hub-input-area textarea.chat-hub-textarea:focus{background-color:#fff;border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.chat-hub-input-area button.chat-hub-send-btn{padding:.5rem 1.25rem;font-size:.9rem;height:40px;display:flex;align-items:center;gap:.5rem;border-radius:.375rem}.chat-hub-messages.compact-container,.messages.compact-container{gap:0 !important;padding:.75rem 1rem !important;background-color:#fff !important;display:flex !important;flex-direction:column !important}.chat-hub-messages.compact-container .message.compact,.messages.compact-container .message.compact{display:block !important;max-width:100% !important;padding:.1rem 0 !important;border-radius:0 !important;background-color:rgba(0,0,0,0) !important;border:none !important;box-shadow:none !important;align-self:flex-start !important;font-size:.875rem !important;line-height:1.45 !important;margin:0 !important;white-space:nowrap !important}.chat-hub-messages.compact-container .message.compact:hover,.messages.compact-container .message.compact:hover{background-color:#f8fafc !important;overflow:visible !important;white-space:normal !important}.chat-hub-messages.compact-container .message.compact .datetime,.messages.compact-container .message.compact .datetime{color:#a0a0a0 !important;margin-right:.4rem !important;font-size:.78rem !important;font-family:monospace !important;opacity:1 !important;display:inline !important}.chat-hub-messages.compact-container .message.compact .username,.messages.compact-container .message.compact .username{font-weight:bold !important;margin-right:.2rem !important;font-size:.875rem !important;display:inline !important}.chat-hub-messages.compact-container .message.compact .messagetext,.messages.compact-container .message.compact .messagetext{color:#1e293b !important;white-space:normal !important;word-break:break-word !important;display:inline !important;margin:0 !important}.side-bar{display:flex;flex-direction:column;background:#fff}.side-bar .mail-compose-btn{width:96%;margin:.25rem;padding:.75rem 0}.compose-mail__from{display:flex;justify-content:flex-start;align-items:center;gap:.5rem;padding-bottom:.5rem;border-bottom:2px solid #eef3f6}.compose-mail__recipients{padding:.5rem 0;display:flex;flex-direction:column;gap:.5rem;border-bottom:2px solid #eef3f6}.compose-mail__recipients__container{display:flex;gap:.5rem}.compose-mail__recipients__container>label{text-transform:capitalize}.compose-mail__recipients__container .recipients{width:100%;display:flex;gap:.5rem;flex-wrap:wrap}.compose-mail__recipients__container .recipients__selected{padding:.125rem .5rem;display:flex;align-items:center;gap:.5rem;border:1px solid #eef3f6;border-radius:3px;cursor:default}.compose-mail__recipients__container .recipients__selected i{cursor:pointer;padding:.25rem}.compose-mail__recipients__container .recipients__input{display:flex;position:relative;flex-grow:1}.compose-mail__recipients__container .recipients__input-field{flex-grow:1;min-width:200px;padding:0;border:none;box-shadow:none}.compose-mail__recipients__container .recipients__input-field:focus+.recipients__input-list{display:flex}.compose-mail__recipients__container .recipients__input-list{z-index:1;position:absolute;top:1rem;padding:0;width:100%;max-height:15rem;flex-direction:column;overflow:auto;display:none;background:#fff;border-top:1px solid #eef3f6;border-bottom:1px solid #eef3f6}.compose-mail__recipients__container .recipients__input-list:hover{display:flex}.compose-mail__recipients__container .recipients__input-list li{list-style:none;padding:.25rem .5rem;cursor:pointer;background:#fff;border:1px solid #eef3f6;border-top:0px}.compose-mail__recipients__container .recipients__input-list li:hover{background:#eef3f6}.compose-mail__recipients__container .recipients__input-list li:last-child{border-bottom:0px}.compose-mail__recipients .remove-recipient{padding:.125rem .5rem}.compose-mail input[type=text].compose-mail__subject{padding:.5rem 0;border:none;box-shadow:none;border-bottom:2px solid #eef3f6;border-radius:0}.compose-mail__message{margin:.5rem 0;height:100%;display:flex;flex-direction:column;overflow:auto}.compose-mail__message-body{height:100%;outline:rgba(0,0,0,0)}.compose-mail__send-btn{display:flex;align-items:center;gap:.5rem}.compose-mail__send-btn i{transform:translateY(-1px)}.msg-view{height:100%;display:flex;flex-direction:column;gap:1rem;overflow:auto}.msg-view-nav{display:flex;justify-content:space-between;align-items:column}.msg-view-nav__action{display:flex;gap:.5rem}.msg-view__header{display:flex;flex-direction:column;gap:1rem}.msg-view__header>h3{line-height:1}.msg-view__header .msg-details{display:flex;gap:1rem}.msg-view__header .msg-details__avatar{height:max-content}.msg-view__header .msg-details__info{display:flex;flex-direction:column}.msg-view__header .msg-details__info-item{display:flex;gap:.5rem}.msg-view__body{height:100%;overflow:auto;font-size:14px !important}.msg-view__attachment{height:50%;overflow:auto;display:flex;flex-direction:column}.msg-view__attachment-items{height:100%;overflow:auto}.mail-tag{width:8rem;padding:.5rem}.msgHeader{display:flex}.msgHeaderDetails{display:flex;flex-direction:column}table.mails th:nth-child(1){width:5%;color:#fcba03}table.mails th:nth-child(2){width:5%;color:hsl(202.5,30.7692307692%,44.9019607843%)}table.mails th:nth-child(3){width:50%;text-align:start}table.mails th:nth-child(4),table.mails th:nth-child(5){width:20%;text-align:start}table.mails td:nth-child(3){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.mails td:nth-child(4),table.mails td:nth-child(5){text-align:start}table.mails tr:hover{background-color:#eef3f6;cursor:pointer}table.mails tr.unread{color:#000;background-color:#eef3f6}table.mails>tr:hover{cursor:auto;background-color:#fff}table.mails th.sortable-th{cursor:pointer;user-select:none;transition:background-color .2s,color .2s}table.mails th.sortable-th:hover{background-color:#eef3f6;color:hsl(202.5,30.7692307692%,14.9019607843%)}input.star-check{display:none}input.star-check+label.star-check{color:gray}input.star-check:checked+label.star-check{color:#fcba03}#truncate{height:6rem;overflow:auto}#truncate.truncated-view{height:1.75rem;overflow:hidden}.toggle-truncate{font-size:.75rem;padding:0 .25rem;background:#999;color:#14141b;box-shadow:none;border-radius:2px}table.attachment-container{padding:0}table.attachment-container>tr{border:0}table.attachment-container .attachment-header{width:100%;display:flex;justify-content:space-between}table.attachment-container .attachment-header th{text-align:start}table.attachment-container .attachment-header th:nth-child(1){flex-basis:45%}table.attachment-container .attachment-header th:nth-child(2){flex-basis:15%}table.attachment-container .attachment-header th:nth-child(3){flex-basis:10%}table.attachment-container .attachment-header th:nth-child(4){flex-basis:20%}table.attachment-container .attachment-header th:nth-child(5){text-align:center;flex-basis:10%}table.attachment-container .attachment{width:100%;display:flex;justify-content:space-between;text-align:start}table.attachment-container .attachment__name{flex-basis:45%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}table.attachment-container .attachment__name span{margin-left:8px}table.attachment-container .attachment__from{flex-basis:15%}table.attachment-container .attachment__size{flex-basis:10%}table.attachment-container .attachment__date{flex-basis:20%}table.attachment-container .attachment td:nth-child(5){display:flex;justify-content:center;align-items:center;flex-basis:10%}table.attachment-container .attachment td:nth-child(5) button{font-size:.875rem}.view-toggle{height:max-content;border:1px solid #019dff;border-radius:4px;display:flex}.view-toggle *{padding:4px 12px;border-radius:4px}.composePopupOverlay{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1;background-color:rgba(0,0,0,.2)}.composePopupOverlay .composePopup{position:absolute;inset:0;margin:auto;width:80%;height:90%}.composePopupOverlay .composePopup>.widget{padding:2rem}.composePopupOverlay .composePopup .close-btn{position:absolute;top:1.5rem;right:1.5rem}.file-view{width:100%;padding:1rem;margin-top:1.5rem;border-radius:8px;border:1px solid #ccc;animation:fadein .5s}.file-view__heading{display:flex;justify-content:space-between;margin-bottom:.5rem}.file-view__heading-chunk{display:flex;gap:1rem}.file-view__body{display:flex;flex-direction:column;gap:1rem}.file-view__body-details{display:flex;align-items:center}.file-view__body-details-stat{width:100%;display:grid;grid-template-columns:repeat(5, 1fr)}.file-view__body-details-stat span>i{margin-right:.5rem}.file-view__body-details-action{display:flex;gap:1rem;height:100%}.file-view__body-details-action button,.file-view__body-details-action button.red{padding:.25rem .75rem}table.myfiles td{word-wrap:break-word}table.myfiles th:nth-child(1){width:2%}table.myfiles th:nth-child(2){width:50%}table.myfiles td:nth-child(2){text-align:start}table.friendsfiles td{word-wrap:break-word}table.friendsfiles th:nth-child(1){width:2%}table.friendsfiles th:nth-child(2){width:50%}table.friendsfiles th:nth-child(4){width:40%}table.friendsfiles td:nth-child(2){text-align:start}.file-search-container{margin-top:1rem;padding:8px;display:flex;gap:8px;border:1px solid rgba(20,20,27,.2);border-radius:6px;height:100%;overflow:auto}.file-search-container__keywords{flex-basis:15%;padding-right:.25rem;border-right:1px solid rgba(20,20,27,.1)}.file-search-container__keywords .keywords-container{display:flex;flex-direction:column;border-top:2.5px solid rgba(20,20,27,.08);margin-top:.125rem;padding-top:.25rem}.file-search-container__keywords .keywords-container a{font-size:1.2rem;text-decoration:none;color:#14141b}.file-search-container__keywords .keywords-container a.selected{color:#019dff}.file-search-container__results{flex-basis:85%;height:100%;overflow:auto}.file-search-container__results .results-container .results-header tr{display:flex}.file-search-container__results .results-container .results-header tr th{font-size:1.25rem;font-weight:bold;text-align:left}.file-search-container__results .results-container .results-header tr th:nth-child(1){flex-basis:40%}.file-search-container__results .results-container .results-header tr th:nth-child(2){flex-basis:10%;text-align:center}.file-search-container__results .results-container .results-header tr th:nth-child(3){flex-basis:40%}.file-search-container__results .results-container .results-header tr th:nth-child(4){flex-basis:10%}.file-search-container__results .results-container .results{height:100%;overflow:auto}.file-search-container__results .results-container .results tr{display:flex}.file-search-container__results .results-container .results tr .results__hash,.file-search-container__results .results-container .results tr .results__name{text-align:left;flex-basis:40%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.file-search-container__results .results-container .results tr .results__hash span,.file-search-container__results .results-container .results tr .results__name span{margin-left:8px}.file-search-container__results .results-container .results tr .results__size{flex-basis:10%}.file-search-container__results .results-container .results tr .results__download{flex-basis:10%;display:flex;justify-content:start;align-items:center}.search-form{display:flex;width:40%}.search-form input{width:100%}.search-form button{margin-left:.5rem}.shareManagerPopupOverlay{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1;background-color:rgba(0,0,0,.2)}.shareManagerPopupOverlay .shareManagerPopup{position:absolute;inset:0;margin:auto;width:80%;height:90%}.shareManagerPopupOverlay .shareManagerPopup>.widget{padding:1.5rem}.shareManagerPopupOverlay .shareManagerPopup .close-btn{position:absolute;top:1.5rem;right:1.5rem}.share-manager{display:flex;flex-direction:column;justify-content:space-between}.share-manager__table{margin:1rem 0 auto}.share-manager__table thead{font-weight:bold;text-align:left}.share-manager__table thead td:nth-child(1),.share-manager__table thead td:nth-child(2){padding-left:.5rem}.share-manager__table thead td:nth-child(3) .tooltip,.share-manager__table thead td:nth-child(4) .tooltip{font-weight:normal;font-size:1rem}.share-manager__table tbody{text-align:left}.share-manager__table tbody td:nth-child(4){font-size:1rem}.share-manager__table td input{border:0 !important}.share-manager__table td input[type=text]{width:100%}.share-manager__table td:nth-child(1){width:45%}.share-manager__table td:nth-child(2){width:20%}.share-manager__table td:nth-child(3){width:10%}.share-manager__table td:nth-child(4){width:25%}.share-manager__actions{display:flex;justify-content:space-between}.share-manager__form{display:flex;flex-direction:column;gap:.5rem}.share-manager__form_input{display:flex;flex-direction:column;gap:.5rem}.share-manager__form_input input{flex-grow:1}.share-manager .share-flags input.share-flags-check{display:none}.share-manager .share-flags input.share-flags-check+label.share-flags-label{color:gray;margin-right:.25rem;padding:.25rem .25rem .125rem;border:1px solid #6d6d6d;border-radius:.5rem}.share-manager .share-flags input.share-flags-check:checked+label.share-flags-label{color:#118fcc}.share-manager label span{display:inline-block;width:1.125rem}.manage-visibility label{width:100%;cursor:pointer}.manage-visibility{display:flex;justify-content:space-between}@media(max-width: 700px){.file-view__body-details{flex-direction:column;align-items:flex-start;gap:1rem}.file-view__body-details-stat{grid-template-columns:1fr;gap:.5rem}.file-view__body-details-stat span{display:flex;align-items:center}.share-manager__table,.share-manager__table thead,.share-manager__table tbody,.share-manager__table tr,.share-manager__table td{display:block;width:100% !important}.share-manager__table thead{display:none}.share-manager__table tr{border:1px solid #ccc;border-radius:8px;margin-bottom:1rem;padding:.5rem;background:#fff}.share-manager__table td{margin-bottom:.5rem;border:none !important;padding-left:0 !important}table.myfiles,table.myfiles tr,table.myfiles td,table.friendsfiles,table.friendsfiles tr,table.friendsfiles td{display:block;width:100% !important}table.myfiles th,table.friendsfiles th{display:none}table.myfiles tr,table.friendsfiles tr{border:1px solid #ccc;border-radius:8px;margin-bottom:1rem;padding:.5rem;background:#fff}.file-search-container{flex-direction:column}.file-search-container__keywords{flex-basis:auto;width:100%;border-right:none;border-bottom:1px solid rgba(20,20,27,.1);padding-bottom:1rem;margin-bottom:1rem}.results-container,.results-container thead,.results-container tbody,.results-container tr,.results-container td{display:block;width:100% !important}.results-container thead{display:none}.results-container tr{border-bottom:1px solid #eee;padding:1rem 0}.results-container td{margin-bottom:.5rem;word-break:break-all}}.file-section{margin-top:2rem;display:flex;flex-direction:column}.comments-section{margin-top:2rem;display:flex;justify-content:space-between}.comments-section__menu{display:flex;gap:1rem}.comments-section__menu-id{display:flex;align-items:center;gap:.25rem}#toggleunsub{position:relative;background:gray}table.channels th:nth-child(1){width:50%;text-align:start}table.channels td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.channels tr:hover{background-color:#eef3f6;cursor:pointer}table.channels tr.hidden{display:none}table{padding:.5rem}table.comments{border:1px solid #eee}table.comments th{height:40px}table.comments th:nth-child(1){width:2%}table.comments th:nth-child(2){width:40%}table.comments td{word-wrap:break-word}table.comments td:nth-child(2){text-align:start}table.files th:first-child{text-align:start;width:60%}table.files tr td:first-child{text-align:start}table.files td{word-wrap:break-word}#mtags{width:160px;text-align:center;font-size:medium;margin-left:10px;height:40px}.forums-node-panel{position:relative;bottom:200px;margin-left:200px;animation:fadein .5s}table.forums th:nth-child(1){width:50%;text-align:start}table.forums td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.forums tr:hover{background-color:#eef3f6;cursor:pointer}table.forums tr.hidden{display:none}#searchforum{position:relative;margin-left:250px}#forumdetails{position:relative;padding:10px}.p{margin:0}#toggleunsub{position:relative;background:gray}table.threads tr:hover{background-color:#eef3f6;cursor:pointer}table.threads td{word-wrap:break-word}table.threadreply th:nth-child(2){width:50%}table.threadreply th:nth-child(1){width:2%}table.threadreply td:nth-child(2){width:50%;text-align:start}table.threadreply td{word-wrap:break-word}table.threadreply tr:hover{background-color:#eef3f6;cursor:pointer}table.boards th:nth-child(1){width:50%;text-align:start}table.boards td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.boards tr:hover{background-color:#eef3f6;cursor:pointer}table.boards tr.hidden{display:none}#toggleunsub{position:relative;background:gray}#options{width:100px;text-align:center;font-size:medium;margin-left:20px;height:40px}#composepopup{height:80%;width:70%}#mtags{width:160px;text-align:center;font-size:medium;margin-left:10px;height:40px}.mail .permission-flag{margin-bottom:1rem;display:flex;gap:1rem}.mail-tags{padding:.5rem;border:1px solid rgba(20,20,27,.2);border-radius:6px}.mail-tags__container{display:flex;flex-direction:column}.mail-tags__container .tag-item{display:flex;align-items:center;gap:4px;border-bottom:1px solid rgba(20,20,27,.1);padding:2px 0}.mail-tags__container .tag-item:last-child{border:none}.mail-tags__container .tag-item__color{width:1.25rem;height:1.25rem;aspect-ratio:1}.mail-tags__container .tag-item__name{font-size:1.125rem}.mail-tags__container .tag-item__modify{margin-left:auto;font-size:.75rem;display:flex;gap:4px}.mail-tags__container .tag-item:hover{background-color:#eef3f6}.mail-tags__container .tag-item button,.mail-tags__container .tag-item button.red{padding:.25rem .6rem}.mail-tags-form .input-field{margin-bottom:.5rem}.mail-tags-form .input-field label{margin-right:.5rem}.external-address{margin:0;padding-left:1rem;height:100px;overflow:hidden auto}.external-address::-webkit-scrollbar{display:none}.proxy-server{display:flex;flex-direction:column;gap:4px}.proxy-server__tor>h4,.proxy-server__i2p>h4{margin-bottom:.25rem}.proxy-server__tor>input,.proxy-server__i2p>input{margin-right:.5rem}.proxy-server__tor .proxy-outgoing,.proxy-server__i2p .proxy-outgoing{display:inline-flex;align-items:center;gap:.5rem}.proxy-server__tor .proxy-outgoing__status,.proxy-server__i2p .proxy-outgoing__status{width:1rem;height:1rem;aspect-ratio:1;border:1px solid #000;border-radius:50%}.config-files{display:flex;flex-direction:column;gap:1rem} + */@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:auto;src:url("./webfonts/fa-solid-900.eot");src:url("./webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"),url("./webfonts/fa-solid-900.woff2") format("woff2"),url("./webfonts/fa-solid-900.woff") format("woff"),url("./webfonts/fa-solid-900.ttf") format("truetype"),url("./webfonts/fa-solid-900.svg#fontawesome") format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900}html{font-size:87.5%;box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}body,h1,h2,h3,h4,h5,h6,p,figure,blockquote,dl,dd{margin:0;padding:0}ul[role=list],ol[role=list]{list-style:none}html:focus-within{scroll-behavior:smooth}body{text-rendering:optimizeSpeed;line-height:1.5;font-family:"Roboto",Arial,Helvetica,sans-serif !important;letter-spacing:-0.025ch}a:not([class]){text-decoration-skip-ink:auto}img,picture{max-width:100%;display:block}input,button,textarea,select{font:inherit}@media(prefers-reduced-motion: reduce){html:focus-within{scroll-behavior:auto}*,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important;scroll-behavior:auto !important}}#main{height:100vh}.content{display:flex;height:100%;overflow:hidden}.tab-content{display:flex;height:100%;width:100%;background-color:#eef3f6;animation:fadein .3s;overflow:auto}input[type=text],input[type=password],input[type=number],textarea{box-sizing:border-box;background:#fff;max-width:100%;font-size:1rem;font-weight:400;border:1px solid #ccc;border-radius:.25rem;padding:.25rem .5rem;outline:rgba(0,0,0,0)}input:focus{border:1px solid #3ba4d7;box-shadow:inset 0 0 5px #ccc}input.stretched{width:90%}input.small{max-width:70%;padding:.1rem}input.searchbar{width:40%}a{cursor:pointer}a[title=Back]{width:max-content;height:max-content;padding:.475rem .75rem;border-radius:50%;transition:100ms}a[title=Back]:hover{background:#eef3f6}table{padding:20px;table-layout:fixed;width:100%;border-collapse:collapse;text-align:center;color:#333;font-size:1.125rem}table th{font-size:1.125rem;color:#000;border-bottom:2px solid #eee}table tr{border-bottom:1px solid #eee}h3{color:#444}hr{margin-left:0;color:#aaa}.grid-2col{display:grid;grid-template-columns:auto auto;gap:1rem;justify-content:start}.grid-2col input[type=checkbox]{margin-top:20px}.error{color:red}.tooltip{color:#333;position:relative;display:inline-block;margin:0 .25rem}.tooltiptext{visibility:hidden;position:absolute;top:100%;left:50%;min-width:250px;margin-left:-120px;z-index:1;color:#ccc;background-color:#333;font-size:.875rem;text-align:center;padding:.25rem;border-radius:.5rem}.tooltip:hover .tooltiptext{visibility:visible;animation:fadein .5s}blockquote{color:#14141b;padding:.75rem 1rem .75rem 2rem;border-radius:.25rem}blockquote.info{position:relative;line-height:1.2;color:rgba(20,20,27,.8);border:1px solid rgba(17,143,204,.8)}blockquote.info::before{font-family:"Font Awesome 5 Free";position:absolute;top:.5rem;left:.5rem;content:"";color:#019dff}@keyframes fadein{from{opacity:0}to{opacity:1}}.fadein{animation:fadein .5s}@keyframes swipe-from-left{from{margin-left:100%}to{margin-left:0}}button{width:max-content;height:max-content;color:#fff;background:#019dff;font-size:1rem;padding:.4rem 1rem;border:0;border-radius:5px;cursor:pointer;box-shadow:inset -3px -3px 0 rgb(0,94.5826771654,154)}button:active{outline:none;box-shadow:inset 3px 3px 0 rgb(0,94.5826771654,154)}button.red{width:max-content;height:max-content;color:#fff;background:#ff3a4a;font-size:1rem;padding:.4rem 1rem;border:0;border-radius:5px;cursor:pointer;box-shadow:inset -3px -3px 0 rgb(211,0,17.1370558376)}button.red:active{outline:none;box-shadow:inset 3px 3px 0 rgb(211,0,17.1370558376)}.media-item{display:flex;margin-top:.5rem;padding:1rem;border:1px solid rgba(20,20,27,.1);border-radius:4px}.media-item__details{flex-basis:40%;display:flex;align-items:start;gap:.5rem}.media-item__details img{width:6rem;object-fit:contain}.media-item__desc{flex-basis:60%}.active-link{background:hsla(0,0%,100%,.1) !important}.nav-menu{background-color:#14141b;box-shadow:0 5px 5px #222;display:flex;flex-direction:column;align-items:center;height:100%;padding:.5rem .25rem;margin-right:0rem}.nav-menu__logo{padding:1.2rem 0;display:flex;align-items:center;gap:.3rem}.nav-menu__logo img{width:1.6rem}.nav-menu__logo h5{line-height:1;color:#fff}.nav-menu__box{padding:2rem .125rem;display:flex;flex-direction:column;gap:.5rem;position:relative}.nav-menu__box .item{margin:0;padding:.675rem .5rem;width:10rem;display:flex;align-items:center;line-height:1;border-radius:.5rem;text-decoration:none;color:#ccc;text-transform:capitalize;transition:0ms}.nav-menu__box .item:hover{background-color:rgba(238,243,246,.15)}.nav-menu__box .item i.sidenav-icon{width:2.5rem;height:1.4rem;display:grid;place-items:center}.nav-menu__box .item.item-selected{color:#9bdaff;background-color:rgba(155,218,255,.15);font-weight:medium}.nav-menu__box button.toggle-nav{display:none;position:absolute;padding:0;top:0;right:-1rem;background:rgb(77.5,186.5157480315,255);width:1.5rem;height:1.5rem;aspect-ratio:1;justify-content:center;align-items:center;border-radius:50%;box-shadow:none}.nav-menu.collapsed .nav-menu__logo .logo-container{display:flex;flex-direction:column;align-items:center;gap:.5rem}.nav-menu.collapsed .nav-menu__logo .logo-container>*:not(img){display:block}.nav-menu.collapsed .nav-menu__logo .nav-menu__logo-text{display:none !important}.nav-menu.collapsed .nav-menu__box .item{padding:.675rem 0;width:2.5rem;justify-content:center;transition:300ms}.nav-menu.collapsed .nav-menu__box .item span,.nav-menu.collapsed .nav-menu__box .item p{display:none !important}.nav-menu.collapsed button i{rotate:180deg}.nav-menu:hover button.toggle-nav{display:flex}.sidebar{width:13rem;background-color:#fff;display:flex;flex-direction:column}.sidebar a{text-decoration:none;text-transform:capitalize;padding:1rem;cursor:pointer;color:#999}.sidebar a:hover{color:#222}.sidebar .selected-sidebar-link{font-weight:bold;color:#222;border-left:5px solid #3ba4d7;animation:expand-left-border .1s}.sidebarquickview>h6{padding:.5rem}.sidebarquickview a{text-decoration:none;text-transform:capitalize;padding:.5rem 1rem;display:block;color:#999}.sidebarquickview a a:hover{color:#222}.sidebarquickview .selected-sidebarquickview-link{font-weight:bold;color:#222;border-left:5px solid #3ba4d7;animation:expand-left-border .1s}.node-panel{width:100%;padding:.5rem;animation:fadein .5s}@keyframes expand-left-border{from{border-left:0}to{border-left:5px solid #3ba4d7}}@media(max-width: 700px){.tab-content{flex-direction:column}.sidebar{width:100% !important;flex-direction:row !important;overflow-x:auto !important;overflow-y:hidden !important;white-space:nowrap !important;border-bottom:1px solid rgba(20,20,27,.1) !important;background:#fff !important;z-index:50 !important;flex-shrink:0 !important;height:auto !important;padding:0 !important}.sidebar a{display:inline-block !important;padding:.8rem 1.2rem !important;border-bottom:3px solid rgba(0,0,0,0) !important;border-left:none !important}.sidebar .selected-sidebar-link{border-left:none !important;border-bottom:3px solid #3ba4d7 !important;animation:none !important}.sidebarquickview>h4,.sidebarquickview>h6{display:none !important}}.posts{height:100%;margin-top:1rem;flex-direction:column;overflow:auto}.posts__heading{display:flex;flex-direction:column;justify-content:space-between}.posts-container{height:100%;padding:1rem;display:grid;grid-template-columns:repeat(auto-fill, minmax(150px, 1fr));gap:2rem;border:1px solid rgba(20,20,27,.1);border-radius:4px;overflow:auto}.posts-container-card{min-height:240px;flex-direction:column;border:1px solid rgba(20,20,27,.5);border-radius:4px;cursor:pointer;text-align:center}.posts-container-card img{flex-basis:90%;object-fit:cover}.posts-container-card p{padding:0 .125rem;flex-basis:10%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.progress-bar{width:100%;height:2rem;position:relative;text-align:center;background-color:#eef3f6;border-radius:20px;overflow:hidden}.progress-bar__status{position:absolute;top:0;left:0;height:100%;color:#14141b;background-color:#019dff}.progress-bar__percent{position:absolute;inset:0;margin:auto;width:fit-content;height:fit-content}.progress-bar-chunks{position:relative;margin-top:.5rem;width:100%;height:2rem;display:flex;border-radius:.25rem;overflow:hidden;background-color:#eef3f6}.progress-bar-chunks .chunk{width:100%}.progress-bar-chunks .chunk[data-chunkVal="0"]{background-color:rgba(155,218,255,.2)}.progress-bar-chunks .chunk[data-chunkVal="1"]{background-color:#ff3a4a}.progress-bar-chunks .chunk[data-chunkVal="2"]{background-color:#019dff}.progress-bar-chunks .chunk[data-chunkVal="3"]{background-color:#fcba03}.progress-bar-chunks__percent{position:absolute;inset:0;margin:auto;width:fit-content;height:fit-content}.widget{height:100%;padding:1rem;display:flex;flex-direction:column;gap:.5rem;background-color:#fff;border-radius:.5rem;overflow:auto}.widget .top-heading{display:flex;justify-content:space-between}.widget__heading{display:flex;justify-content:space-between;align-items:center;border-bottom:2px solid #999}.widget__body{height:100%;display:flex;flex-direction:column;overflow:auto}.widget__body-heading{display:flex;justify-content:space-between;align-items:center}.widget__body-heading .action{display:flex;gap:.5rem}.widget__body-content{height:100%;overflow:auto}.widget__body-box{display:flex;flex-direction:column;gap:.5rem}.widget-half{max-width:50%}#modal-container{display:none;position:fixed;z-index:1;height:100%;top:0;left:0;width:100%;background-color:rgba(0,0,0,.2)}.modal-content{position:absolute;color:#555;width:40%;min-height:10rem;height:max-content;padding:1.5rem;inset:0;margin:auto;background-color:#fff;border-radius:.5rem;animation:fadein .5s;display:flex;flex-direction:column}.modal-content button:last-child{margin-top:auto}.modal-content .close-btn{position:absolute;right:1.5rem}.modal-content .widget{padding:0}#notification-container{position:absolute;bottom:0;right:0}.login-page{background-image:linear-gradient(-45deg, rgba(1, 157, 255, 0.75), rgba(17, 143, 204, 0.75));height:100%;animation:fadein .5s}.login-page .login-container{background-color:#fff;box-shadow:3px 3px 5px rgba(20,20,27,.4);margin:auto;position:relative;top:100px;max-width:400px;max-height:500px;border-radius:5px;display:flex;flex-direction:column;align-items:center}.login-page .login-container input{padding:.375rem .75rem;border-radius:.275rem}.login-page .login-container *{margin-bottom:1rem}.login-page .login-container>img{margin:1rem 0 2rem}.login-page .login-container extra{margin:0}.login-page .login-container>a{text-decoration:underline;cursor:pointer}.login-page .extra>label,.login-page .extra>br,.login-page .extra>input{margin-bottom:0}.homepage{margin:2rem auto 0;display:flex;flex-direction:column;gap:4rem}.homepage .logo{display:flex;justify-content:center;align-items:center}.homepage .logo img{width:90px}.homepage .logo .retroshareText{display:flex;flex-direction:column;align-items:center}.homepage .logo .retroshareText .retrotext{font-size:36px;font-weight:600;line-height:1.125}.homepage .logo .retroshareText .retrotext>span{color:#118fcc}.homepage .logo .retroshareText>b{font-size:14px;line-height:1}.homepage .certificate{display:flex;flex-direction:column;gap:4rem}.homepage .certificate__heading{text-align:center}.homepage .certificate__heading>h1{margin-bottom:1rem}.homepage .certificate__content{display:flex;flex-direction:column;gap:2rem;padding:2rem;text-align:center;border:1.5px solid rgba(17,143,204,.2);border-radius:6px;box-shadow:0px 0px 8px 2px rgba(20,20,27,.05)}.homepage .certificate__content .rsId>p{margin-bottom:.5rem;color:#118fcc}.homepage .certificate__content .retroshareID{padding:.25rem;display:flex;align-items:center;justify-self:start;font-size:1.25rem;border-radius:4px;background:rgba(20,20,27,.05)}.homepage .certificate__content .retroshareID .textArea{padding:0;width:100%;min-height:75px;font-size:1rem;font-family:monospace;background:rgba(0,0,0,0);border:none;resize:none}.homepage .certificate__content .retroshareID i{color:#118fcc}.homepage .certificate__content .retroshareID>i{margin:0 .5rem;cursor:pointer}.homepage .certificate__content .webhelp{padding:.5rem;background:#f5f5f5;display:flex;justify-content:center;align-items:center;gap:.5rem;border-radius:4px;border:1px solid rgba(20,20,27,.5);width:fit-content;cursor:pointer}.homepage .certificate__content .webhelp-container{display:grid;place-items:center}.homepage .certificate__content .webhelp:hover{background:#eef3f6;border:1px solid #14141b}.homepage .certificate__content .webhelp>i{font-size:1.2rem;color:green}.homepage .certificate__content .add-friend>h6,.homepage .certificate__content .webhelp-container>h6{font-weight:normal;margin-bottom:.5rem}.friend{color:#444;font-size:1.2em;margin:1rem .5rem;padding:1.5rem;border:1px solid #aaa;border-radius:20px}.friend i{float:left;padding:0 10px;cursor:pointer}.friend h4{margin-bottom:5px}.friend button{font-size:.9em}.friend.hidden{display:none}.friend .brief-info.online{color:green}.friend .location{margin:5px;border-top:1px solid #bbb;display:grid;grid-template-columns:auto auto;justify-content:start}.friend .brief-info{display:flex;align-items:center;justify-self:start}.friend .fa-times-circle{color:#555}.friend .fa-check-circle{color:green}.identity{color:#444;font-size:1.1em;margin:20px;padding:10px;border:1px solid #aaa;border-radius:20px}.identity>h4{margin:5px;font-size:1.3em}.identity button{font-size:.9em}.identity .details{display:grid;grid-template-columns:140px auto;grid-row-gap:5px;justify-content:left}.defaultAvatar{width:3rem;height:3rem;aspect-ratio:1;background:#b0c4de;border-radius:50%;display:grid;place-items:center}.defaultAvatar p{font-weight:900;color:#666f7f;transform:translateY(1px)}img.avatar{display:block;width:3rem;height:max-content;aspect-ratio:1;margin-right:.3em;border-radius:50%}.counter{margin-left:.5em}.counter:before{content:"("}.counter:after{content:")"}.chatInit{margin-left:.5em;color:green;cursor:pointer}.lobby{margin:10px;border:1px solid #aaa;border-radius:20px}.lobby .mainname{margin:20px;font-weight:100;font-size:1.2em}.topic{color:#666}.lobby>.topic{font-size:.95em;margin-left:25px;margin-bottom:5px}.lefttitle{margin-top:15px;margin-bottom:0;font-weight:100;font-size:1.2em}.leftname{margin-top:5px;margin-bottom:5px;padding:5px;font-weight:100;font-size:1em}.leftlobby>.topic{font-size:.75em;margin-left:15px;margin-bottom:5px}.subscribed,.public{cursor:pointer}.leftlobby{border:1px solid #aaa;border-radius:10px;margin-top:5px;background-color:#fff}.leftlobby.selected-lobby,.selectedidentity{color:#fff;background-color:#3ba4d7}.rightbar{position:absolute;width:185px;background-color:#fff;overflow:auto;top:130px;bottom:15px;right:15px}.user{padding:5px}.lobbyName{padding:15px;margin-top:2rem}.lobbies{position:absolute;width:185px;left:165px;bottom:15px;top:130px;overflow:auto}.messages,.setup{position:absolute;background-color:#fff;top:130px;left:360px;right:215px;overflow:auto}.messages{bottom:115px}.messagetext{white-space:break-spaces;margin-right:5px}.message>*{margin-left:5px}.username{color:#006400;font-weight:bolder}.chatMessage{position:absolute;background-color:#fff;height:85px;bottom:15px;right:215px;left:360px}textarea.chatMsg{height:100%;width:100%}.chatatchar{margin-left:.2em;margin-right:.2em;color:silver}.setupicon{margin-left:1em;cursor:pointer}.leaveicon{margin-left:1em;cursor:pointer;color:#d40000}.selectidentity{margin:15px;font-size:1.2em}.setup>.identity{cursor:pointer}.setup{bottom:15px}.createDistantChat{margin-top:1em}.no-lobbies .messages,.no-lobbies .chatMessage,.no-lobbies .setup{left:165px}@media(min-width: 900px){.node-panel.chat-room{display:grid !important;grid-template-columns:250px 1fr 200px !important;grid-template-rows:auto 1fr auto !important;grid-template-areas:"lobbies header rightbar" "lobbies messages rightbar" "lobbies input rightbar" !important;padding:0 !important;height:100% !important}.node-panel.chat-room .lobbyName{grid-area:header;padding:10px;border-bottom:1px solid #eee;margin:0;z-index:10;background:#fff}.node-panel.chat-room .lobbies{grid-area:lobbies;position:static !important;width:auto !important;height:auto !important;border-right:1px solid #ccc;overflow-y:auto;display:block !important;top:auto !important;bottom:auto !important;left:auto !important}.node-panel.chat-room .messages{grid-area:messages;position:static !important;width:auto !important;height:auto !important;overflow-y:auto;padding:10px;left:auto !important;right:auto !important;top:auto !important;bottom:auto !important;margin:0 !important}.node-panel.chat-room .rightbar{grid-area:rightbar;position:static !important;width:auto !important;border-left:1px solid #ccc;overflow-y:auto;display:block !important}.node-panel.chat-room .chatMessage{grid-area:input;position:static !important;width:auto !important;height:auto !important;border-top:1px solid #eee;left:auto !important;right:auto !important;bottom:auto !important;flex:0 0 auto;padding:10px !important;background:#fff;z-index:10}}@media(max-width: 899px){.node-panel.chat-room{display:flex !important;flex-direction:column !important;height:100% !important;position:relative !important}.node-panel.chat-room .lobbyName{flex:0 0 auto}.node-panel.chat-room .messages{flex:1 !important;overflow-y:auto !important;position:relative !important;top:0 !important;bottom:0 !important;left:0 !important;right:0 !important;width:100% !important;height:auto !important;margin:0 !important}.node-panel.chat-room .chatMessage{flex:0 0 auto !important;position:relative !important;bottom:0 !important;left:0 !important;right:0 !important;width:100% !important;height:auto !important;z-index:100}.node-panel.chat-room .rightbar,.node-panel.chat-room .lobbies{display:none !important;position:fixed !important;top:60px !important;bottom:0 !important;width:80% !important;background:#fff !important;z-index:200 !important;box-shadow:2px 0 10px rgba(0,0,0,.2) !important}.node-panel.chat-room.show-lobbies .lobbies{display:block !important;left:0 !important}.node-panel.chat-room.show-users .rightbar{display:block !important;right:0 !important}.chat-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.4);z-index:150}.show-lobbies .chat-overlay,.show-users .chat-overlay{display:block}.mobile-menu-icons{display:flex;gap:15px;font-size:1.2rem}.mobile-menu-icons i{cursor:pointer;padding:5px}}@media(min-width: 900px){.mobile-menu-icons{display:none}}.side-bar{display:flex;flex-direction:column;background:#fff}.side-bar .mail-compose-btn{width:96%;margin:.25rem;padding:.75rem 0}.compose-mail__from{display:flex;justify-content:space-between;padding-bottom:.5rem;border-bottom:2px solid #eef3f6}.compose-mail__recipients{padding:.5rem 0;display:flex;flex-direction:column;gap:.5rem;border-bottom:2px solid #eef3f6}.compose-mail__recipients__container{display:flex;gap:.5rem}.compose-mail__recipients__container>label{text-transform:capitalize}.compose-mail__recipients__container .recipients{width:100%;display:flex;gap:.5rem;flex-wrap:wrap}.compose-mail__recipients__container .recipients__selected{padding:.125rem .5rem;display:flex;align-items:center;gap:.5rem;border:1px solid #eef3f6;border-radius:3px;cursor:default}.compose-mail__recipients__container .recipients__selected i{cursor:pointer;padding:.25rem}.compose-mail__recipients__container .recipients__input{display:flex;position:relative;flex-grow:1}.compose-mail__recipients__container .recipients__input-field{flex-grow:1;min-width:200px;padding:0;border:none;box-shadow:none}.compose-mail__recipients__container .recipients__input-field:focus+.recipients__input-list{display:flex}.compose-mail__recipients__container .recipients__input-list{z-index:1;position:absolute;top:1rem;padding:0;width:100%;max-height:15rem;flex-direction:column;overflow:auto;display:none;background:#fff;border-top:1px solid #eef3f6;border-bottom:1px solid #eef3f6}.compose-mail__recipients__container .recipients__input-list:hover{display:flex}.compose-mail__recipients__container .recipients__input-list li{list-style:none;padding:.25rem .5rem;cursor:pointer;background:#fff;border:1px solid #eef3f6;border-top:0px}.compose-mail__recipients__container .recipients__input-list li:hover{background:#eef3f6}.compose-mail__recipients__container .recipients__input-list li:last-child{border-bottom:0px}.compose-mail__recipients .remove-recipient{padding:.125rem .5rem}.compose-mail input[type=text].compose-mail__subject{padding:.5rem 0;border:none;box-shadow:none;border-bottom:2px solid #eef3f6;border-radius:0}.compose-mail__message{margin:.5rem 0;height:100%;display:flex;flex-direction:column;overflow:auto}.compose-mail__message-body{height:100%;outline:rgba(0,0,0,0)}.compose-mail__send-btn{display:flex;align-items:center;gap:.5rem}.compose-mail__send-btn i{transform:translateY(-1px)}.msg-view{height:100%;display:flex;flex-direction:column;gap:1rem;overflow:auto}.msg-view-nav{display:flex;justify-content:space-between;align-items:column}.msg-view-nav__action{display:flex;gap:.5rem}.msg-view__header{display:flex;flex-direction:column;gap:1rem}.msg-view__header>h3{line-height:1}.msg-view__header .msg-details{display:flex;gap:1rem}.msg-view__header .msg-details__avatar{height:max-content}.msg-view__header .msg-details__info{display:flex;flex-direction:column}.msg-view__header .msg-details__info-item{display:flex;gap:.5rem}.msg-view__body{height:100%;overflow:auto;font-size:14px !important}.msg-view__attachment{height:50%;overflow:auto;display:flex;flex-direction:column}.msg-view__attachment-items{height:100%;overflow:auto}.mail-tag{width:8rem;padding:.5rem}.msgHeader{display:flex}.msgHeaderDetails{display:flex;flex-direction:column}table.mails th:nth-child(1){width:5%;color:#fcba03}table.mails th:nth-child(2){width:5%;color:hsl(202.5,30.7692307692%,44.9019607843%)}table.mails th:nth-child(3){width:50%;text-align:start}table.mails th:nth-child(4),table.mails th:nth-child(5){width:20%;text-align:start}table.mails td:nth-child(3){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.mails td:nth-child(4),table.mails td:nth-child(5){text-align:start}table.mails tr:hover{background-color:#eef3f6;cursor:pointer}table.mails tr.unread{color:#000;background-color:#eef3f6}table.mails>tr:hover{cursor:auto;background-color:#fff}input.star-check{display:none}input.star-check+label.star-check{color:gray}input.star-check:checked+label.star-check{color:#fcba03}#truncate{height:6rem;overflow:auto}#truncate.truncated-view{height:1.75rem;overflow:hidden}.toggle-truncate{font-size:.75rem;padding:0 .25rem;background:#999;color:#14141b;box-shadow:none;border-radius:2px}table.attachment-container{padding:0}table.attachment-container>tr{border:0}table.attachment-container .attachment-header{width:100%;display:flex;justify-content:space-between}table.attachment-container .attachment-header th{text-align:start}table.attachment-container .attachment-header th:nth-child(1){flex-basis:45%}table.attachment-container .attachment-header th:nth-child(2){flex-basis:15%}table.attachment-container .attachment-header th:nth-child(3){flex-basis:10%}table.attachment-container .attachment-header th:nth-child(4){flex-basis:20%}table.attachment-container .attachment-header th:nth-child(5){text-align:center;flex-basis:10%}table.attachment-container .attachment{width:100%;display:flex;justify-content:space-between;text-align:start}table.attachment-container .attachment__name{flex-basis:45%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}table.attachment-container .attachment__name span{margin-left:8px}table.attachment-container .attachment__from{flex-basis:15%}table.attachment-container .attachment__size{flex-basis:10%}table.attachment-container .attachment__date{flex-basis:20%}table.attachment-container .attachment td:nth-child(5){display:flex;justify-content:center;align-items:center;flex-basis:10%}table.attachment-container .attachment td:nth-child(5) button{font-size:.875rem}.view-toggle{height:max-content;border:1px solid #019dff;border-radius:4px;display:flex}.view-toggle *{padding:4px 12px;border-radius:4px}.composePopupOverlay{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1;background-color:rgba(0,0,0,.2)}.composePopupOverlay .composePopup{position:absolute;inset:0;margin:auto;width:80%;height:90%}.composePopupOverlay .composePopup>.widget{padding:2rem}.composePopupOverlay .composePopup .close-btn{position:absolute;top:1.5rem;right:1.5rem}.file-view{width:100%;padding:1rem;margin-top:1.5rem;border-radius:8px;border:1px solid #ccc;animation:fadein .5s}.file-view__heading{display:flex;justify-content:space-between;margin-bottom:.5rem}.file-view__heading-chunk{display:flex;gap:1rem}.file-view__body{display:flex;flex-direction:column;gap:1rem}.file-view__body-details{display:flex;align-items:center}.file-view__body-details-stat{width:100%;display:grid;grid-template-columns:repeat(5, 1fr)}.file-view__body-details-stat span>i{margin-right:.5rem}.file-view__body-details-action{display:flex;gap:1rem;height:100%}.file-view__body-details-action button,.file-view__body-details-action button.red{padding:.25rem .75rem}table.myfiles td{word-wrap:break-word}table.myfiles th:nth-child(1){width:2%}table.myfiles th:nth-child(2){width:50%}table.myfiles td:nth-child(2){text-align:start}table.friendsfiles td{word-wrap:break-word}table.friendsfiles th:nth-child(1){width:2%}table.friendsfiles th:nth-child(2){width:50%}table.friendsfiles th:nth-child(4){width:40%}table.friendsfiles td:nth-child(2){text-align:start}.file-search-container{margin-top:1rem;padding:8px;display:flex;gap:8px;border:1px solid rgba(20,20,27,.2);border-radius:6px;height:100%;overflow:auto}.file-search-container__keywords{flex-basis:15%;padding-right:.25rem;border-right:1px solid rgba(20,20,27,.1)}.file-search-container__keywords .keywords-container{display:flex;flex-direction:column;border-top:2.5px solid rgba(20,20,27,.08);margin-top:.125rem;padding-top:.25rem}.file-search-container__keywords .keywords-container a{font-size:1.2rem;text-decoration:none;color:#14141b}.file-search-container__keywords .keywords-container a.selected{color:#019dff}.file-search-container__results{flex-basis:85%;height:100%;overflow:auto}.file-search-container__results .results-container .results-header tr{display:flex}.file-search-container__results .results-container .results-header tr th{font-size:1.25rem;font-weight:bold;text-align:left}.file-search-container__results .results-container .results-header tr th:nth-child(1){flex-basis:40%}.file-search-container__results .results-container .results-header tr th:nth-child(2){flex-basis:10%;text-align:center}.file-search-container__results .results-container .results-header tr th:nth-child(3){flex-basis:40%}.file-search-container__results .results-container .results-header tr th:nth-child(4){flex-basis:10%}.file-search-container__results .results-container .results{height:100%;overflow:auto}.file-search-container__results .results-container .results tr{display:flex}.file-search-container__results .results-container .results tr .results__hash,.file-search-container__results .results-container .results tr .results__name{text-align:left;flex-basis:40%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.file-search-container__results .results-container .results tr .results__hash span,.file-search-container__results .results-container .results tr .results__name span{margin-left:8px}.file-search-container__results .results-container .results tr .results__size{flex-basis:10%}.file-search-container__results .results-container .results tr .results__download{flex-basis:10%;display:flex;justify-content:start;align-items:center}.search-form{display:flex;width:40%}.search-form input{width:100%}.search-form button{margin-left:.5rem}.shareManagerPopupOverlay{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1;background-color:rgba(0,0,0,.2)}.shareManagerPopupOverlay .shareManagerPopup{position:absolute;inset:0;margin:auto;width:80%;height:90%}.shareManagerPopupOverlay .shareManagerPopup>.widget{padding:1.5rem}.shareManagerPopupOverlay .shareManagerPopup .close-btn{position:absolute;top:1.5rem;right:1.5rem}.share-manager{display:flex;flex-direction:column;justify-content:space-between}.share-manager__table{margin:1rem 0 auto}.share-manager__table thead{font-weight:bold;text-align:left}.share-manager__table thead td:nth-child(1),.share-manager__table thead td:nth-child(2){padding-left:.5rem}.share-manager__table thead td:nth-child(3) .tooltip,.share-manager__table thead td:nth-child(4) .tooltip{font-weight:normal;font-size:1rem}.share-manager__table tbody{text-align:left}.share-manager__table tbody td:nth-child(4){font-size:1rem}.share-manager__table td input{border:0 !important}.share-manager__table td input[type=text]{width:100%}.share-manager__table td:nth-child(1){width:45%}.share-manager__table td:nth-child(2){width:20%}.share-manager__table td:nth-child(3){width:10%}.share-manager__table td:nth-child(4){width:25%}.share-manager__actions{display:flex;justify-content:space-between}.share-manager__form{display:flex;flex-direction:column;gap:.5rem}.share-manager__form_input{display:flex;flex-direction:column;gap:.5rem}.share-manager__form_input input{flex-grow:1}.share-manager .share-flags input.share-flags-check{display:none}.share-manager .share-flags input.share-flags-check+label.share-flags-label{color:gray;margin-right:.25rem;padding:.25rem .25rem .125rem;border:1px solid #6d6d6d;border-radius:.5rem}.share-manager .share-flags input.share-flags-check:checked+label.share-flags-label{color:#118fcc}.share-manager label span{display:inline-block;width:1.125rem}.manage-visibility label{width:100%;cursor:pointer}.manage-visibility{display:flex;justify-content:space-between}@media(max-width: 700px){.file-view__body-details{flex-direction:column;align-items:flex-start;gap:1rem}.file-view__body-details-stat{grid-template-columns:1fr;gap:.5rem}.file-view__body-details-stat span{display:flex;align-items:center}.share-manager__table,.share-manager__table thead,.share-manager__table tbody,.share-manager__table tr,.share-manager__table td{display:block;width:100% !important}.share-manager__table thead{display:none}.share-manager__table tr{border:1px solid #ccc;border-radius:8px;margin-bottom:1rem;padding:.5rem;background:#fff}.share-manager__table td{margin-bottom:.5rem;border:none !important;padding-left:0 !important}table.myfiles,table.myfiles tr,table.myfiles td,table.friendsfiles,table.friendsfiles tr,table.friendsfiles td{display:block;width:100% !important}table.myfiles th,table.friendsfiles th{display:none}table.myfiles tr,table.friendsfiles tr{border:1px solid #ccc;border-radius:8px;margin-bottom:1rem;padding:.5rem;background:#fff}.file-search-container{flex-direction:column}.file-search-container__keywords{flex-basis:auto;width:100%;border-right:none;border-bottom:1px solid rgba(20,20,27,.1);padding-bottom:1rem;margin-bottom:1rem}.results-container,.results-container thead,.results-container tbody,.results-container tr,.results-container td{display:block;width:100% !important}.results-container thead{display:none}.results-container tr{border-bottom:1px solid #eee;padding:1rem 0}.results-container td{margin-bottom:.5rem;word-break:break-all}}.file-section{margin-top:2rem;display:flex;flex-direction:column}.comments-section{margin-top:2rem;display:flex;justify-content:space-between}.comments-section__menu{display:flex;gap:1rem}.comments-section__menu-id{display:flex;align-items:center;gap:.25rem}#toggleunsub{position:relative;background:gray}table.channels th:nth-child(1){width:50%;text-align:start}table.channels td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.channels tr:hover{background-color:#eef3f6;cursor:pointer}table.channels tr.hidden{display:none}table{padding:.5rem}table.comments{border:1px solid #eee}table.comments th{height:40px}table.comments th:nth-child(1){width:2%}table.comments th:nth-child(2){width:40%}table.comments td{word-wrap:break-word}table.comments td:nth-child(2){text-align:start}table.files th:first-child{text-align:start;width:60%}table.files tr td:first-child{text-align:start}table.files td{word-wrap:break-word}#mtags{width:160px;text-align:center;font-size:medium;margin-left:10px;height:40px}.forums-node-panel{position:relative;bottom:200px;margin-left:200px;animation:fadein .5s}table.forums th:nth-child(1){width:50%;text-align:start}table.forums td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.forums tr:hover{background-color:#eef3f6;cursor:pointer}table.forums tr.hidden{display:none}#searchforum{position:relative;margin-left:250px}#forumdetails{position:relative;padding:10px}.p{margin:0}#toggleunsub{position:relative;background:gray}table.threads tr:hover{background-color:#eef3f6;cursor:pointer}table.threads td{word-wrap:break-word}table.threadreply th:nth-child(2){width:50%}table.threadreply th:nth-child(1){width:2%}table.threadreply td:nth-child(2){width:50%;text-align:start}table.threadreply td{word-wrap:break-word}table.threadreply tr:hover{background-color:#eef3f6;cursor:pointer}table.boards th:nth-child(1){width:50%;text-align:start}table.boards td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.boards tr:hover{background-color:#eef3f6;cursor:pointer}table.boards tr.hidden{display:none}#toggleunsub{position:relative;background:gray}#options{width:100px;text-align:center;font-size:medium;margin-left:20px;height:40px}#composepopup{height:80%;width:70%}#mtags{width:160px;text-align:center;font-size:medium;margin-left:10px;height:40px}.mail .permission-flag{margin-bottom:1rem;display:flex;gap:1rem}.mail-tags{padding:.5rem;border:1px solid rgba(20,20,27,.2);border-radius:6px}.mail-tags__container{display:flex;flex-direction:column}.mail-tags__container .tag-item{display:flex;align-items:center;gap:4px;border-bottom:1px solid rgba(20,20,27,.1);padding:2px 0}.mail-tags__container .tag-item:last-child{border:none}.mail-tags__container .tag-item__color{width:1.25rem;height:1.25rem;aspect-ratio:1}.mail-tags__container .tag-item__name{font-size:1.125rem}.mail-tags__container .tag-item__modify{margin-left:auto;font-size:.75rem;display:flex;gap:4px}.mail-tags__container .tag-item:hover{background-color:#eef3f6}.mail-tags__container .tag-item button,.mail-tags__container .tag-item button.red{padding:.25rem .6rem}.mail-tags-form .input-field{margin-bottom:.5rem}.mail-tags-form .input-field label{margin-right:.5rem}.external-address{margin:0;padding-left:1rem;height:100px;overflow:hidden auto}.external-address::-webkit-scrollbar{display:none}.proxy-server{display:flex;flex-direction:column;gap:4px}.proxy-server__tor>h4,.proxy-server__i2p>h4{margin-bottom:.25rem}.proxy-server__tor>input,.proxy-server__i2p>input{margin-right:.5rem}.proxy-server__tor .proxy-outgoing,.proxy-server__i2p .proxy-outgoing{display:inline-flex;align-items:center;gap:.5rem}.proxy-server__tor .proxy-outgoing__status,.proxy-server__i2p .proxy-outgoing__status{width:1rem;height:1rem;aspect-ratio:1;border:1px solid #000;border-radius:50%}.config-files{display:flex;flex-direction:column;gap:1rem} + +/* Custom improvements for Network Page */ + +.network-container { + display: flex; + height: 100%; + width: 100%; + overflow: hidden; + background-color: #f1f5f9; +} + +.network-left-pane { + width: 320px; + min-width: 300px; + max-width: 350px; + border-right: 1px solid #cbd5e1; + display: flex; + flex-direction: column; + background: #ffffff; + box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05); +} + +.own-profile-card { + padding: 1.25rem; + border-bottom: 1px solid #e2e8f0; + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.own-profile-card .profile-header { + display: flex; + align-items: center; + gap: 1rem; +} + +.own-profile-card .profile-info { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; +} + +.own-profile-card .profile-info .profile-name { + font-weight: 700; + color: #1e293b; + font-size: 1.1rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.own-profile-card .profile-info .profile-status { + font-size: 0.85rem; + color: #10b981; + font-weight: 500; + display: flex; + align-items: center; + gap: 0.35rem; +} + +.own-profile-card .profile-info .profile-status::before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + background-color: #10b981; + border-radius: 50%; +} + +.own-profile-card .own-identity-select-container { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.own-profile-card .own-identity-select-container label { + font-size: 0.75rem; + color: #64748b; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.own-profile-card .own-identity-select-container select.own-identity-select { + width: 100%; + padding: 0.375rem 0.5rem; + font-size: 0.85rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + background-color: #ffffff; + color: #334155; + outline: none; + cursor: pointer; + transition: border-color 0.2s; +} + +.own-profile-card .own-identity-select-container select.own-identity-select:focus { + border-color: #3ba4d7; +} + +.friends-list-container { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + position: relative; +} + +.friends-list-container .people-context-menu { + position: absolute; + left: 2rem; + width: 210px; + background-color: #ffffff; + border: 1px solid #e2e8f0; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); + border-radius: 0.375rem; + z-index: 1010; + padding: 0.25rem 0; + display: flex; + flex-direction: column; +} + +.friends-list-container .people-context-menu .menu-item { + padding: 0.5rem 1rem; + font-size: 0.85rem; + color: #334155; + cursor: pointer; + display: flex; + align-items: center; + transition: background-color 0.2s; +} + +.friends-list-container .people-context-menu .menu-item:hover { + background-color: #f1f5f9; + color: #0f172a; +} + +.friends-list-container .searchbar-container { + padding: 0.75rem 1rem; + border-bottom: 1px solid #e2e8f0; +} + +.friends-list-container .searchbar-container input.searchbar { + width: 100%; + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + background-color: #f8fafc; + outline: none; + transition: all 0.2s; +} + +.friends-list-container .searchbar-container input.searchbar:focus { + background-color: #ffffff; + border-color: #3ba4d7; + box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); +} + +.friends-list-container .friends-scroll { + flex: 1; + overflow-y: auto; + padding: 0.5rem 0; +} + +.friend-list-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + margin: 0.125rem 0.5rem; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s; +} + +.friend-list-item:hover { + background-color: #f1f5f9; +} + +.friend-list-item.selected { + background-color: #e0f2fe; +} + +.friend-list-item.selected .friend-meta .friend-name { + color: #0369a1; + font-weight: 600; +} + +.friend-list-item .friend-avatar { + flex-shrink: 0; +} + +.friend-list-item .friend-meta { + flex: 1; + min-width: 0; +} + +.friend-list-item .friend-meta .friend-name { + font-size: 0.95rem; + color: #334155; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: color 0.2s; +} + +.friend-list-item .friend-meta .friend-status { + font-size: 0.8rem; + color: #94a3b8; +} + +.friend-list-item .friend-meta .friend-status.online { + color: #10b981; + font-weight: 500; +} + +.network-right-pane { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: #f8fafc; +} + +.network-pane-placeholder { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: #94a3b8; + gap: 1rem; + padding: 2rem; + text-align: center; +} + +.network-pane-placeholder i { + font-size: 4rem; + color: #cbd5e1; +} + +.network-pane-placeholder p { + font-size: 1.1rem; + max-width: 400px; +} + +.network-tabs { + display: flex; + background-color: #ffffff; + border-bottom: 1px solid #cbd5e1; + padding: 0.5rem 1rem 0; + gap: 0.5rem; +} + +.network-tabs .tab-btn { + padding: 0.625rem 1.25rem; + font-size: 0.95rem; + font-weight: 600; + color: #64748b; + background: transparent; + border: none; + border-radius: 0.375rem 0.375rem 0 0; + border-bottom: 3px solid transparent; + cursor: pointer; + box-shadow: none; + transition: all 0.2s; +} + +.network-tabs .tab-btn:hover { + color: #334155; + background-color: #f1f5f9; +} + +.network-tabs .tab-btn.active { + color: #3ba4d7; + border-bottom-color: #3ba4d7; + background-color: transparent; +} + +.network-tab-content { + flex: 1; + overflow-y: auto; + padding: 1.5rem; +} + +.network-detail-view { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.network-detail-view .detail-header { + display: flex; + align-items: center; + gap: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid #e2e8f0; +} + +.network-detail-view .detail-header .detail-title { + flex: 1; +} + +.network-detail-view .detail-header .detail-title h2 { + font-size: 1.75rem; + font-weight: 800; + color: #1e293b; + margin-bottom: 0.25rem; +} + +.network-detail-view .detail-header .detail-title .detail-subtitle { + font-size: 0.9rem; + color: #64748b; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.network-detail-view .detail-header .detail-actions { + display: flex; + gap: 0.75rem; +} + +.network-detail-view .detail-header .detail-actions button { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + font-size: 0.9rem; +} + +.network-detail-view .detail-section { + background-color: #ffffff; + border-radius: 0.5rem; + border: 1px solid #e2e8f0; + padding: 1.25rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); +} + +.network-detail-view .detail-section h3 { + font-size: 1.1rem; + font-weight: 700; + color: #334155; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid #f1f5f9; +} + +.network-detail-view .detail-section .info-grid { + display: grid; + grid-template-columns: 120px 1fr; + row-gap: 0.75rem; + font-size: 0.9rem; +} + +.network-detail-view .detail-section .info-grid .info-label { + font-weight: 600; + color: #64748b; +} + +.network-detail-view .detail-section .info-grid .info-value { + color: #1e293b; + word-break: break-all; +} + +.network-detail-view .locations-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; +} + +.location-card { + background-color: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 0.5rem; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); +} + +.location-card .loc-header { + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #f1f5f9; + padding-bottom: 0.5rem; + margin-bottom: 0.25rem; +} + +.location-card .loc-header .loc-name { + font-weight: 700; + color: #334155; + font-size: 0.95rem; +} + +.location-card .loc-header .loc-status { + font-size: 0.75rem; + font-weight: 600; + padding: 0.125rem 0.5rem; + border-radius: 0.25rem; +} + +.location-card .loc-header .loc-status.online { + background-color: #d1fae5; + color: #065f46; +} + +.location-card .loc-header .loc-status.offline { + background-color: #f1f5f9; + color: #475569; +} + +.location-card .loc-body { + font-size: 0.85rem; + display: grid; + grid-template-columns: 80px 1fr; + row-gap: 0.25rem; +} + +.location-card .loc-body .loc-label { + color: #64748b; +} + +.location-card .loc-body .loc-val { + color: #334155; + word-break: break-all; +} + +.location-card .loc-footer { + margin-top: 0.5rem; + display: flex; + justify-content: flex-end; +} + +.location-card .loc-footer button { + font-size: 0.8rem; + padding: 0.25rem 0.75rem; +} + +.network-chat-view { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + background-color: #f8fafc; +} + +.network-chat-view .chat-messages { + flex: 1; + overflow-y: auto; + padding: 1.25rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.chat-bubble-container { + display: flex; + flex-direction: column; + max-width: 70%; +} + +.chat-bubble-container.outgoing { + align-self: flex-end; + align-items: flex-end; +} + +.chat-bubble-container.outgoing .chat-bubble { + background-color: #3ba4d7; + color: #ffffff; + border-bottom-right-radius: 0.125rem; +} + +.chat-bubble-container.incoming { + align-self: flex-start; + align-items: flex-start; +} + +.chat-bubble-container.incoming .chat-bubble { + background-color: #ffffff; + color: #1e293b; + border: 1px solid #e2e8f0; + border-bottom-left-radius: 0.125rem; +} + +.chat-bubble-container .chat-sender { + font-size: 0.75rem; + color: #64748b; + margin-bottom: 0.25rem; + padding: 0 0.25rem; +} + +.chat-bubble-container .chat-bubble { + padding: 0.625rem 0.875rem; + border-radius: 0.75rem; + font-size: 0.925rem; + line-height: 1.4; + white-space: break-spaces; + word-break: break-word; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.chat-bubble-container .chat-time { + font-size: 0.7rem; + color: #94a3b8; + margin-top: 0.25rem; + padding: 0 0.25rem; +} + +.network-chat-view .chat-input-area { + padding: 1rem; + background-color: #ffffff; + border-top: 1px solid #cbd5e1; + display: flex; + gap: 0.75rem; + align-items: center; +} + +.network-chat-view .chat-input-area textarea.chat-textarea { + flex: 1; + resize: none; + height: 40px; + padding: 0.5rem 0.75rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + font-size: 0.9rem; + outline: none; + transition: all 0.2s; +} + +.network-chat-view .chat-input-area textarea.chat-textarea:focus { + border-color: #3ba4d7; + box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); +} + +.network-chat-view .chat-input-area button.send-btn { + padding: 0.5rem 1.25rem; + font-size: 0.9rem; + height: 40px; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.network-chat-view .chat-warning { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: #64748b; + text-align: center; + padding: 2rem; + gap: 1rem; +} + +.network-chat-view .chat-warning i { + font-size: 3rem; + color: #cbd5e1; +} + +.network-chat-view .chat-warning h4 { + font-weight: 700; + color: #334155; +} + +.network-chat-view .chat-warning p { + max-width: 350px; + font-size: 0.9rem; +} + +/* People Page Modern Split-Pane Layout */ +.people-container { + display: flex; + height: calc(100vh - 55px); + width: 100%; + overflow: hidden; +} + +.people-left-pane { + width: 320px; + border-right: 1px solid #cbd5e1; + display: flex; + flex-direction: column; + background-color: #ffffff; + overflow: hidden; +} + +.people-right-pane { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: #f8fafc; +} + +.people-filter-group { + display: flex; + padding: 0.75rem 1rem 0.25rem 1rem; + gap: 0.25rem; + border-bottom: 1px solid #e2e8f0; +} + +.people-filter-group button.filter-btn { + flex: 1; + padding: 0.375rem 0.5rem; + font-size: 0.85rem; + font-weight: 600; + color: #64748b; + background-color: #f1f5f9; + border: none; + border-radius: 0.375rem; + cursor: pointer; + box-shadow: none; + transition: all 0.2s; +} + +.people-filter-group button.filter-btn:hover { + background-color: #e2e8f0; + color: #334155; +} + +.people-filter-group button.filter-btn.active { + background-color: #3ba4d7; + color: #ffffff; +} + +.people-left-pane .create-id-container { + padding: 0.75rem 1rem; + border-bottom: 1px solid #e2e8f0; + display: flex; +} + +.people-left-pane .create-id-container button.create-id-btn { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.5rem; + font-weight: 600; + font-size: 0.9rem; +} + +/* ===================================================== + CHAT HUB - Two-Pane Layout + ===================================================== */ + +.chat-hub-container { + display: flex; + height: 100%; + width: 100%; + overflow: hidden; + background-color: #f1f5f9; +} + +.chat-hub-left-pane { + width: 320px; + min-width: 300px; + max-width: 350px; + border-right: 1px solid #cbd5e1; + display: flex; + flex-direction: column; + background: #ffffff; + box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05); +} + +.chat-own-profile-card { + padding: 1.25rem; + border-bottom: 1px solid #e2e8f0; + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); + position: relative; +} + +.chat-create-lobby-btn { + position: absolute; + bottom: 0.5rem; + right: 1.25rem; + background-color: #0084ff; + color: #ffffff; + border: none; + border-radius: 0.375rem; + padding: 0.35rem 0.75rem; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + box-shadow: 0 4px 6px -1px rgba(0, 132, 255, 0.2), 0 2px 4px -1px rgba(0, 132, 255, 0.1); + transition: background-color 0.2s, transform 0.2s; + display: flex; + align-items: center; + gap: 0.25rem; +} + +.chat-create-lobby-btn:hover { + background-color: #0073e6; + transform: translateY(-1px); +} + +.chat-create-lobby-btn:active { + transform: translateY(0); +} + +.chat-own-profile-card .profile-header { + display: flex; + align-items: center; + gap: 1rem; +} + +.chat-own-profile-card .profile-info { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; +} + +.chat-own-profile-card .profile-info .profile-name { + font-weight: 700; + color: #1e293b; + font-size: 1.1rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chat-own-profile-card .profile-info .profile-status { + font-size: 0.85rem; + color: #10b981; + font-weight: 500; + display: flex; + align-items: center; + gap: 0.35rem; +} + +.chat-own-profile-card .profile-info .profile-status::before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + background-color: #10b981; + border-radius: 50%; +} + +.chat-rooms-list-container { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.chat-rooms-list-container .searchbar-container { + padding: 0.75rem 1rem; + border-bottom: 1px solid #e2e8f0; +} + +.chat-rooms-list-container .searchbar-container input.searchbar { + width: 100%; + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + background-color: #f8fafc; + outline: none; + transition: all 0.2s; +} + +.chat-rooms-list-container .searchbar-container input.searchbar:focus { + background-color: #ffffff; + border-color: #3ba4d7; + box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); +} + +.chat-rooms-list-container .rooms-scroll { + flex: 1; + overflow-y: auto; + padding: 0.5rem 0; +} + +.rooms-section-title { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem 0.375rem; + font-size: 0.75rem; + font-weight: 700; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.rooms-section-title i { + font-size: 0.7rem; + color: #94a3b8; +} + +.chat-room-list-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + margin: 0.125rem 0.5rem; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s; +} + +.chat-room-list-item:hover { + background-color: #f1f5f9; +} + +.chat-room-list-item.selected { + background-color: #e0f2fe; +} + +.chat-room-list-item.selected .room-meta .room-name { + color: #0369a1; + font-weight: 600; +} + +.chat-room-list-item .room-icon { + flex-shrink: 0; + width: 36px; + height: 36px; + border-radius: 0.5rem; + background: linear-gradient(135deg, #3ba4d7, #0ea5e9); + display: flex; + align-items: center; + justify-content: center; + color: #ffffff; + font-size: 0.85rem; +} + +.chat-room-list-item.public-room .room-icon { + background: linear-gradient(135deg, #10b981, #059669); +} + +.chat-room-list-item .room-meta { + flex: 1; + min-width: 0; +} + +.chat-room-list-item .room-meta .room-name { + font-size: 0.95rem; + color: #334155; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: color 0.2s; +} + +.chat-room-list-item .room-meta .room-topic { + font-size: 0.8rem; + color: #94a3b8; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chat-room-list-item .room-badge { + flex-shrink: 0; + min-width: 24px; + height: 24px; + border-radius: 12px; + background-color: #e2e8f0; + color: #475569; + font-size: 0.75rem; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + padding: 0 0.375rem; +} + +.chat-hub-right-pane { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: #f8fafc; +} + +.chat-pane-placeholder { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: #94a3b8; + gap: 1rem; + padding: 2rem; + text-align: center; +} + +.chat-pane-placeholder i { + font-size: 4rem; + color: #cbd5e1; +} + +.chat-pane-placeholder p { + font-size: 1.1rem; + max-width: 400px; +} + +.chat-hub-tab-content { + flex: 1; + overflow-y: auto; + padding: 1.5rem; +} + +.chat-room-detail-view { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.chat-room-detail-view .detail-header { + display: flex; + align-items: flex-start; + gap: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid #e2e8f0; + flex-wrap: wrap; +} + +.chat-room-detail-view .detail-header .detail-title { + flex: 1; + min-width: 200px; +} + +.chat-room-detail-view .detail-header .detail-title h2 { + font-size: 1.75rem; + font-weight: 800; + color: #1e293b; + margin-bottom: 0.25rem; +} + +.chat-room-detail-view .detail-header .detail-title .detail-subtitle { + font-size: 0.9rem; + color: #64748b; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.chat-room-detail-view .detail-header .detail-actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +.chat-room-detail-view .detail-header .detail-actions button { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + font-size: 0.9rem; +} + +.chat-room-detail-view .detail-section { + background-color: #ffffff; + border-radius: 0.5rem; + border: 1px solid #e2e8f0; + padding: 1.25rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); +} + +.chat-room-detail-view .detail-section h3 { + font-size: 1.1rem; + font-weight: 700; + color: #334155; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid #f1f5f9; +} + +.chat-room-detail-view .detail-section .info-grid { + display: grid; + grid-template-columns: 130px 1fr; + row-gap: 0.75rem; + font-size: 0.9rem; +} + +.chat-room-detail-view .detail-section .info-grid .info-label { + font-weight: 600; + color: #64748b; +} + +.chat-room-detail-view .detail-section .info-grid .info-value { + color: #1e293b; + word-break: break-all; +} + +.participants-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 0.5rem; +} + +.participant-card { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + background-color: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 0.375rem; +} + +.participant-card .participant-name { + font-size: 0.875rem; + color: #334155; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.no-participants { + color: #94a3b8; + font-size: 0.9rem; + font-style: italic; +} + +.detail-actions-footer { + display: flex; + gap: 0.75rem; +} + +.detail-actions-footer button { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + font-size: 0.9rem; +} + +.join-description { + color: #64748b; + font-size: 0.9rem; + margin-bottom: 1rem; +} + +.identities-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 0.75rem; +} + +.identity-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + background-color: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s; +} + +.identity-card:hover { + background-color: #e0f2fe; + border-color: #3ba4d7; +} + +.identity-card .identity-name { + font-size: 0.95rem; + font-weight: 600; + color: #334155; +} + +.identity-card i { + color: #3ba4d7; + font-size: 0.9rem; +} + +.no-rooms { + padding: 1rem; + color: #94a3b8; + text-align: center; + font-style: italic; +} + +/* Chat Hub Responsive - Mobile */ +@media (max-width: 899px) { + .chat-hub-container { + flex-direction: column; + } + + .chat-hub-left-pane { + width: 100%; + min-width: 0; + max-width: none; + max-height: 45%; + border-right: none; + border-bottom: 1px solid #cbd5e1; + } + + .chat-hub-right-pane { + flex: 1; + min-height: 0; + } +} + +/* ===================================================== + CHAT HUB - Right Pane Conversation & Tabs Styling + ===================================================== */ + +.chat-hub-header-bar { + padding: 0.75rem 1.5rem; + background-color: #ffffff; + border-bottom: 1px solid #e2e8f0; + display: flex; + align-items: center; + justify-content: space-between; + height: 65px; + flex-shrink: 0; +} + +.chat-hub-header-bar .chat-header-info { + display: flex; + flex-direction: column; + overflow: hidden; +} + +.chat-hub-header-bar .chat-header-info .chat-header-name { + font-size: 1.15rem; + font-weight: 800; + color: #1e293b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chat-hub-header-bar .chat-header-info .chat-header-topic { + font-size: 0.85rem; + color: #64748b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 0.125rem; +} + +.chat-hub-header-bar .chat-header-actions { + display: flex; + gap: 0.5rem; +} + +.chat-hub-header-bar .chat-header-actions button { + display: flex; + align-items: center; + gap: 0.35rem; + padding: 0.375rem 0.75rem; + font-size: 0.85rem; +} + +.chat-hub-tabs-container { + background-color: #ffffff; + border-bottom: 1px solid #cbd5e1; + padding: 0.5rem 1.5rem 0; +} + +.chat-hub-tabs { + display: flex; + gap: 0.5rem; +} + +.chat-hub-tabs .tab-btn { + padding: 0.625rem 1.25rem; + font-size: 0.95rem; + font-weight: 600; + color: #64748b; + background: transparent; + border: none; + border-radius: 0.375rem 0.375rem 0 0; + border-bottom: 3px solid transparent; + cursor: pointer; + box-shadow: none; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.chat-hub-tabs .tab-btn:hover { + color: #334155; + background-color: #f1f5f9; +} + +.chat-hub-tabs .tab-btn.active { + color: #3ba4d7; + border-bottom-color: #3ba4d7; + background-color: transparent; +} + +.chat-hub-tab-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: #f8fafc; +} + +.chat-hub-conversation-layout { + display: flex; + flex-direction: row; + height: 100%; + width: 100%; + overflow: hidden; +} + +.chat-hub-conversation-main { + display: flex; + flex-direction: column; + flex: 1; + height: 100%; + overflow: hidden; +} + +.chat-hub-rightbar { + width: 200px; + border-left: 1px solid #cbd5e1; + background-color: #ffffff; + display: flex; + flex-direction: column; + flex-shrink: 0; + position: relative; +} + +.chat-hub-rightbar .rightbar-title { + padding: 0.75rem 1rem; + font-size: 0.85rem; + font-weight: 700; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid #e2e8f0; +} + +.chat-hub-rightbar .rightbar-users-list { + flex: 1; + overflow-y: auto; + padding: 0.5rem; +} + +.chat-hub-rightbar .user { + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + color: #334155; + border-radius: 0.375rem; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 0.5rem; + position: relative; +} + +.chat-hub-rightbar .user .user-name { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; +} + +.chat-hub-rightbar .user:hover { + background-color: #f1f5f9; + color: #0f172a; +} + +.chat-hub-rightbar .user-tooltip { + position: absolute; + left: -275px; + transform: translateY(-50%); + width: 260px; + background-color: #ffffe1; + border: 1px solid #7f7f7f; + box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.25); + padding: 0.5rem; + border-radius: 0.25rem; + z-index: 1000; + white-space: normal; + display: flex; + gap: 0.5rem; + align-items: flex-start; +} + +.chat-hub-rightbar .user-tooltip .tooltip-avatar { + flex-shrink: 0; +} + +.chat-hub-rightbar .user-tooltip .tooltip-details { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 0.8rem; + color: #000000; + text-align: left; +} + +.chat-hub-rightbar .user-tooltip .tooltip-row { + line-height: 1.2; +} + +.chat-hub-rightbar .user-tooltip .tooltip-label { + font-weight: bold; +} + +.chat-hub-rightbar .user-tooltip .tooltip-value { + font-weight: normal; + word-break: break-all; +} + +.chat-hub-rightbar .user-tooltip .tooltip-value.tooltip-id { + font-family: monospace; +} + +.chat-hub-rightbar .rightbar-context-menu { + position: absolute; + right: 1rem; + width: 210px; + background-color: #ffffff; + border: 1px solid #e2e8f0; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); + border-radius: 0.375rem; + z-index: 1010; + padding: 0.25rem 0; + display: flex; + flex-direction: column; +} + +.chat-hub-rightbar .rightbar-context-menu .menu-item { + padding: 0.5rem 1rem; + font-size: 0.85rem; + color: #334155; + cursor: pointer; + display: flex; + align-items: center; + transition: background-color 0.2s; +} + +.chat-hub-rightbar .rightbar-context-menu .menu-item:hover { + background-color: #f1f5f9; + color: #0f172a; +} + +.chat-hub-rightbar .user .defaultAvatar { + width: 2rem; + height: 2rem; + font-size: 0.9rem; + flex-shrink: 0; +} + +.chat-hub-rightbar .user img.avatar { + width: 2rem; + height: 2rem; + flex-shrink: 0; +} + +@media (max-width: 899px) { + .chat-hub-rightbar { + display: none; + } +} + +.chat-hub-messages { + flex: 1; + overflow-y: auto; + padding: 1.25rem 1.5rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +/* Chat bubble overrides for two-pane layout */ +.chat-hub-messages .message { + display: flex; + flex-direction: column; + max-width: 70%; + padding: 0.625rem 0.875rem; + border-radius: 0.75rem; + font-size: 1rem; + line-height: 1.4; + word-break: break-word; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.chat-hub-messages .message.incoming { + align-self: flex-start; + align-items: flex-start; + background-color: #ffffff; + color: #1e293b; + border: 1px solid #e2e8f0; + border-bottom-left-radius: 0.125rem; +} + +.chat-hub-messages .message.outgoing { + align-self: flex-end; + align-items: flex-end; + background-color: #3ba4d7; + color: #ffffff; + border-bottom-right-radius: 0.125rem; +} + +.chat-hub-messages .message .username { + font-size: 0.75rem; + margin-bottom: 0.25rem; + padding: 0 0.125rem; + font-weight: 700; +} + +.chat-hub-messages .message.incoming .username { + color: #0369a1; +} + +.chat-hub-messages .message.outgoing .username { + color: #e0f2fe; +} + +.chat-hub-messages .message .messagetext { + white-space: break-spaces; + margin: 0; +} + +.chat-hub-messages .message .datetime { + font-size: 0.7rem; + margin-top: 0.25rem; + padding: 0 0.125rem; + opacity: 0.8; +} + +.chat-hub-messages .message.incoming .datetime { + color: #64748b; +} + +.chat-hub-messages .message.outgoing .datetime { + color: #f1f5f9; +} + +.chat-hub-input-area { + padding: 1rem 1.5rem; + background-color: #ffffff; + border-top: 1px solid #cbd5e1; + display: flex; + gap: 0.75rem; + align-items: center; + flex-shrink: 0; +} + +.chat-hub-input-area textarea.chat-hub-textarea { + flex: 1; + resize: none; + height: 40px; + padding: 0.5rem 0.75rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + font-size: 0.9rem; + outline: none; + transition: all 0.2s; + background-color: #f8fafc; +} + +.chat-hub-input-area textarea.chat-hub-textarea:focus { + background-color: #ffffff; + border-color: #3ba4d7; + box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); +} + +.chat-hub-input-area button.chat-hub-send-btn { + padding: 0.5rem 1.25rem; + font-size: 0.9rem; + height: 40px; + display: flex; + align-items: center; + gap: 0.5rem; + border-radius: 0.375rem; +} + +/* Compact Room Chat Style (No bubbles, IRC-style single line per message) */ +.chat-hub-messages.compact-container, +.messages.compact-container { + gap: 0 !important; + padding: 0.75rem 1rem !important; + background-color: #ffffff !important; + display: flex !important; + flex-direction: column !important; + flex: 1 !important; + overflow-y: auto !important; + min-height: 0 !important; +} + +.chat-hub-messages.compact-container .message.compact, +.messages.compact-container .message.compact { + display: block !important; + max-width: 100% !important; + padding: 0.15rem 0 !important; + border-radius: 0 !important; + background-color: transparent !important; + border: none !important; + box-shadow: none !important; + align-self: flex-start !important; + font-size: 1rem !important; + line-height: 1.5 !important; + margin: 0 !important; + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + width: 100% !important; +} + +.chat-hub-messages.compact-container .message.compact:hover, +.messages.compact-container .message.compact:hover { + background-color: #f8fafc !important; + overflow: visible !important; + white-space: normal !important; +} + +.chat-hub-messages.compact-container .message.compact .datetime, +.messages.compact-container .message.compact .datetime { + color: #a0a0a0 !important; + margin-right: 0.4rem !important; + font-size: 0.78rem !important; + font-family: monospace !important; + opacity: 1 !important; + display: inline !important; + margin-top: 0 !important; + margin-bottom: 0 !important; + padding: 0 !important; +} + +.chat-hub-messages.compact-container .message.compact .username, +.messages.compact-container .message.compact .username { + font-weight: bold !important; + margin-right: 0.2rem !important; + margin-bottom: 0 !important; + font-size: 0.875rem !important; + display: inline !important; + padding: 0 !important; +} + +.chat-hub-messages.compact-container .message.compact .messagetext, +.messages.compact-container .message.compact .messagetext { + color: #1e293b !important; + white-space: normal !important; + word-break: break-word !important; + display: inline !important; + margin: 0 !important; + font-size: 1rem !important; +} + +/* Make emoji characters render larger than surrounding text in chat */ +.chat-hub-messages .message .messagetext, +.chat-hub-messages.compact-container .message.compact .messagetext, +.messages.compact-container .message.compact .messagetext { + font-family: 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', 'Roboto', Arial, sans-serif; +} + +.chat-emoji { + font-size: 1.45em; + line-height: 1; + vertical-align: -0.15em; + display: inline-block; +} + +/* Fix RetroShare ID textarea - auto-size to content, no scrollbar */ +.homepage .certificate__content .retroshareID .textArea { + min-height: unset !important; + height: auto !important; + overflow: hidden !important; + field-sizing: content !important; +} + +/* Attach file button and modal popup */ +.chat-hub-attach-btn { + background-color: transparent; + border: none; + font-size: 1.25rem; + color: #64748b; + cursor: pointer; + padding: 0.5rem; + margin-right: 0.25rem; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + transition: color 0.2s, transform 0.2s; +} + +.chat-hub-attach-btn:hover { + color: #3b82f6; + transform: scale(1.05); +} + +.attach-modal-overlay { + 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; +} + +.attach-modal { + background-color: #ffffff; + border-radius: 0.5rem; + width: 450px; + max-width: 90%; + padding: 1.5rem; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + gap: 1rem; +} + +.attach-modal .attach-modal-header { + display: flex; + align-items: center; + gap: 0.6rem; + margin-bottom: 0.25rem; +} + +.attach-modal .attach-modal-icon { + font-size: 1.2rem; + color: #3b82f6; +} + +.attach-modal h4 { + margin: 0; + font-size: 1.2rem; + color: #0f172a; +} + +.attach-modal p { + margin: 0; + font-size: 0.9rem; + color: #475569; +} + +.attach-modal .attach-path-row { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.attach-modal .attach-path-row input[type="text"] { + flex: 1; + padding: 0.75rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + font-size: 0.9rem; + outline: none; + transition: border-color 0.2s; + min-width: 0; +} + +.attach-modal .attach-path-row input[type="text"]:focus { + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); +} + +.attach-browse-btn { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 0.35rem; + padding: 0.625rem 0.9rem; + font-size: 0.875rem; + background-color: #f1f5f9; + color: #334155; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + cursor: pointer; + box-shadow: none; + transition: background-color 0.2s, border-color 0.2s; + white-space: nowrap; +} + +.attach-browse-btn:hover { + background-color: #e2e8f0; + border-color: #94a3b8; +} + +.attach-path-hint { + display: flex; + align-items: flex-start; + gap: 0.5rem; + padding: 0.6rem 0.75rem; + background-color: #fffbeb; + border: 1px solid #fcd34d; + border-left: 3px solid #f59e0b; + border-radius: 0.375rem; + font-size: 0.825rem; + color: #92400e; + line-height: 1.45; +} + +.attach-path-hint i { + color: #f59e0b; + margin-top: 0.1rem; + flex-shrink: 0; +} + +.attach-path-hint code { + font-family: monospace; + background-color: rgba(245, 158, 11, 0.15); + padding: 0.05rem 0.25rem; + border-radius: 0.2rem; +} + +.attach-modal .hashing-spinner { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9rem; + color: #3b82f6; +} + +.attach-modal .error-text { + color: #ef4444; + font-size: 0.85rem; + margin: 0; +} + +.attach-modal .modal-buttons { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + margin-top: 0.5rem; +} + +.attach-modal .modal-buttons button { + padding: 0.5rem 1rem; + font-size: 0.9rem; + border-radius: 0.25rem; + border: none; + cursor: pointer; + transition: opacity 0.2s; +} + +.attach-modal .modal-buttons button:hover { + opacity: 0.9; +} + +/* ========================= Emoji Picker ========================= */ +.chat-hub-emoji-btn { + background-color: transparent; + border: none; + font-size: 1.3rem; + cursor: pointer; + padding: 0.35rem 0.4rem; + margin-right: 0.25rem; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.375rem; + line-height: 1; + transition: background-color 0.15s, transform 0.15s; + box-shadow: none; +} + +.chat-hub-emoji-btn:hover { + background-color: #f1f5f9; + transform: scale(1.1); +} + +.emoji-picker-wrapper { + position: relative; + flex-shrink: 0; + display: flex; + align-items: center; +} + +.emoji-picker { + position: absolute; + bottom: calc(100% + 0.5rem); + left: 0; + width: 320px; + background-color: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 0.625rem; + box-shadow: 0 8px 30px -4px rgba(0, 0, 0, 0.18), 0 4px 12px -2px rgba(0, 0, 0, 0.1); + z-index: 3000; + display: flex; + flex-direction: column; + overflow: hidden; + animation: emoji-pop 0.15s ease-out; +} + +@keyframes emoji-pop { + from { opacity: 0; transform: scale(0.92) translateY(6px); } + to { opacity: 1; transform: scale(1) translateY(0); } +} + +.emoji-search-row { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.6rem 0.75rem 0.4rem; + border-bottom: 1px solid #f1f5f9; +} + +.emoji-search-icon { + color: #94a3b8; + font-size: 0.8rem; + flex-shrink: 0; +} + +.emoji-search-input { + flex: 1; + border: 1px solid #e2e8f0; + border-radius: 0.375rem; + padding: 0.3rem 0.5rem; + font-size: 0.85rem; + outline: none; + background-color: #f8fafc; + transition: border-color 0.15s; +} + +.emoji-search-input:focus { + border-color: #3ba4d7; + background-color: #fff; +} + +.emoji-search-clear { + background: none; + border: none; + cursor: pointer; + color: #94a3b8; + padding: 0.2rem; + font-size: 0.8rem; + box-shadow: none; + display: flex; + align-items: center; +} + +.emoji-search-clear:hover { + color: #475569; +} + +.emoji-categories { + display: flex; + gap: 0.1rem; + padding: 0.35rem 0.5rem; + border-bottom: 1px solid #f1f5f9; + overflow-x: auto; + scrollbar-width: none; +} + +.emoji-categories::-webkit-scrollbar { + display: none; +} + +.emoji-cat-btn { + background: none; + border: none; + cursor: pointer; + font-size: 1.2rem; + padding: 0.3rem 0.35rem; + border-radius: 0.375rem; + line-height: 1; + box-shadow: none; + transition: background-color 0.1s; + flex-shrink: 0; +} + +.emoji-cat-btn:hover { + background-color: #f1f5f9; +} + +.emoji-cat-btn.active { + background-color: #e0f2fe; + box-shadow: inset 0 -2px 0 #3ba4d7; +} + +.emoji-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 0; + padding: 0.4rem 0.35rem; + max-height: 220px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: #cbd5e1 transparent; +} + +.emoji-grid::-webkit-scrollbar { + width: 4px; +} + +.emoji-grid::-webkit-scrollbar-track { + background: transparent; +} + +.emoji-grid::-webkit-scrollbar-thumb { + background-color: #cbd5e1; + border-radius: 4px; +} + +.emoji-btn { + background: none; + border: none; + cursor: pointer; + font-size: 1.7rem; + padding: 0.25rem; + border-radius: 0.3rem; + line-height: 1; + box-shadow: none; + text-align: center; + transition: background-color 0.1s, transform 0.1s; + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: 1; +} + +.emoji-btn:hover { + background-color: #f1f5f9; + transform: scale(1.2); +} + +table.mails th.sortable-th { + cursor: pointer; + user-select: none; + transition: background-color 0.2s, color 0.2s; +} + +table.mails th.sortable-th:hover { + background-color: #eef3f6; + color: #000; +} + +.compose-mail__from { + display: flex; + justify-content: flex-start; + align-items: center; + gap: 0.5rem; +} + +/* Status Bar Styles */ +.statusbar { + display: flex; + justify-content: space-between; + align-items: center; + height: 28px; + background-color: #14141b; + border-top: 1px solid #2e2e38; + padding: 0 1rem; + font-size: 0.8rem; + color: #94a3b8; + z-index: 100; + box-sizing: border-box; + user-select: none; + flex-shrink: 0; +} + +.statusbar-left { + display: flex; + align-items: center; +} + +.statusbar-right { + display: flex; + align-items: center; + gap: 1.5rem; +} + +.statusbar-item { + display: flex; + align-items: center; +} + +.statusbar-divider { + width: 1px; + height: 14px; + background-color: #2e2e38; +} + +.status-bullet { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.5); +} + +/* Hidden Service Configuration layout overrides */ +.proxy-server-container { + width: 100%; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.proxy-description { + color: #334155; + font-size: 0.95rem; + margin-bottom: 0.5rem; +} + +.proxy-rows-container { + display: flex; + flex-direction: column; + gap: 0.75rem; + width: 100%; +} + +.proxy-row { + display: grid; + grid-template-columns: 160px 220px 220px auto; + gap: 0.75rem; + align-items: center; + width: 100%; +} + +.proxy-label { + font-size: 0.95rem; + font-weight: 500; + color: #1e293b; +} + +.proxy-addr-input { + width: 100% !important; + max-width: none !important; +} + +.proxy-port-input { + width: 100% !important; + max-width: none !important; +} + +.proxy-status-container { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.proxy-status-bullet { + width: 14px; + height: 14px; + border-radius: 50%; + display: inline-block; + border: 1px solid #475569; +} + +.proxy-status-text { + font-size: 0.95rem; + color: #1e293b; +} + From 94f0ad985dbc822678bda69cc9521352f990593b Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:29:17 +0200 Subject: [PATCH 15/40] fix forward mail with correct id --- webui-src/app/mail/mail_compose.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webui-src/app/mail/mail_compose.js b/webui-src/app/mail/mail_compose.js index b895548..0bb24d2 100644 --- a/webui-src/app/mail/mail_compose.js +++ b/webui-src/app/mail/mail_compose.js @@ -148,7 +148,7 @@ const Layout = () => { } } - if (msgType === 'reply' || msgType === 'replyAll') { + if (msgType === 'reply' || msgType === 'replyAll' || msgType === 'forward') { Data.identity = Data.ownId.filter((id) => Object.prototype.hasOwnProperty.call(recipientList, id) )[0]; From fe63ef872b5b325461e3939f45329c8833f615ba Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:49:07 +0200 Subject: [PATCH 16/40] Fixed to not allow endless parcipants, added restriction like qt gui --- webui-src/app/mail/mail_compose.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/webui-src/app/mail/mail_compose.js b/webui-src/app/mail/mail_compose.js index 0bb24d2..87fdcee 100644 --- a/webui-src/app/mail/mail_compose.js +++ b/webui-src/app/mail/mail_compose.js @@ -4,6 +4,7 @@ const widget = require('widgets'); const peopleUtil = require('people/people_util'); const UserAvatarsCache = {}; +const MAX_RECIPIENTS = 20; const Layout = () => { let showCc = false; @@ -256,7 +257,13 @@ const Layout = () => { item.mGroupName.toLowerCase().includes(e.target.value.toLowerCase()) ); } + function totalRecipients() { + return Data.recipients.to.sendList.length + + Data.recipients.cc.sendList.length + + Data.recipients.bcc.sendList.length; + } function handleClick(item, recipientType) { + if (totalRecipients() >= MAX_RECIPIENTS) return; Data.recipients[recipientType].sendList.push(item); if (item.mGroupId && !UserAvatarsCache[item.mGroupId]) { rs.rsJsonApiRequest( @@ -364,6 +371,8 @@ const Layout = () => { m('input[type=text].recipients__input-field', { value: Data.recipients.to.inputVal, oninput: (e) => handleInput(e, 'to'), + placeholder: totalRecipients() >= MAX_RECIPIENTS ? 'Max recipients reached' : '', + disabled: totalRecipients() >= MAX_RECIPIENTS, }), m('ul.recipients__input-list[autocomplete=off]', [ Data.recipients.to.inputList.length > 0 @@ -418,6 +427,8 @@ const Layout = () => { m('input[type=text].recipients__input-field', { value: Data.recipients[recipientType].inputVal, oninput: (e) => handleInput(e, recipientType), + placeholder: totalRecipients() >= MAX_RECIPIENTS ? 'Max recipients reached' : '', + disabled: totalRecipients() >= MAX_RECIPIENTS, }), m('ul.recipients__input-list[autocomplete=off]', [ Data.recipients[recipientType].inputList.length > 0 @@ -434,6 +445,9 @@ const Layout = () => { ]), ]); }), + totalRecipients() >= MAX_RECIPIENTS && m('.compose-mail__recipient-limit', { + style: { color: '#e67e22', fontSize: '0.85rem', padding: '0.25rem 0' } + }, `Maximum of ${MAX_RECIPIENTS} recipients reached. Remove a recipient to add more.`), ]), m('input.compose-mail__subject[type=text][placeholder=Subject]', { value: Data.subject, From 059a28ab14346e4bb1e73adcbe7214063fc6c378 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Mon, 20 Jul 2026 21:07:06 +0200 Subject: [PATCH 17/40] Fixed sorting in mails & show identity icon when no avatar available --- webui-src/app/mail/mail_util.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/webui-src/app/mail/mail_util.js b/webui-src/app/mail/mail_util.js index e8f82f5..ad29988 100644 --- a/webui-src/app/mail/mail_util.js +++ b/webui-src/app/mail/mail_util.js @@ -175,15 +175,10 @@ const MessageSummary = () => { }, }, [ - fromUserInfo && - fromUserInfo.mAvatar && - fromUserInfo.mAvatar.mData && - fromUserInfo.mAvatar.mData.base64 && - fromUserInfo.mAvatar.mData.base64 !== '' && - m(peopleUtil.UserAvatar, { - avatar: fromUserInfo.mAvatar, - firstLetter: (fromUserInfo.mNickname || '').slice(0, 1).toUpperCase(), - identityId: details.from._addr_string, + m(peopleUtil.UserAvatar, { + avatar: fromUserInfo?.mAvatar, + firstLetter: (fromUserInfo?.mNickname || '').slice(0, 1).toUpperCase(), + identityId: details.from?._addr_string, size: 24, }), m('span', fromUserInfo && Number(fromUserInfo.mId) !== 0 ? fromUserInfo.mNickname : '[Unknown]'), @@ -477,8 +472,10 @@ function sortList(list) { case 'from': { const aSenderId = MessageCache[msgA.msgId]?.from?._addr_string || msgA.from?._addr_string; const bSenderId = MessageCache[msgB.msgId]?.from?._addr_string || msgB.from?._addr_string; - const aFrom = (aSenderId && (UserNicknamesCache[aSenderId] || rs.userList.userMap[aSenderId])) || ''; - const bFrom = (bSenderId && (UserNicknamesCache[bSenderId] || rs.userList.userMap[bSenderId])) || ''; + const aName = aSenderId && rs.userList.userMap[aSenderId]; + const bName = bSenderId && rs.userList.userMap[bSenderId]; + const aFrom = (UserNicknamesCache[aSenderId] || (aName && aName.name) || aName || '') + ''; + const bFrom = (UserNicknamesCache[bSenderId] || (bName && bName.name) || bName || '') + ''; valA = aFrom.toLowerCase(); valB = bFrom.toLowerCase(); break; From 6fd01182251a454b7b749f0e7465e201f29a8c5c Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Mon, 20 Jul 2026 22:07:05 +0200 Subject: [PATCH 18/40] split chats code --- webui-src/app/chat/chat.js | 1018 +++--------------------------- webui-src/app/chat/chat_emoji.js | 149 +++++ webui-src/app/chat/chat_state.js | 695 ++++++++++++++++++++ 3 files changed, 937 insertions(+), 925 deletions(-) create mode 100644 webui-src/app/chat/chat_emoji.js create mode 100644 webui-src/app/chat/chat_state.js diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index 9808a0e..bdefad4 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -2,852 +2,29 @@ const m = require('mithril'); const rs = require('rswebui'); const peopleUtil = require('people/people_util'); const people = require('people/people'); +const chatState = require('chat/chat_state'); +const chatEmoji = require('chat/chat_emoji'); -// **************** utility functions ******************** +const { + get64Num, + loadLobbyDetails, + loadDistantChatDetails, + sortLobbies, + getNicknameColor, + getStatusColor, + getStatusTooltip, + renderTextWithEmoji, + getSafeAvatar, + MobileState, + ChatRoomsModel, + Message, + ChatLobbyModel, + ChatHubState, +} = chatState; -function get64Num(val) { - if (!val) return 0; - if (typeof val === 'object') { - return val.xint64 || parseInt(val.xstr64) || 0; - } - return Number(val) || 0; -} +chatEmoji.setDependencies({ ChatHubState }); -function loadLobbyDetails(id, apply) { - rs.rsJsonApiRequest( - '/rsChats/getChatLobbyInfo', - { - id: { xstr64: id }, - }, - (detail, success) => { - if (success && detail.retval) { - detail.info.chatType = 3; // LOBBY - apply(detail.info); - } else { - apply(null); - } - }, - true - ); -} - -function loadDistantChatDetails(pid, apply) { - // pid is DistantChatPeerId (uint32) - rs.rsJsonApiRequest( - '/rsChats/getDistantChatStatus', - { - pid: pid, - }, - (detail, success) => { - if (success && detail.retval) { - // Map to lobby-like structure for UI compatibility - const info = detail.info; - info.chatType = 2; // DISTANT (matches TYPE_PRIVATE_DISTANT in rschats.h) - info.lobby_name = rs.userList.username(info.to_id) || 'Distant Chat ' + pid; - info.lobby_topic = 'Private Encrypted Chat'; - info.gxs_id = info.own_id; - info.lobby_id = pid; // Distant IDs are 128-bit hex strings, NO xstr64 wrapper - apply(info); - } else { - apply(null); - } - }, - true - ); -} - -function sortLobbies(lobbies) { - if (lobbies !== undefined && lobbies !== null) { - const list = [...lobbies]; - list.sort((a, b) => a.lobby_name.localeCompare(b.lobby_name)); - return list; - } - return []; // return empty array instead of undefined -} - -function getNicknameColor(id, name) { - const hashString = id && id !== '00000000000000000000000000000000' ? id : (name || ''); - let hash = 0; - for (let i = 0; i < hashString.length; i++) { - hash = hashString.charCodeAt(i) + ((hash << 5) - hash); - } - const hue = Math.abs(hash) % 360; - return `hsl(${hue}, 75%, 35%)`; -} - -// ***************************** models *********************************** - -const MobileState = { - showLobbies: false, - showUsers: false, - toggleLobbies() { - this.showLobbies = !this.showLobbies; - this.showUsers = false; - }, - toggleUsers() { - this.showUsers = !this.showUsers; - this.showLobbies = false; - }, - closeAll() { - this.showLobbies = false; - this.showUsers = false; - }, -}; - - -const ChatRoomsModel = { - allRooms: [], - knownSubscrIds: [], // to exclude subscribed from public rooms (subscribedRooms filled to late) - subscribedRooms: {}, - loadPublicRooms() { - // TODO: this doesn't preserve id of rooms, - // use regex on response to extract ids. - rs.rsJsonApiRequest( - '/rsChats/getListOfNearbyChatLobbies', - {}, - (data) => { - if (data && data.public_lobbies) { - // Deduplicate by ID to avoid double display if backend returns redundant info - const seen = new Set(); - const uniqueLobbies = data.public_lobbies.filter((lobby) => { - const id = rs.idToHex(lobby.lobby_id); - if (seen.has(id)) return false; - seen.add(id); - return true; - }); - ChatRoomsModel.allRooms = sortLobbies(uniqueLobbies); - } else { - // No public lobbies - ChatRoomsModel.allRooms = []; - } - } - ); - }, - loadSubscribedRooms(after = null) { - rs.rsJsonApiRequest( - '/rsChats/getChatLobbyList', - {}, - (data) => { - if (data && data.cl_list) { - // Robust deduplication of IDs - const ids = [...new Set(data.cl_list.map((lid) => rs.idToHex(lid)))]; - ChatRoomsModel.knownSubscrIds = ids; - - // Remove stale entries that are no longer in the subscribed list - Object.keys(ChatRoomsModel.subscribedRooms).forEach((id) => { - if (!ids.includes(id)) { - delete ChatRoomsModel.subscribedRooms[id]; - } - }); - - if (ids.length === 0) { - ChatRoomsModel.loadPublicRooms(); - if (after != null) after(); - m.redraw(); - return; - } - - let count = 0; - ids.forEach((id) => - loadLobbyDetails(id, (info) => { - if (info) { - ChatRoomsModel.subscribedRooms[id] = info; - } - count++; - if (count === ids.length) { - ChatRoomsModel.loadPublicRooms(); // Load public rooms after we know all subscribed IDs - if (after != null) { - after(); - } - m.redraw(); - } - }) - ); - } else { - // No subscribed lobbies - ChatRoomsModel.loadPublicRooms(); - } - } - ); - }, - subscribed(info) { - return this.knownSubscrIds.includes(rs.idToHex(info.lobby_id)); - }, -}; - -/** - * Wraps emoji characters in a span so CSS can size them independently. - */ -function renderTextWithEmoji(text) { - if (!text) return ''; - // Match emoji sequences (flags, ZWJ sequences, variation selectors, skin tones, etc.) - const emojiRegex = /(?:\p{Emoji_Presentation}|\p{Extended_Pictographic})(?:[\u{1F3FB}-\u{1F3FF}])?(?:\u{FE0F})?(?:\u{20E3})?(?:(?:\u{200D}(?:\p{Emoji_Presentation}|\p{Extended_Pictographic})(?:[\u{1F3FB}-\u{1F3FF}])?(?:\u{FE0F})?)*)/gu; - const parts = []; - let last = 0; - let match; - // eslint-disable-next-line no-cond-assign - while ((match = emojiRegex.exec(text)) !== null) { - if (match[0].length === 0) { emojiRegex.lastIndex++; continue; } - if (match.index > last) parts.push(text.slice(last, match.index)); - parts.push(m('span.chat-emoji', match[0])); - last = match.index + match[0].length; - } - if (last < text.length) parts.push(text.slice(last)); - return parts.length > 0 ? parts : text; -} - -/** - * Message displays a single Chat-Message
- * currently removes formatting and in consequence inline links - * msg: Message to Display - */ -const Message = () => { - return { - view: (vnode) => { - const msg = vnode.attrs; - const datetime = new Date(msg.sendTime * 1000).toLocaleTimeString(); - 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( - '.message.incoming', - [ - m('span.datetime', datetime), - m('span.username', 'Chat status'), - m('.messagetext', { - style: { - backgroundColor: bgColor, - border: `1px ${borderStyle} ${borderColor}`, - color: textColor, - padding: '0.5rem 0.75rem', - borderRadius: '0.375rem', - display: 'inline-block', - marginTop: '0.25rem', - } - }, text) - ] - ); - } - // Handle both HistoryMsg (peerId) and ChatMessage (lobby_peer_gxs_id) - const rawGxsId = msg.lobby_peer_gxs_id || msg.peerId; - let gxsId = rs.idToHex(rawGxsId); - - // Fallback for 1-to-1 chats where sender ID might be missing (zeros) - const isZero = (id) => !id || id === '00000000000000000000000000000000'; - if (isZero(gxsId)) { - const lobby = ChatLobbyModel.currentLobby; - // Types 1 (Private), 2 (Distant) are "private" conversations here - if (lobby && (lobby.chatType === 1 || lobby.chatType === 2)) { - gxsId = msg.incoming ? rs.idToHex(lobby.to_id || lobby.peer_id || lobby.distant_chat_id) : rs.idToHex(lobby.own_id || lobby.gxs_id); - } - } - - const isMuted = ChatHubState.mutedUsers && ChatHubState.mutedUsers.has(gxsId); - const details = ChatHubState.gxsDetails[gxsId]; - const opinion = details && details.mReputation ? details.mReputation.mOwnOpinion : 1; - const isBanned = opinion === 0; - - if (isMuted || isBanned) { - return null; - } - - let username = rs.userList.username(gxsId) || msg.peerName || '???'; - // If we only have the hex ID, try to fallback to the peerName from the message - if (username === gxsId && msg.peerName) { - username = msg.peerName; - } - if (username === gxsId && gxsId && gxsId.length > 12) { - username = gxsId.substring(0, 8) + '...'; - } - const text = (msg.msg || msg.message || '') - .replaceAll('
', '\n') - .replace(new RegExp('|<[^>]*>', 'gm'), ''); - - const chatType = ChatLobbyModel.currentLobby && ChatLobbyModel.currentLobby.chatType; - const isRoom = chatType === 3; - - if (isRoom) { - const nickColor = getNicknameColor(gxsId, username); - return m( - '.message.compact', - m('span.datetime', datetime), - m('span.username', { style: { color: nickColor } }, username + ':'), - m('span.messagetext', renderTextWithEmoji(text)) - ); - } - - return m( - '.message' + (msg.incoming ? '.incoming' : '.outgoing'), - m('span.datetime', datetime), - m('span.username', username), - m('span.messagetext', renderTextWithEmoji(text)) - ); - }, - }; -}; - -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.'; - } -} - -const ChatLobbyModel = { - currentLobby: { - lobby_name: '...', - }, - lobby_user: '...', - isSubscribed: false, - messages: [], - users: [], - messageKeys: new Set(), - lastLobbyId: null, - distantChatStatus: null, - statusPollInterval: null, - - pollDistantChatStatus() { - if (!this.currentLobby || this.currentLobby.chatType !== 2) return; - rs.rsJsonApiRequest( - '/rsChats/getDistantChatStatus', - { - pid: this.currentLobby.lobby_id, - }, - (detail, success) => { - if (success && detail.retval) { - const oldStatus = this.distantChatStatus ? this.distantChatStatus.status : null; - this.distantChatStatus = detail.info; - - if (oldStatus !== null && oldStatus !== detail.info.status) { - if (detail.info.status === 2) { - this.addMessages([{ - chat_id: this.chatId(), - isSystem: true, - msg: 'Tunnel is secured. You can talk!', - sendTime: Math.floor(Date.now() / 1000) - }]); - } else if (detail.info.status === 3) { - this.addMessages([{ - chat_id: this.chatId(), - isSystem: true, - msg: 'Your partner closed the conversation.', - sendTime: Math.floor(Date.now() / 1000) - }]); - } - } - m.redraw(); - } - } - ); - }, - - startStatusPolling() { - this.stopStatusPolling(); - this.pollDistantChatStatus(); - this.statusPollInterval = setInterval(() => this.pollDistantChatStatus(), 3000); - }, - - stopStatusPolling() { - if (this.statusPollInterval) { - clearInterval(this.statusPollInterval); - this.statusPollInterval = null; - } - this.distantChatStatus = null; - }, - - // Helper to generate a unique key for deduplication - getMessageKey(msg) { - if (msg.msgId && msg.msgId !== 0) return 'id_' + msg.msgId; - // Fallback for live messages or history without IDs - const text = msg.msg || msg.message || ''; - return 't_' + msg.sendTime + '_' + text.substring(0, 32); - }, - - addMessages(newMsgs, scroll = false) { - let added = false; - newMsgs.forEach((msg) => { - const key = this.getMessageKey(msg); - if (!this.messageKeys.has(key)) { - // Near-duplicate check for messages without IDs (live events vs optimistic echo) - const text = msg.msg || msg.message || ''; - const isNearDuplicate = this.messages.some((existingMsg) => { - const eAttrs = existingMsg.attrs; - const eText = eAttrs.msg || eAttrs.message || ''; - return ( - eText === text && - Math.abs(eAttrs.sendTime - msg.sendTime) < 5 // 5 seconds window - ); - }); - - if (!isNearDuplicate) { - this.messageKeys.add(key); - this.messages.push(m(Message, msg)); - added = true; - } - } - }); - - if (added) { - this.messages.sort((a, b) => a.attrs.sendTime - b.attrs.sendTime); - m.redraw(); - if (scroll) { - setTimeout(() => { - const element = document.querySelector('.messages'); - if (element) { - element.scrollTop = element.scrollHeight; - } - }, 100); - } - } - }, - - loadHistory(id, type) { - const chatPeerId = { - broadcast_status_peer_id: '00000000000000000000000000000000', - type: type, - peer_id: '00000000000000000000000000000000', - distant_chat_id: '00000000000000000000000000000000', - lobby_id: { xstr64: '0' }, - }; - - if (type === 3) chatPeerId.lobby_id.xstr64 = id; - else if (type === 2) chatPeerId.distant_chat_id = id; - else if (type === 1) chatPeerId.peer_id = id; - - rs.rsJsonApiRequest( - '/rsHistory/getMessages', - { - chatPeerId: chatPeerId, - loadCount: 20, - }, - (data, success) => { - if (success && data.msgs) { - this.addMessages(data.msgs); - } - } - ); - }, - setupAction: (lobbyId, nick) => { }, - setIdentity(lobbyId, nick) { - rs.rsJsonApiRequest( - '/rsChats/setIdentityForChatLobby', - { - lobby_id: { xstr64: lobbyId }, - nick: nick, - }, - () => m.route.set('/chat/:lobby', { lobby: lobbyId }), - true - ); - }, - enterPublicLobby(lobbyId, nick) { - // Set lobby nickname - rs.rsJsonApiRequest( - '/rsChats/joinVisibleChatLobby', - { - lobby_id: { xstr64: lobbyId }, - own_id: nick, - }, - () => { - loadLobbyDetails(lobbyId, (info) => { - ChatRoomsModel.subscribedRooms[lobbyId] = info; - ChatRoomsModel.loadSubscribedRooms(() => { - m.route.set('/chat/:lobby', { lobby: rs.idToHex(info.lobby_id) }); - }); - }); - }, - true - ); - }, - unsubscribeChatLobby(lobbyId, follow) { - // Unsubscribe - rs.rsJsonApiRequest( - '/rsChats/unsubscribeChatLobby', - { - lobby_id: { xstr64: lobbyId }, - }, - (data, success) => { - if (success) { - ChatRoomsModel.loadSubscribedRooms(follow); - } - }, - true - ); - }, - chatId() { - const type = (this.currentLobby && this.currentLobby.chatType) || 3; - const id = this.lastLobbyId || m.route.param('lobby'); - const cid = { - broadcast_status_peer_id: '00000000000000000000000000000000', - type: type, - peer_id: '00000000000000000000000000000000', - distant_chat_id: '00000000000000000000000000000000', - lobby_id: { xstr64: '0' }, - }; - if (type === 3) cid.lobby_id.xstr64 = id; - else if (type === 2) cid.distant_chat_id = id; - else if (type === 1) cid.peer_id = id; - return cid; - }, - loadLobby(currentlobbyid) { - this.stopStatusPolling(); - this.lastLobbyId = currentlobbyid; - - const finishLoad = (detail) => { - this.setupAction = this.setIdentity; - this.currentLobby = detail; - this.isSubscribed = true; - this.lobby_user = rs.userList.username(detail.gxs_id) || '???'; - - // Reset local state for this lobby - this.messages = []; - this.messageKeys.clear(); - - // Load history first - this.loadHistory(currentlobbyid, detail.chatType); - - // Apply existing messages from live cache - const cid = this.chatId(); - rs.events[15].chatMessages(cid, rs.events[15], (l) => { - this.addMessages(l); - }); - - // Register for chatEvents for future messages - rs.events[15].notify = (chatMessage) => { - // DEBUG: Log incoming message structure - console.log('[RS-DEBUG] Incoming Chat Message:', JSON.stringify(chatMessage, null, 2)); - - const msgCid = chatMessage.chat_id; - let msgId; - - if (msgCid.type === 3) { - msgId = rs.idToHex(msgCid.lobby_id); - } else if (msgCid.type === 2) { - // For Distant Chat, the ID is the distant_chat_id - msgId = rs.idToHex(msgCid.distant_chat_id); - } else if (msgCid.type === 1) { - // For Private Chat, the ID is the peer_id - msgId = rs.idToHex(msgCid.peer_id); - } else { - // Fallback - msgId = rs.idToHex(msgCid); - } - - console.log('[RS-DEBUG] Resolved Msg ID:', msgId, 'Current Lobby ID:', currentlobbyid, 'Match:', msgId === currentlobbyid); - - if (msgId === currentlobbyid) { - this.addMessages([chatMessage]); - } - }; - - // Lookup for chat-user names - let list = []; - if (detail.gxs_ids) { - if (Array.isArray(detail.gxs_ids)) { - list = detail.gxs_ids.map((u) => { - const key = u.key; - return { key, name: rs.userList.username(key) || key, lastAct: get64Num(u.value) }; - }); - } else if (typeof detail.gxs_ids === 'object') { - list = Object.keys(detail.gxs_ids).map((key) => { - return { key, name: rs.userList.username(key) || key, lastAct: get64Num(detail.gxs_ids[key]) }; - }); - } - } - - const ownId = detail.gxs_id; - if (ownId && ownId !== '00000000000000000000000000000000') { - const hasOwn = list.some((u) => u.key === ownId); - if (!hasOwn) { - list.push({ - key: ownId, - name: rs.userList.username(ownId) || ownId, - lastAct: Math.floor(Date.now() / 1000) - }); - } - } - - if (list.length === 0) { - list = [{ key: ownId || '', name: rs.userList.username(ownId) || detail.lobby_name || '???', lastAct: Math.floor(Date.now() / 1000) }]; - } - - list.sort((a, b) => a.name.localeCompare(b.name)); - this.users = list; - - if (detail.chatType === 2) { - this.startStatusPolling(); - } - - m.redraw(); - }; - - loadLobbyDetails(currentlobbyid, (detail) => { - if (detail) { - finishLoad(detail); - } else { - // Fallback to Distant Chat - loadDistantChatDetails(currentlobbyid, (dDetail) => { - if (dDetail) { - finishLoad(dDetail); - } - }); - } - }); - }, - loadPublicLobby(currentlobbyid) { - this.setupAction = this.enterPublicLobby; - this.isSubscribed = false; - ChatRoomsModel.allRooms.forEach((it) => { - if (rs.idToHex(it.lobby_id) === currentlobbyid) { - this.currentLobby = it; - this.lobby_user = '???'; - this.lobbyid = currentlobbyid; - } - }); - this.users = []; - }, - sendMessage(msg, onsuccess) { - const cid = this.chatId(); - // Optimistic echo for immediate feedback - const echoMsg = { - chat_id: cid, - msg: msg, - sendTime: Math.floor(Date.now() / 1000), - lobby_peer_gxs_id: this.currentLobby.gxs_id, - }; - this.addMessages([echoMsg], true); - - rs.rsJsonApiRequest( - '/rsChats/sendChat', - { - id: cid, - msg: msg, - }, - (data, success) => { - if (success) { - onsuccess(); - } else { - console.error('[RS] Failed to send chat message'); - onsuccess(); // Clear the input even on failure to avoid stuck 'sending...' state - } - } - ); - }, - selected(info, selName, defaultName) { - const currid = rs.idToHex(ChatLobbyModel.currentLobby.lobby_id || { xstr64: m.route.param('lobby') }); - return (rs.idToHex(info.lobby_id) === currid ? selName : '') + defaultName; - }, - switchToEvent(info) { - return () => { - ChatLobbyModel.currentLobby = info; - m.route.set('/chat/:lobby', { lobby: rs.idToHex(info.lobby_id) }); - ChatLobbyModel.loadLobby(rs.idToHex(info.lobby_id)); // update - }; - }, - setupEvent(info) { - return () => { - m.route.set('/chat/:lobby/setup', { lobby: rs.idToHex(info.lobby_id) }); - ChatLobbyModel.loadPublicLobby(rs.idToHex(info.lobby_id)); // update - }; - }, -}; - -// ************************* Chat Hub State **************************** - -function getSafeAvatar(details) { - if ( - details && - details.mAvatar && - details.mAvatar.mData && - details.mAvatar.mData.base64 !== '' - ) { - return details.mAvatar; - } - return undefined; -} - -const ChatHubState = { - selectedRoomId: null, - selectedRoom: null, - selectedRoomType: null, - searchString: '', - ownProfile: { name: 'Loading...' }, - gxsDetails: {}, - hoveredUser: null, - mutedUsers: new Set(), - activeMenu: null, - showAttachModal: false, - attachPath: '', - attachBrowseHint: false, - isHashing: false, - hashingError: '', - showEmojiPicker: false, - emojiSearch: '', - emojiCategory: 'Smileys', - showCreateRoomModal: false, - newRoomName: '', - newRoomTopic: '', - newRoomIdentity: '', - newRoomPublic: true, - newRoomSigned: false, - ownGxsIdentities: [], - createRoomError: '', - userSortMethod: 'name', - showInviteModal: false, - friendsList: [], - selectedFriendsToInvite: new Set(), -}; - -// ========================= Emoji Data ========================= -const EMOJI_CATEGORIES = ['Smileys', 'People', 'Animals', 'Food', 'Travel', 'Activities', 'Objects', 'Symbols']; -const EMOJI_ICONS = { - Smileys: '😊', People: '👥', Animals: '🐾', Food: '🍎', - Travel: '✈️', Activities: '⚽', Objects: '💡', Symbols: '❤️', -}; -const EMOJI_DATA = { - Smileys: [ - '😀','😁','😂','🤣','😃','😄','😅','😆','😉','😊','😋','😎','😍','😘','🥰','😗','😙','😚', - '🙂','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😯','😪','😫','🥱', - '😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','☹️','🙁','😖','😞','😟', - '😤','😢','😭','😦','😧','😨','😩','🤯','😬','😰','😱','🥵','🥶','😳','🤪','😵','😡','😠', - '🤬','😷','🤒','🤕','🤢','🤮','🤧','🥴','😇','🥳','🥺','🤠','🤡','🤥','🤫','🤭','🧐','🤓', - '😈','👿','👹','👺','💀','☠️','👻','👽','👾','🤖','😺','😸','😹','😻','😼','😽','🙀','😿','😾', - ], - People: [ - '👋','🤚','🖐️','✋','🖖','👌','🤌','🤏','✌️','🤞','🤟','🤘','🤙','👈','👉','👆','🖕','👇', - '☝️','👍','👎','✊','👊','🤛','🤜','👏','🙌','👐','🤲','🤝','🙏','✍️','💅','🤳','💪','🦾', - '🦿','🦵','🦶','👂','🦻','👃','🫀','🫁','🧠','🦷','🦴','👀','👁️','👅','👄','🫦','👶','🧒', - '👦','👧','🧑','👱','👨','🧔','👩','🧓','👴','👵','🙍','🙎','🙅','🙆','💁','🙋','🧏','🙇', - '🤦','🤷','👮','🕵️','💂','🥷','👷','🫅','🤴','👸','👲','🧕','🤵','👰','🤰','🫃','🫄','🤱', - '👼','🎅','🤶','🧑‍🎄','🦸','🦹','🧙','🧝','🧛','🧟','🧞','🧜','🧚','🧑‍🤝‍🧑','👫','👬','👭','💏','💑','👪', - ], - Animals: [ - '🐶','🐱','🐭','🐹','🐰','🦊','🐻','🐼','🐻‍❄️','🐨','🐯','🦁','🐮','🐷','🐸','🐵','🙈','🙉', - '🙊','🐒','🦆','🦅','🦉','🦇','🐝','🪱','🐛','🦋','🐌','🐞','🐜','🪲','🦗','🪳','🕷️','🦂', - '🐢','🐍','🦎','🦖','🦕','🐙','🦑','🦐','🦞','🦀','🐡','🐠','🐟','🐬','🐳','🐋','🦈','🦭', - '🐊','🐅','🐆','🦓','🦍','🦧','🦣','🐘','🦛','🦏','🐪','🐫','🦒','🦘','🦬','🐃','🐂','🐄', - '🐎','🐖','🐏','🐑','🦙','🐐','🦌','🐕','🐩','🦮','🐕‍🦺','🐈','🐈‍⬛','🐓','🦃','🦤','🦚','🦜', - '🦢','🦩','🕊️','🐇','🦝','🦨','🦡','🦫','🦦','🦥','🐁','🐀','🐿️','🦔','🐾','🐉','🐲','🌵', - ], - Food: [ - '🍎','🍊','🍋','🍌','🍍','🥭','🍓','🍒','🍑','🥝','🍅','🥥','🥑','🍆','🥔','🥕','🌽','🌶️', - '🫑','🥒','🥬','🥦','🧄','🧅','🍄','🥜','🌰','🍞','🥐','🥖','🫓','🥨','🧀','🥚','🍳','🧈', - '🥞','🧇','🥓','🥩','🍗','🍖','🦴','🌭','🍔','🍟','🍕','🫔','🌮','🌯','🥙','🧆','🥚','🍱', - '🍘','🍙','🍚','🍛','🍜','🍝','🍠','🍢','🍣','🍤','🍥','🥮','🍡','🥟','🥠','🥡','🦪','🍦', - '🍧','🍨','🍩','🍪','🎂','🍰','🧁','🥧','🍫','🍬','🍭','🍮','🍯','🍼','🥛','☕','🫖','🍵', - '🧃','🥤','🧋','🍶','🍺','🍻','🥂','🍷','🥃','🍸','🍹','🧉','🍾','🧊','🥄','🍴','🍽️','🥢', - ], - Travel: [ - '🚗','🚕','🚙','🚌','🚎','🏎️','🚓','🚑','🚒','🚐','🛻','🚚','🚛','🚜','🦯','🦽','🦼','🛺', - '🚲','🛴','🛵','🏍️','🛺','🚨','🚔','🚍','🚘','🚖','🚡','🚠','🚟','🚃','🚋','🚞','🚝','🚄', - '🚅','🚈','🚂','🚆','🚇','🚊','🚉','✈️','🛫','🛬','🛩️','💺','🛸','🚁','🛶','⛵','🚤','🛥️', - '🛳️','⛴️','🚢','⚓','🗺️','🧭','🏔️','⛰️','🌋','🗻','🏕️','🏖️','🏜️','🏝️','🏞️','🏟️','🏛️','🏗️', - '🧱','🪨','🪵','🛖','🏘️','🏚️','🏠','🏡','🏢','🏣','🏤','🏥','🏦','🏨','🏩','🏪','🏫','🏬', - '🏭','🏯','🏰','💒','🗼','🗽','⛪','🕌','🛕','🕍','⛩️','🕋','⛲','⛺','🌁','🌃','🏙️','🌄', - ], - Activities: [ - '⚽','🏀','🏈','⚾','🥎','🎾','🏐','🏉','🥏','🎱','🏓','🏸','🏒','🏑','🥍','🏏','🪃','🥅', - '⛳','🪁','🛝','🏹','🎣','🤿','🥊','🥋','🎽','🛹','🛷','⛸️','🥌','🎿','⛷️','🏂','🪂','🏋️', - '🤼','🤸','⛹️','🤺','🏇','🧘','🏄','🏊','🤽','🚣','🧗','🚵','🚴','🏆','🥇','🥈','🥉','🏅', - '🎖️','🏵️','🎗️','🎫','🎟️','🎪','🤹','🎭','🩰','🎨','🖼️','🎰','🎲','🧩','🪄','🎯','🪅','🎮', - '🕹️','🎳','🎻','🎷','🥁','🪘','🎺','🎸','🪗','🎹','🎵','🎶','🎼','🎤','🎧','📻','🎙️','🎚️', - '🎬','📽️','🎞️','📱','📲','☎️','📞','📟','📠','🔋','🪫','🔌','💡','🔦','🕯️','💸','💵','🪙', - ], - Objects: [ - '⌚','📱','📲','💻','⌨️','🖥️','🖨️','🖱️','🖲️','💾','💿','📀','🧮','📷','📸','📹','🎥','📽️', - '📞','☎️','📟','📠','📺','📻','🧭','⏱️','⏲️','⏰','🕰️','⌛','⏳','📡','🔋','🪫','🔌','💡', - '🔦','🕯️','🪔','🧱','💰','💴','💵','💶','💷','💸','💳','🪙','💹','✉️','📧','📨','📩','📤', - '📥','📦','📫','📪','📬','📭','📮','🗳️','✏️','✒️','🖊️','🖋️','📝','📁','📂','🗂️','📅','📆', - '🗒️','🗓️','📇','📈','📉','📊','📋','📌','📍','🗺️','📏','📐','✂️','🗃️','🗄️','🗑️','🔒','🔓', - '🔏','🔐','🔑','🗝️','🔨','🪓','⛏️','⚒️','🛠️','🗡️','⚔️','🔫','🪃','🏹','🛡️','🪚','🔧','🪛', - ], - Symbols: [ - '❤️','🧡','💛','💚','💙','💜','🖤','🤍','🤎','💔','❣️','💕','💞','💓','💗','💖','💘','💝', - '💟','☮️','✝️','☪️','🕉️','☸️','✡️','🔯','🕎','☯️','☦️','🛐','⛎','♈','♉','♊','♋','♌', - '♍','♎','♏','♐','♑','♒','♓','🆔','⚛️','🉑','☢️','☣️','📴','📳','🈶','🈚','🈸','🈺', - '🈷️','✴️','🆚','💮','🉐','㊙️','㊗️','🈴','🈵','🈹','🈲','🅰️','🅱️','🆎','🆑','🅾️','🆘', - '❌','⭕','🛑','⛔','📛','🚫','💯','💢','♨️','🚷','🚯','🚳','🚱','🔞','📵','🚭','❗','❕', - '❓','❔','‼️','⁉️','🔅','🔆','📶','🛜','📳','📴','🔱','📛','🔰','♻️','✅','🈯','💹','❎', - '🌐','💠','Ⓜ️','🌀','💤','🏧','🚾','♿','🅿️','🛗','🈳','🈹','🚰','🔤','🔡','🔠','🆖','🆗', - '🆙','🆒','🆕','🆓','🔟','📊','🔣','✔️','☑️','🔘','🔲','🔳','⬛','⬜','◼️','◻️','◾','◽', - '▪️','▫️','🔶','🔷','🔸','🔹','🔺','🔻','💠','🔘','🔲','🔳','🏁','🚩','🎌','🏴','🏳️','⭐', - '🌟','💫','✨','🌈','☀️','🌤️','⛅','🌥️','☁️','🌦️','🌧️','⛈️','🌩️','🌨️','❄️','☃️','⛄','🌬️', - ], -}; - -function insertEmojiIntoTextarea(emoji) { - const textarea = document.querySelector('.chat-hub-textarea'); - if (!textarea) return; - const start = textarea.selectionStart; - const end = textarea.selectionEnd; - const before = textarea.value.substring(0, start); - const after = textarea.value.substring(end); - textarea.value = before + emoji + after; - const newPos = start + emoji.length; - textarea.selectionStart = newPos; - textarea.selectionEnd = newPos; - textarea.focus(); -} - -const EmojiPicker = () => ({ - view: () => { - const search = ChatHubState.emojiSearch.toLowerCase(); - const cat = ChatHubState.emojiCategory; - let emojis; - if (search) { - emojis = Object.values(EMOJI_DATA).flat(); - } else { - emojis = EMOJI_DATA[cat] || []; - } - return m('.emoji-picker', [ - // Search bar - m('.emoji-search-row', [ - m('i.fas.fa-search.emoji-search-icon'), - m('input.emoji-search-input[type=text][placeholder=Search emoji...]', { - value: ChatHubState.emojiSearch, - oninput: (e) => { ChatHubState.emojiSearch = e.target.value; }, - }), - ChatHubState.emojiSearch && m('button.emoji-search-clear', { - onclick: () => { ChatHubState.emojiSearch = ''; }, - }, m('i.fas.fa-times')), - ]), - // Category tabs (hidden while searching) - !search && m('.emoji-categories', EMOJI_CATEGORIES.map(c => - m('button.emoji-cat-btn' + (c === cat ? '.active' : ''), { - title: c, - onclick: () => { ChatHubState.emojiCategory = c; }, - }, EMOJI_ICONS[c]) - )), - // Emoji grid - m('.emoji-grid', - emojis.map(e => - m('button.emoji-btn', { - onclick: () => { - insertEmojiIntoTextarea(e); - ChatHubState.showEmojiPicker = false; - m.redraw(); - }, - }, e) - ) - ), - ]); - }, -}); +// ************************* helpers **************************** function loadOwnChatProfile() { rs.rsJsonApiRequest('/rsConfig/getConfigNetStatus', {}, (data) => { @@ -858,6 +35,66 @@ function loadOwnChatProfile() { }); } +function loadFriendsForInvite() { + ChatHubState.friendsList = []; + rs.rsJsonApiRequest('/rsPeers/getFriendList', {}, (data) => { + if (data && data.sslIds) { + data.sslIds.forEach((sslId) => { + rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { sslId }, (detData) => { + if (detData && detData.det) { + rs.rsJsonApiRequest('/rsPeers/isOnline', { sslId }, (onlineData) => { + ChatHubState.friendsList.push({ + id: sslId, + name: detData.det.name, + online: onlineData ? onlineData.retval : false + }); + ChatHubState.friendsList.sort((a, b) => { + if (a.online !== b.online) return a.online ? -1 : 1; + return a.name.localeCompare(b.name); + }); + m.redraw(); + }); + } + }); + }); + } + }); +} + +function scrollChatToBottom() { + setTimeout(() => { + const element = document.querySelector('.chat-hub-messages'); + if (element) { + element.scrollTop = element.scrollHeight; + } + }, 50); +} + +function pollHashStatus(localpath) { + rs.rsJsonApiRequest('/rsFiles/ExtraFileStatus', { localpath }, (data) => { + if (data && data.retval && data.info && data.info.hash && data.info.hash !== '0000000000000000000000000000000000000000') { + const info = data.info; + const sizeNum = info.size.xint64 || parseInt(info.size.xstr64) || info.size; + const fileLink = `${info.name} (${rs.formatBytes(sizeNum)})`; + + const textarea = document.querySelector('.chat-hub-textarea'); + if (textarea) { + const val = textarea.value; + textarea.value = val ? val + '\n' + fileLink : fileLink; + } + + ChatHubState.showAttachModal = false; + ChatHubState.isHashing = false; + ChatHubState.attachPath = ''; + m.redraw(); + } else { + if (ChatHubState.isHashing) { + setTimeout(() => pollHashStatus(localpath), 1000); + } + } + }); +} + // ************************* views **************************** const Lobby = () => { @@ -929,33 +166,6 @@ const PublicLobbies = { // ************************* Chat Hub Sub-Components **************************** -function loadFriendsForInvite() { - ChatHubState.friendsList = []; - rs.rsJsonApiRequest('/rsPeers/getFriendList', {}, (data) => { - if (data && data.sslIds) { - data.sslIds.forEach((sslId) => { - rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { sslId }, (detData) => { - if (detData && detData.det) { - rs.rsJsonApiRequest('/rsPeers/isOnline', { sslId }, (onlineData) => { - ChatHubState.friendsList.push({ - id: sslId, - name: detData.det.name, - online: onlineData ? onlineData.retval : false - }); - // Sort online friends first, then alphabetical name - ChatHubState.friendsList.sort((a, b) => { - if (a.online !== b.online) return a.online ? -1 : 1; - return a.name.localeCompare(b.name); - }); - m.redraw(); - }); - } - }); - }); - } - }); -} - const ChatRoomHeader = () => { return { view: (vnode) => { @@ -1040,40 +250,6 @@ const ChatRoomHeader = () => { }; }; -function scrollChatToBottom() { - setTimeout(() => { - const element = document.querySelector('.chat-hub-messages'); - if (element) { - element.scrollTop = element.scrollHeight; - } - }, 50); -} - -function pollHashStatus(localpath) { - rs.rsJsonApiRequest('/rsFiles/ExtraFileStatus', { localpath }, (data) => { - if (data && data.retval && data.info && data.info.hash && data.info.hash !== '0000000000000000000000000000000000000000') { - const info = data.info; - const sizeNum = info.size.xint64 || parseInt(info.size.xstr64) || info.size; - const fileLink = `${info.name} (${rs.formatBytes(sizeNum)})`; - - const textarea = document.querySelector('.chat-hub-textarea'); - if (textarea) { - const val = textarea.value; - textarea.value = val ? val + '\n' + fileLink : fileLink; - } - - ChatHubState.showAttachModal = false; - ChatHubState.isHashing = false; - ChatHubState.attachPath = ''; - m.redraw(); - } else { - if (ChatHubState.isHashing) { - setTimeout(() => pollHashStatus(localpath), 1000); - } - } - }); -} - const ChatConversationView = () => { function onDocClick(e) { if (ChatHubState.showEmojiPicker && !e.target.closest('.emoji-picker-wrapper')) { @@ -1136,7 +312,7 @@ const ChatConversationView = () => { }, '😊' ), - ChatHubState.showEmojiPicker && m(EmojiPicker), + ChatHubState.showEmojiPicker && m(chatEmoji.EmojiPicker), ]), m('textarea.chat-hub-textarea', { placeholder: canTalk ? 'Type a message... Press Enter to send' : 'Waiting for tunnel to be secured...', @@ -1193,24 +369,20 @@ const ChatConversationView = () => { m('h4', 'Attach File to Chat'), ]), m('p', 'Browse for a file or type the absolute path on your local system:'), - // Hidden native file input for browsing m('input#attach-file-picker[type=file]', { style: 'display:none', onchange: (e) => { const file = e.target.files && e.target.files[0]; if (file) { - // file.path is only available in Electron/desktop; browsers restrict full path const fullPath = file.path; const hasFullPath = fullPath && (fullPath.includes('/') || fullPath.includes('\\')) && fullPath !== file.name; if (hasFullPath) { ChatHubState.attachPath = fullPath; ChatHubState.attachBrowseHint = false; } else { - // Browser security: only the filename is available, not the full path ChatHubState.attachPath = file.name; ChatHubState.attachBrowseHint = true; } - // Reset the picker so the same file can be re-selected e.target.value = ''; ChatHubState.hashingError = ''; m.redraw(); @@ -1223,7 +395,7 @@ const ChatConversationView = () => { value: ChatHubState.attachPath, oninput: (e) => { ChatHubState.attachPath = e.target.value; - ChatHubState.attachBrowseHint = false; // user is editing manually, hint no longer relevant + ChatHubState.attachBrowseHint = false; }, disabled: ChatHubState.isHashing, }), @@ -1306,9 +478,8 @@ const ChatConversationView = () => { const gxsId = user.key; const name = user.name; - // Load details for avatar if not cached if (gxsId && ChatHubState.gxsDetails[gxsId] === undefined) { - ChatHubState.gxsDetails[gxsId] = null; // Mark as loading + ChatHubState.gxsDetails[gxsId] = null; rs.rsJsonApiRequest('/rsIdentity/getIdDetails', { id: gxsId }, (data) => { if (data && data.details) { ChatHubState.gxsDetails[gxsId] = data.details; @@ -1325,32 +496,31 @@ const ChatConversationView = () => { const isBanned = opinion === 0; if (isBanned) return null; - // Calculate status color and tooltip const now = Math.floor(Date.now() / 1000); const tLastAct = user.lastAct || 0; const isOwn = gxsId === rs.idToHex(ChatLobbyModel.currentLobby.gxs_id || ''); const isMuted = ChatHubState.mutedUsers && ChatHubState.mutedUsers.has(gxsId); - let statusColor = '#22c55e'; // active (green) + let statusColor = '#22c55e'; let statusTooltip = 'Active'; if (isMuted) { - statusColor = '#ef4444'; // muted (red) + statusColor = '#ef4444'; statusTooltip = 'Muted'; } else if (isOwn) { - statusColor = '#3ba4d7'; // own identity (blue) + statusColor = '#3ba4d7'; statusTooltip = 'You'; } else if (tLastAct + 600 < now) { - statusColor = '#cbd5e1'; // inactive > 10 mins (grey) + statusColor = '#cbd5e1'; statusTooltip = 'Inactive'; } else if (tLastAct + 300 < now) { - statusColor = '#eab308'; // away > 5 mins (yellow) + statusColor = '#eab308'; statusTooltip = 'Away'; } return m('.user', { onmouseenter: (e) => { - if (ChatHubState.activeMenu) return; // skip tooltip if menu is open + if (ChatHubState.activeMenu) return; const rect = e.currentTarget.getBoundingClientRect(); const rightbar = document.querySelector('.chat-hub-rightbar'); if (rightbar) { @@ -1365,7 +535,7 @@ const ChatConversationView = () => { onclick: (e) => { e.preventDefault(); e.stopPropagation(); - ChatHubState.hoveredUser = null; // hide tooltip + ChatHubState.hoveredUser = null; const rect = e.currentTarget.getBoundingClientRect(); const rightbar = document.querySelector('.chat-hub-rightbar'); @@ -1389,7 +559,7 @@ const ChatConversationView = () => { oncontextmenu: (e) => { e.preventDefault(); e.stopPropagation(); - ChatHubState.hoveredUser = null; // hide tooltip + ChatHubState.hoveredUser = null; const rect = e.currentTarget.getBoundingClientRect(); const rightbar = document.querySelector('.chat-hub-rightbar'); @@ -1800,7 +970,6 @@ const Layout = { } window.addEventListener('click', Layout.dismissMenu); - // Load own identities for room creation peopleUtil.ownIds((ids) => { ChatHubState.ownGxsIdentities = ids || []; if (ChatHubState.ownGxsIdentities.length > 0) { @@ -2055,8 +1224,8 @@ const Layout = { const isPublic = ChatHubState.newRoomPublic; const isSigned = ChatHubState.newRoomSigned; let flags = 0; - if (isPublic) flags |= 4; // RS_CHAT_LOBBY_FLAGS_PUBLIC - if (isSigned) flags |= 8; // RS_CHAT_LOBBY_FLAGS_SIGNED_ONLY + if (isPublic) flags |= 4; + if (isSigned) flags |= 8; rs.rsJsonApiRequest('/rsChats/createChatLobby', { lobby_name: name, @@ -2071,7 +1240,6 @@ const Layout = { ChatHubState.newRoomTopic = ''; ChatHubState.newRoomSigned = false; ChatHubState.createRoomError = ''; - // Refresh rooms list ChatRoomsModel.loadSubscribedRooms(); m.redraw(); } else { diff --git a/webui-src/app/chat/chat_emoji.js b/webui-src/app/chat/chat_emoji.js new file mode 100644 index 0000000..4d94d98 --- /dev/null +++ b/webui-src/app/chat/chat_emoji.js @@ -0,0 +1,149 @@ +const m = require('mithril'); + +const EMOJI_CATEGORIES = ['Smileys', 'People', 'Animals', 'Food', 'Travel', 'Activities', 'Objects', 'Symbols']; +const EMOJI_ICONS = { + Smileys: '😊', People: '👥', Animals: '🐾', Food: '🍎', + Travel: '✈️', Activities: '⚽', Objects: '💡', Symbols: '❤️', +}; +const EMOJI_DATA = { + Smileys: [ + '😀','😁','😂','🤣','😃','😄','😅','😆','😉','😊','😋','😎','😍','😘','🥰','😗','😙','😚', + '🙂','🤗','🤩','🤔','🤨','😐','😑','😶','🙄','😏','😣','😥','😮','🤐','😯','😪','😫','🥱', + '😴','😌','😛','😜','😝','🤤','😒','😓','😔','😕','🙃','🤑','😲','☹️','🙁','😖','😞','😟', + '😤','😢','😭','😦','😧','😨','😩','🤯','😬','😰','😱','🥵','🥶','😳','🤪','😵','😡','😠', + '🤬','😷','🤒','🤕','🤢','🤮','🤧','🥴','😇','🥳','🥺','🤠','🤡','🤥','🤫','🤭','🧐','🤓', + '😈','👿','👹','👺','💀','☠️','👻','👽','👾','🤖','😺','😸','😹','😻','😼','😽','🙀','😿','😾', + ], + People: [ + '👋','🤚','🖐️','✋','🖖','👌','🤌','🤏','✌️','🤞','🤟','🤘','🤙','👈','👉','👆','🖕','👇', + '☝️','👍','👎','✊','👊','🤛','🤜','👏','🙌','👐','🤲','🤝','🙏','✍️','💅','🤳','💪','🦾', + '🦿','🦵','🦶','👂','🦻','👃','🫀','🫁','🧠','🦷','🦴','👀','👁️','👅','👄','🫦','👶','🧒', + '👦','👧','🧑','👱','👨','🧔','👩','🧓','👴','👵','🙍','🙎','🙅','🙆','💁','🙋','🧏','🙇', + '🤦','🤷','👮','🕵️','💂','🥷','👷','🫅','🤴','👸','👲','🧕','🤵','👰','🤰','🫃','🫄','🤱', + '👼','🎅','🤶','🧑‍🎄','🦸','🦹','🧙','🧝','🧛','🧟','🧞','🧜','🧚','🧑‍🤝‍🧑','👫','👬','👭','💏','💑','👪', + ], + Animals: [ + '🐶','🐱','🐭','🐹','🐰','🦊','🐻','🐼','🐻‍❄️','🐨','🐯','🦁','🐮','🐷','🐸','🐵','🙈','🙉', + '🙊','🐒','🦆','🦅','🦉','🦇','🐝','🪱','🐛','🦋','🐌','🐞','🐜','🪲','🦗','🪳','🕷️','🦂', + '🐢','🐍','🦎','🦖','🦕','🐙','🦑','🦐','🦞','🦀','🐡','🐠','🐟','🐬','🐳','🐋','🦈','🦭', + '🐊','🐅','🐆','🦓','🦍','🦧','🦣','🐘','🦛','🦏','🐪','🐫','🦒','🦘','🦬','🐃','🐂','🐄', + '🐎','🐖','🐏','🐑','🦙','🐐','🦌','🐕','🐩','🦮','🐕‍🦺','🐈','🐈‍⬛','🐓','🦃','🦤','🦚','🦜', + '🦢','🦩','🕊️','🐇','🦝','🦨','🦡','🦫','🦦','🦥','🐁','🐀','🐿️','🦔','🐾','🐉','🐲','🌵', + ], + Food: [ + '🍎','🍊','🍋','🍌','🍍','🥭','🍓','🍒','🍑','🥝','🍅','🥥','🥑','🍆','🥔','🥕','🌽','🌶️', + '🫑','🥒','🥬','🥦','🧄','🧅','🍄','🥜','🌰','🍞','🥐','🥖','🫓','🥨','🧀','🥚','🍳','🧈', + '🥞','🧇','🥓','🥩','🍗','🍖','🦴','🌭','🍔','🍟','🍕','🫔','🌮','🌯','🥙','🧆','🥚','🍱', + '🍘','🍙','🍚','🍛','🍜','🍝','🍠','🍢','🍣','🍤','🍥','🥮','🍡','🥟','🥠','🥡','🦪','🍦', + '🍧','🍨','🍩','🍪','🎂','🍰','🧁','🥧','🍫','🍬','🍭','🍮','🍯','🍼','🥛','☕','🫖','🍵', + '🧃','🥤','🧋','🍶','🍺','🍻','🥂','🍷','🥃','🍸','🍹','🧉','🍾','🧊','🥄','🍴','🍽️','🥢', + ], + Travel: [ + '🚗','🚕','🚙','🚌','🚎','🏎️','🚓','🚑','🚒','🚐','🛻','🚚','🚛','🚜','🦯','🦽','🦼','🛺', + '🚲','🛴','🛵','🏍️','🛺','🚨','🚔','🚍','🚘','🚖','🚡','🚠','🚟','🚃','🚋','🚞','🚝','🚄', + '🚅','🚈','🚂','🚆','🚇','🚊','🚉','✈️','🛫','🛬','🛩️','💺','🛸','🚁','🛶','⛵','🚤','🛥️', + '🛳️','⛴️','🚢','⚓','🗺️','🧭','🏔️','⛰️','🌋','🗻','🏕️','🏖️','🏜️','🏝️','🏞️','🏟️','🏛️','🏗️', + '🧱','🪨','🪵','🛖','🏘️','🏚️','🏠','🏡','🏢','🏣','🏤','🏥','🏦','🏨','🏩','🏪','🏫','🏬', + '🏭','🏯','🏰','💒','🗼','🗽','⛪','🕌','🛕','🕍','⛩️','🕋','⛲','⛺','🌁','🌃','🏙️','🌄', + ], + Activities: [ + '⚽','🏀','🏈','⚾','🥎','🎾','🏐','🏉','🥏','🎱','🏓','🏸','🏒','🏑','🥍','🏏','🪃','🥅', + '⛳','🪁','🛝','🏹','🎣','🤿','🥊','🥋','🎽','🛹','🛷','⛸️','🥌','🎿','⛷️','🏂','🪂','🏋️', + '🤼','🤸','⛹️','🤺','🏇','🧘','🏄','🏊','🤽','🚣','🧗','🚵','🚴','🏆','🥇','🥈','🥉','🏅', + '🎖️','🏵️','🎗️','🎫','🎟️','🎪','🤹','🎭','🩰','🎨','🖼️','🎰','🎲','🧩','🪄','🎯','🪅','🎮', + '🕹️','🎳','🎻','🎷','🥁','🪘','🎺','🎸','🪗','🎹','🎵','🎶','🎼','🎤','🎧','📻','🎙️','🎚️', + '🎬','📽️','🎞️','📱','📲','☎️','📞','📟','📠','🔋','🪫','🔌','💡','🔦','🕯️','💸','💵','🪙', + ], + Objects: [ + '⌚','📱','📲','💻','⌨️','🖥️','🖨️','🖱️','🖲️','💾','💿','📀','🧮','📷','📸','📹','🎥','📽️', + '📞','☎️','📟','📠','📺','📻','🧭','⏱️','⏲️','⏰','🕰️','⌛','⏳','📡','🔋','🪫','🔌','💡', + '🔦','🕯️','🪔','🧱','💰','💴','💵','💶','💷','💸','💳','🪙','💹','✉️','📧','📨','📩','📤', + '📥','📦','📫','📪','📬','📭','📮','🗳️','✏️','✒️','🖊️','🖋️','📝','📁','📂','🗂️','📅','📆', + '🗒️','🗓️','📇','📈','📉','📊','📋','📌','📍','🗺️','📏','📐','✂️','🗃️','🗄️','🗑️','🔒','🔓', + '🔏','🔐','🔑','🗝️','🔨','🪓','⛏️','⚒️','🛠️','🗡️','⚔️','🔫','🪃','🏹','🛡️','🪚','🔧','🪛', + ], + Symbols: [ + '❤️','🧡','💛','💚','💙','💜','🖤','🤍','🤎','💔','❣️','💕','💞','💓','💗','💖','💘','💝', + '💟','☮️','✝️','☪️','🕉️','☸️','✡️','🔯','🕎','☯️','☦️','🛐','⛎','♈','♉','♊','♋','♌', + '♍','♎','♏','♐','♑','♒','♓','🆔','⚛️','🉑','☢️','☣️','📴','📳','🈶','🈚','🈸','🈺', + '🈷️','✴️','🆚','💮','🉐','㊙️','㊗️','🈴','🈵','🈹','🈲','🅰️','🅱️','🆎','🆑','🅾️','🆘', + '❌','⭕','🛑','⛔','📛','🚫','💯','💢','♨️','🚷','🚯','🚳','🚱','🔞','📵','🚭','❗','❕', + '❓','❔','‼️','⁉️','🔅','🔆','📶','🛜','📳','📴','🔱','📛','🔰','♻️','✅','🈯','💹','❎', + '🌐','💠','Ⓜ️','🌀','💤','🏧','🚾','♿','🅿️','🛗','🈳','🈹','🚰','🔤','🔡','🔠','🆖','🆗', + '🆙','🆒','🆕','🆓','🔟','📊','🔣','✔️','☑️','🔘','🔲','🔳','⬛','⬜','◼️','◻️','◾','◽', + '▪️','▫️','🔶','🔷','🔸','🔹','🔺','🔻','💠','🔘','🔲','🔳','🏁','🚩','🎌','🏴','🏳️','⭐', + '🌟','💫','✨','🌈','☀️','🌤️','⛅','🌥️','☁️','🌦️','🌧️','⛈️','🌩️','🌨️','❄️','☃️','⛄','🌬️', + ], +}; + +function insertEmojiIntoTextarea(emoji) { + const textarea = document.querySelector('.chat-hub-textarea'); + if (!textarea) return; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const before = textarea.value.substring(0, start); + const after = textarea.value.substring(end); + textarea.value = before + emoji + after; + const newPos = start + emoji.length; + textarea.selectionStart = newPos; + textarea.selectionEnd = newPos; + textarea.focus(); +} + +const EmojiPicker = () => ({ + view: () => { + const search = ChatHubState.emojiSearch.toLowerCase(); + const cat = ChatHubState.emojiCategory; + let emojis; + if (search) { + emojis = Object.values(EMOJI_DATA).flat(); + } else { + emojis = EMOJI_DATA[cat] || []; + } + return m('.emoji-picker', [ + m('.emoji-search-row', [ + m('i.fas.fa-search.emoji-search-icon'), + m('input.emoji-search-input[type=text][placeholder=Search emoji...]', { + value: ChatHubState.emojiSearch, + oninput: (e) => { ChatHubState.emojiSearch = e.target.value; }, + }), + ChatHubState.emojiSearch && m('button.emoji-search-clear', { + onclick: () => { ChatHubState.emojiSearch = ''; }, + }, m('i.fas.fa-times')), + ]), + !search && m('.emoji-categories', EMOJI_CATEGORIES.map(c => + m('button.emoji-cat-btn' + (c === cat ? '.active' : ''), { + title: c, + onclick: () => { ChatHubState.emojiCategory = c; }, + }, EMOJI_ICONS[c]) + )), + m('.emoji-grid', + emojis.map(e => + m('button.emoji-btn', { + onclick: () => { + insertEmojiIntoTextarea(e); + ChatHubState.showEmojiPicker = false; + m.redraw(); + }, + }, e) + ) + ), + ]); + }, +}); + +// Lazy reference set by chat.js to avoid circular dependency +let ChatHubState = null; + +function setDependencies(deps) { + ChatHubState = deps.ChatHubState; +} + +module.exports = { + EMOJI_CATEGORIES, + EMOJI_ICONS, + EMOJI_DATA, + insertEmojiIntoTextarea, + EmojiPicker, + setDependencies, +}; diff --git a/webui-src/app/chat/chat_state.js b/webui-src/app/chat/chat_state.js new file mode 100644 index 0000000..54a4e62 --- /dev/null +++ b/webui-src/app/chat/chat_state.js @@ -0,0 +1,695 @@ +const m = require('mithril'); +const rs = require('rswebui'); +const peopleUtil = require('people/people_util'); +const people = require('people/people'); + +// **************** utility functions ******************** + +function get64Num(val) { + if (!val) return 0; + if (typeof val === 'object') { + return val.xint64 || parseInt(val.xstr64) || 0; + } + return Number(val) || 0; +} + +function loadLobbyDetails(id, apply) { + rs.rsJsonApiRequest( + '/rsChats/getChatLobbyInfo', + { + id: { xstr64: id }, + }, + (detail, success) => { + if (success && detail.retval) { + detail.info.chatType = 3; // LOBBY + apply(detail.info); + } else { + apply(null); + } + }, + true + ); +} + +function loadDistantChatDetails(pid, apply) { + rs.rsJsonApiRequest( + '/rsChats/getDistantChatStatus', + { + pid: pid, + }, + (detail, success) => { + if (success && detail.retval) { + const info = detail.info; + info.chatType = 2; // DISTANT (matches TYPE_PRIVATE_DISTANT in rschats.h) + info.lobby_name = rs.userList.username(info.to_id) || 'Distant Chat ' + pid; + info.lobby_topic = 'Private Encrypted Chat'; + info.gxs_id = info.own_id; + info.lobby_id = pid; // Distant IDs are 128-bit hex strings, NO xstr64 wrapper + apply(info); + } else { + apply(null); + } + }, + true + ); +} + +function sortLobbies(lobbies) { + if (lobbies !== undefined && lobbies !== null) { + const list = [...lobbies]; + list.sort((a, b) => a.lobby_name.localeCompare(b.lobby_name)); + return list; + } + return []; +} + +function getNicknameColor(id, name) { + const hashString = id && id !== '00000000000000000000000000000000' ? id : (name || ''); + let hash = 0; + for (let i = 0; i < hashString.length; i++) { + hash = hashString.charCodeAt(i) + ((hash << 5) - hash); + } + const hue = Math.abs(hash) % 360; + return `hsl(${hue}, 75%, 35%)`; +} + +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.'; + } +} + +/** + * Wraps emoji characters in a span so CSS can size them independently. + */ +function renderTextWithEmoji(text) { + if (!text) return ''; + const emojiRegex = /(?:\p{Emoji_Presentation}|\p{Extended_Pictographic})(?:[\u{1F3FB}-\u{1F3FF}])?(?:\u{FE0F})?(?:\u{20E3})?(?:(?:\u{200D}(?:\p{Emoji_Presentation}|\p{Extended_Pictographic})(?:[\u{1F3FB}-\u{1F3FF}])?(?:\u{FE0F})?)*)/gu; + const parts = []; + let last = 0; + let match; + // eslint-disable-next-line no-cond-assign + while ((match = emojiRegex.exec(text)) !== null) { + if (match[0].length === 0) { emojiRegex.lastIndex++; continue; } + if (match.index > last) parts.push(text.slice(last, match.index)); + parts.push(m('span.chat-emoji', match[0])); + last = match.index + match[0].length; + } + if (last < text.length) parts.push(text.slice(last)); + return parts.length > 0 ? parts : text; +} + +function getSafeAvatar(details) { + if ( + details && + details.mAvatar && + details.mAvatar.mData && + details.mAvatar.mData.base64 !== '' + ) { + return details.mAvatar; + } + return undefined; +} + +// ***************************** models *********************************** + +const MobileState = { + showLobbies: false, + showUsers: false, + toggleLobbies() { + this.showLobbies = !this.showLobbies; + this.showUsers = false; + }, + toggleUsers() { + this.showUsers = !this.showUsers; + this.showLobbies = false; + }, + closeAll() { + this.showLobbies = false; + this.showUsers = false; + }, +}; + +const ChatRoomsModel = { + allRooms: [], + knownSubscrIds: [], + subscribedRooms: {}, + loadPublicRooms() { + rs.rsJsonApiRequest( + '/rsChats/getListOfNearbyChatLobbies', + {}, + (data) => { + if (data && data.public_lobbies) { + const seen = new Set(); + const uniqueLobbies = data.public_lobbies.filter((lobby) => { + const id = rs.idToHex(lobby.lobby_id); + if (seen.has(id)) return false; + seen.add(id); + return true; + }); + ChatRoomsModel.allRooms = sortLobbies(uniqueLobbies); + } else { + ChatRoomsModel.allRooms = []; + } + } + ); + }, + loadSubscribedRooms(after = null) { + rs.rsJsonApiRequest( + '/rsChats/getChatLobbyList', + {}, + (data) => { + if (data && data.cl_list) { + const ids = [...new Set(data.cl_list.map((lid) => rs.idToHex(lid)))]; + ChatRoomsModel.knownSubscrIds = ids; + + Object.keys(ChatRoomsModel.subscribedRooms).forEach((id) => { + if (!ids.includes(id)) { + delete ChatRoomsModel.subscribedRooms[id]; + } + }); + + if (ids.length === 0) { + ChatRoomsModel.loadPublicRooms(); + if (after != null) after(); + m.redraw(); + return; + } + + let count = 0; + ids.forEach((id) => + loadLobbyDetails(id, (info) => { + if (info) { + ChatRoomsModel.subscribedRooms[id] = info; + } + count++; + if (count === ids.length) { + ChatRoomsModel.loadPublicRooms(); + if (after != null) { + after(); + } + m.redraw(); + } + }) + ); + } else { + ChatRoomsModel.loadPublicRooms(); + } + } + ); + }, + subscribed(info) { + return this.knownSubscrIds.includes(rs.idToHex(info.lobby_id)); + }, +}; + +/** + * Message displays a single Chat-Message + * currently removes formatting and in consequence inline links + */ +const Message = () => { + return { + view: (vnode) => { + const msg = vnode.attrs; + const datetime = new Date(msg.sendTime * 1000).toLocaleTimeString(); + 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( + '.message.incoming', + [ + m('span.datetime', datetime), + m('span.username', 'Chat status'), + m('.messagetext', { + style: { + backgroundColor: bgColor, + border: `1px ${borderStyle} ${borderColor}`, + color: textColor, + padding: '0.5rem 0.75rem', + borderRadius: '0.375rem', + display: 'inline-block', + marginTop: '0.25rem', + } + }, text) + ] + ); + } + const rawGxsId = msg.lobby_peer_gxs_id || msg.peerId; + let gxsId = rs.idToHex(rawGxsId); + + const isZero = (id) => !id || id === '00000000000000000000000000000000'; + if (isZero(gxsId)) { + const lobby = ChatLobbyModel.currentLobby; + if (lobby && (lobby.chatType === 1 || lobby.chatType === 2)) { + gxsId = msg.incoming ? rs.idToHex(lobby.to_id || lobby.peer_id || lobby.distant_chat_id) : rs.idToHex(lobby.own_id || lobby.gxs_id); + } + } + + const isMuted = ChatHubState.mutedUsers && ChatHubState.mutedUsers.has(gxsId); + const details = ChatHubState.gxsDetails[gxsId]; + const opinion = details && details.mReputation ? details.mReputation.mOwnOpinion : 1; + const isBanned = opinion === 0; + + if (isMuted || isBanned) { + return null; + } + + let username = rs.userList.username(gxsId) || msg.peerName || '???'; + if (username === gxsId && msg.peerName) { + username = msg.peerName; + } + if (username === gxsId && gxsId && gxsId.length > 12) { + username = gxsId.substring(0, 8) + '...'; + } + const text = (msg.msg || msg.message || '') + .replaceAll('
', '\n') + .replace(new RegExp('|<[^>]*>', 'gm'), ''); + + const chatType = ChatLobbyModel.currentLobby && ChatLobbyModel.currentLobby.chatType; + const isRoom = chatType === 3; + + if (isRoom) { + const nickColor = getNicknameColor(gxsId, username); + return m( + '.message.compact', + m('span.datetime', datetime), + m('span.username', { style: { color: nickColor } }, username + ':'), + m('span.messagetext', renderTextWithEmoji(text)) + ); + } + + return m( + '.message' + (msg.incoming ? '.incoming' : '.outgoing'), + m('span.datetime', datetime), + m('span.username', username), + m('span.messagetext', renderTextWithEmoji(text)) + ); + }, + }; +}; + +const ChatLobbyModel = { + currentLobby: { + lobby_name: '...', + }, + lobby_user: '...', + isSubscribed: false, + messages: [], + users: [], + messageKeys: new Set(), + lastLobbyId: null, + distantChatStatus: null, + statusPollInterval: null, + + pollDistantChatStatus() { + if (!this.currentLobby || this.currentLobby.chatType !== 2) return; + rs.rsJsonApiRequest( + '/rsChats/getDistantChatStatus', + { + pid: this.currentLobby.lobby_id, + }, + (detail, success) => { + if (success && detail.retval) { + const oldStatus = this.distantChatStatus ? this.distantChatStatus.status : null; + this.distantChatStatus = detail.info; + + if (oldStatus !== null && oldStatus !== detail.info.status) { + if (detail.info.status === 2) { + this.addMessages([{ + chat_id: this.chatId(), + isSystem: true, + msg: 'Tunnel is secured. You can talk!', + sendTime: Math.floor(Date.now() / 1000) + }]); + } else if (detail.info.status === 3) { + this.addMessages([{ + chat_id: this.chatId(), + isSystem: true, + msg: 'Your partner closed the conversation.', + sendTime: Math.floor(Date.now() / 1000) + }]); + } + } + m.redraw(); + } + } + ); + }, + + startStatusPolling() { + this.stopStatusPolling(); + this.pollDistantChatStatus(); + this.statusPollInterval = setInterval(() => this.pollDistantChatStatus(), 3000); + }, + + stopStatusPolling() { + if (this.statusPollInterval) { + clearInterval(this.statusPollInterval); + this.statusPollInterval = null; + } + this.distantChatStatus = null; + }, + + getMessageKey(msg) { + if (msg.msgId && msg.msgId !== 0) return 'id_' + msg.msgId; + const text = msg.msg || msg.message || ''; + return 't_' + msg.sendTime + '_' + text.substring(0, 32); + }, + + addMessages(newMsgs, scroll = false) { + let added = false; + newMsgs.forEach((msg) => { + const key = this.getMessageKey(msg); + if (!this.messageKeys.has(key)) { + const text = msg.msg || msg.message || ''; + const isNearDuplicate = this.messages.some((existingMsg) => { + const eAttrs = existingMsg.attrs; + const eText = eAttrs.msg || eAttrs.message || ''; + return ( + eText === text && + Math.abs(eAttrs.sendTime - msg.sendTime) < 5 + ); + }); + + if (!isNearDuplicate) { + this.messageKeys.add(key); + this.messages.push(m(Message, msg)); + added = true; + } + } + }); + + if (added) { + this.messages.sort((a, b) => a.attrs.sendTime - b.attrs.sendTime); + m.redraw(); + if (scroll) { + setTimeout(() => { + const element = document.querySelector('.messages'); + if (element) { + element.scrollTop = element.scrollHeight; + } + }, 100); + } + } + }, + + loadHistory(id, type) { + const chatPeerId = { + broadcast_status_peer_id: '00000000000000000000000000000000', + type: type, + peer_id: '00000000000000000000000000000000', + distant_chat_id: '00000000000000000000000000000000', + lobby_id: { xstr64: '0' }, + }; + + if (type === 3) chatPeerId.lobby_id.xstr64 = id; + else if (type === 2) chatPeerId.distant_chat_id = id; + else if (type === 1) chatPeerId.peer_id = id; + + rs.rsJsonApiRequest( + '/rsHistory/getMessages', + { + chatPeerId: chatPeerId, + loadCount: 20, + }, + (data, success) => { + if (success && data.msgs) { + this.addMessages(data.msgs); + } + } + ); + }, + setupAction: (lobbyId, nick) => { }, + setIdentity(lobbyId, nick) { + rs.rsJsonApiRequest( + '/rsChats/setIdentityForChatLobby', + { + lobby_id: { xstr64: lobbyId }, + nick: nick, + }, + () => m.route.set('/chat/:lobby', { lobby: lobbyId }), + true + ); + }, + enterPublicLobby(lobbyId, nick) { + rs.rsJsonApiRequest( + '/rsChats/joinVisibleChatLobby', + { + lobby_id: { xstr64: lobbyId }, + own_id: nick, + }, + () => { + loadLobbyDetails(lobbyId, (info) => { + ChatRoomsModel.subscribedRooms[lobbyId] = info; + ChatRoomsModel.loadSubscribedRooms(() => { + m.route.set('/chat/:lobby', { lobby: rs.idToHex(info.lobby_id) }); + }); + }); + }, + true + ); + }, + unsubscribeChatLobby(lobbyId, follow) { + rs.rsJsonApiRequest( + '/rsChats/unsubscribeChatLobby', + { + lobby_id: { xstr64: lobbyId }, + }, + (data, success) => { + if (success) { + ChatRoomsModel.loadSubscribedRooms(follow); + } + }, + true + ); + }, + chatId() { + const type = (this.currentLobby && this.currentLobby.chatType) || 3; + const id = this.lastLobbyId || m.route.param('lobby'); + const cid = { + broadcast_status_peer_id: '00000000000000000000000000000000', + type: type, + peer_id: '00000000000000000000000000000000', + distant_chat_id: '00000000000000000000000000000000', + lobby_id: { xstr64: '0' }, + }; + if (type === 3) cid.lobby_id.xstr64 = id; + else if (type === 2) cid.distant_chat_id = id; + else if (type === 1) cid.peer_id = id; + return cid; + }, + loadLobby(currentlobbyid) { + this.stopStatusPolling(); + this.lastLobbyId = currentlobbyid; + + const finishLoad = (detail) => { + this.setupAction = this.setIdentity; + this.currentLobby = detail; + this.isSubscribed = true; + this.lobby_user = rs.userList.username(detail.gxs_id) || '???'; + + this.messages = []; + this.messageKeys.clear(); + + this.loadHistory(currentlobbyid, detail.chatType); + + const cid = this.chatId(); + rs.events[15].chatMessages(cid, rs.events[15], (l) => { + this.addMessages(l); + }); + + rs.events[15].notify = (chatMessage) => { + const msgCid = chatMessage.chat_id; + let msgId; + + if (msgCid.type === 3) { + msgId = rs.idToHex(msgCid.lobby_id); + } else if (msgCid.type === 2) { + msgId = rs.idToHex(msgCid.distant_chat_id); + } else if (msgCid.type === 1) { + msgId = rs.idToHex(msgCid.peer_id); + } else { + msgId = rs.idToHex(msgCid); + } + + if (msgId === currentlobbyid) { + this.addMessages([chatMessage]); + } + }; + + let list = []; + if (detail.gxs_ids) { + if (Array.isArray(detail.gxs_ids)) { + list = detail.gxs_ids.map((u) => { + const key = u.key; + return { key, name: rs.userList.username(key) || key, lastAct: get64Num(u.value) }; + }); + } else if (typeof detail.gxs_ids === 'object') { + list = Object.keys(detail.gxs_ids).map((key) => { + return { key, name: rs.userList.username(key) || key, lastAct: get64Num(detail.gxs_ids[key]) }; + }); + } + } + + const ownId = detail.gxs_id; + if (ownId && ownId !== '00000000000000000000000000000000') { + const hasOwn = list.some((u) => u.key === ownId); + if (!hasOwn) { + list.push({ + key: ownId, + name: rs.userList.username(ownId) || ownId, + lastAct: Math.floor(Date.now() / 1000) + }); + } + } + + if (list.length === 0) { + list = [{ key: ownId || '', name: rs.userList.username(ownId) || detail.lobby_name || '???', lastAct: Math.floor(Date.now() / 1000) }]; + } + + list.sort((a, b) => a.name.localeCompare(b.name)); + this.users = list; + + if (detail.chatType === 2) { + this.startStatusPolling(); + } + + m.redraw(); + }; + + loadLobbyDetails(currentlobbyid, (detail) => { + if (detail) { + finishLoad(detail); + } else { + loadDistantChatDetails(currentlobbyid, (dDetail) => { + if (dDetail) { + finishLoad(dDetail); + } + }); + } + }); + }, + loadPublicLobby(currentlobbyid) { + this.setupAction = this.enterPublicLobby; + this.isSubscribed = false; + ChatRoomsModel.allRooms.forEach((it) => { + if (rs.idToHex(it.lobby_id) === currentlobbyid) { + this.currentLobby = it; + this.lobby_user = '???'; + this.lobbyid = currentlobbyid; + } + }); + this.users = []; + }, + sendMessage(msg, onsuccess) { + const cid = this.chatId(); + const echoMsg = { + chat_id: cid, + msg: msg, + sendTime: Math.floor(Date.now() / 1000), + lobby_peer_gxs_id: this.currentLobby.gxs_id, + }; + this.addMessages([echoMsg], true); + + rs.rsJsonApiRequest( + '/rsChats/sendChat', + { + id: cid, + msg: msg, + }, + (data, success) => { + if (success) { + onsuccess(); + } else { + console.error('[RS] Failed to send chat message'); + onsuccess(); + } + } + ); + }, + selected(info, selName, defaultName) { + const currid = rs.idToHex(ChatLobbyModel.currentLobby.lobby_id || { xstr64: m.route.param('lobby') }); + return (rs.idToHex(info.lobby_id) === currid ? selName : '') + defaultName; + }, + switchToEvent(info) { + return () => { + ChatLobbyModel.currentLobby = info; + m.route.set('/chat/:lobby', { lobby: rs.idToHex(info.lobby_id) }); + ChatLobbyModel.loadLobby(rs.idToHex(info.lobby_id)); + }; + }, + setupEvent(info) { + return () => { + m.route.set('/chat/:lobby/setup', { lobby: rs.idToHex(info.lobby_id) }); + ChatLobbyModel.loadPublicLobby(rs.idToHex(info.lobby_id)); + }; + }, +}; + +// ************************* Chat Hub State **************************** + +const ChatHubState = { + selectedRoomId: null, + selectedRoom: null, + selectedRoomType: null, + searchString: '', + ownProfile: { name: 'Loading...' }, + gxsDetails: {}, + hoveredUser: null, + mutedUsers: new Set(), + activeMenu: null, + showAttachModal: false, + attachPath: '', + attachBrowseHint: false, + isHashing: false, + hashingError: '', + showEmojiPicker: false, + emojiSearch: '', + emojiCategory: 'Smileys', + showCreateRoomModal: false, + newRoomName: '', + newRoomTopic: '', + newRoomIdentity: '', + newRoomPublic: true, + newRoomSigned: false, + ownGxsIdentities: [], + createRoomError: '', + userSortMethod: 'name', + showInviteModal: false, + friendsList: [], + selectedFriendsToInvite: new Set(), +}; + +module.exports = { + get64Num, + loadLobbyDetails, + loadDistantChatDetails, + sortLobbies, + getNicknameColor, + getStatusColor, + getStatusTooltip, + renderTextWithEmoji, + getSafeAvatar, + MobileState, + ChatRoomsModel, + Message, + ChatLobbyModel, + ChatHubState, +}; From 39975c960dbe8bcc739b8a7d5d56228de9990294 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:34:40 +0200 Subject: [PATCH 19/40] improved tor configs display --- webui-src/app/config/config_network.js | 236 ++++++++++++++++++------- 1 file changed, 170 insertions(+), 66 deletions(-) diff --git a/webui-src/app/config/config_network.js b/webui-src/app/config/config_network.js index 28fbdee..9f929bf 100644 --- a/webui-src/app/config/config_network.js +++ b/webui-src/app/config/config_network.js @@ -10,6 +10,10 @@ const SetNwMode = () => { 'Inverted: DHT only', 'Dark Net: None', ]; + const hiddenModes = [ + 'Discovery On (recommended)', + 'Discovery Off', + ]; let vsDisc = 0; let vsDht = 0; @@ -17,8 +21,41 @@ const SetNwMode = () => { let sslId = ''; let details = {}; + const updateSelectedMode = (isHiddenMode) => { + if (!details || details.vs_dht === undefined) return; + if (isHiddenMode) { + if (details.vs_disc === util.RS_VS_DISC_OFF) { + selectedMode = hiddenModes[1]; + } else { + selectedMode = hiddenModes[0]; + } + } else { + if ( + details.vs_dht === util.RS_VS_DHT_FULL && + details.vs_disc === util.RS_VS_DISC_FULL + ) { + selectedMode = networkModes[0]; + } else if ( + details.vs_dht === util.RS_VS_DHT_OFF && + details.vs_disc === util.RS_VS_DISC_FULL + ) { + selectedMode = networkModes[1]; + } else if ( + details.vs_dht === util.RS_VS_DHT_FULL && + details.vs_disc === util.RS_VS_DISC_OFF + ) { + selectedMode = networkModes[2]; + } else if ( + details.vs_dht === util.RS_VS_DHT_OFF && + details.vs_disc === util.RS_VS_DISC_OFF + ) { + selectedMode = networkModes[3]; + } + } + }; + return { - oninit: () => { + oninit: (vnode) => { rs.rsJsonApiRequest('/rsAccounts/getCurrentAccountId').then((res) => { if (res.body.retval) { sslId = res.body.id; @@ -27,57 +64,51 @@ const SetNwMode = () => { }).then((res) => { if (res.body.retval) { details = res.body.det; - if ( - details.vs_dht === util.RS_VS_DHT_FULL && - details.vs_disc === util.RS_VS_DISC_FULL - ) { - selectedMode = networkModes[0]; - } else if ( - details.vs_dht === util.RS_VS_DHT_OFF && - details.vs_disc === util.RS_VS_DISC_FULL - ) { - selectedMode = networkModes[1]; - } else if ( - details.vs_dht === util.RS_VS_DHT_FULL && - details.vs_disc === util.RS_VS_DISC_OFF - ) { - selectedMode = networkModes[2]; - } else if ( - details.vs_dht === util.RS_VS_DHT_OFF && - details.vs_disc === util.RS_VS_DISC_OFF - ) { - selectedMode = networkModes[3]; - } + updateSelectedMode(vnode.attrs && vnode.attrs.isHiddenMode); + m.redraw(); } }); } }); }, - view: () => { + onupdate: (vnode) => { + updateSelectedMode(vnode.attrs && vnode.attrs.isHiddenMode); + }, + view: (vnode) => { + const isHiddenMode = vnode.attrs && vnode.attrs.isHiddenMode; + const modes = isHiddenMode ? hiddenModes : networkModes; + return [ - m('p', 'Network mode:'), + m('p', isHiddenMode ? 'Discovery:' : 'Network mode:'), m( 'select', { value: selectedMode, onchange: (e) => { - selectedMode = networkModes[e.target.selectedIndex]; - if (e.target.selectedIndex === 0) { - // Public: DHT & Discovery - vsDisc = util.RS_VS_DISC_FULL; - vsDht = util.RS_VS_DHT_FULL; - } else if (e.target.selectedIndex === 1) { - // Private: Discovery only - vsDisc = util.RS_VS_DISC_FULL; - vsDht = util.RS_VS_DHT_OFF; - } else if (e.target.selectedIndex === 2) { - // Inverted: DHT only - vsDisc = util.RS_VS_DISC_OFF; - vsDht = util.RS_VS_DHT_FULL; - } else if (e.target.selectedIndex === 3) { - // Dark Net: None - vsDisc = util.RS_VS_DISC_OFF; - vsDht = util.RS_VS_DHT_OFF; + const idx = e.target.selectedIndex; + selectedMode = modes[idx]; + if (isHiddenMode) { + if (idx === 0) { + vsDisc = util.RS_VS_DISC_FULL; + vsDht = util.RS_VS_DHT_OFF; + } else if (idx === 1) { + vsDisc = util.RS_VS_DISC_OFF; + vsDht = util.RS_VS_DHT_OFF; + } + } else { + if (idx === 0) { + vsDisc = util.RS_VS_DISC_FULL; + vsDht = util.RS_VS_DHT_FULL; + } else if (idx === 1) { + vsDisc = util.RS_VS_DISC_FULL; + vsDht = util.RS_VS_DHT_OFF; + } else if (idx === 2) { + vsDisc = util.RS_VS_DISC_OFF; + vsDht = util.RS_VS_DHT_FULL; + } else if (idx === 3) { + vsDisc = util.RS_VS_DISC_OFF; + vsDht = util.RS_VS_DHT_OFF; + } } if ( details && @@ -92,7 +123,7 @@ const SetNwMode = () => { } }, }, - [networkModes.map((o) => m('option', { value: o }, o))] + [modes.map((o) => m('option', { value: o }, o))] ), ]; }, @@ -237,8 +268,8 @@ const displayLocalIPAddress = () => { }; const displayExternalIPAddress = () => { return { - view: ({ attrs: { details } }) => - details && [m('p', 'External Address: '), m('p', details.extAddr)], + view: ({ attrs: { details, isHiddenMode } }) => + details && [m('p', 'External Address: '), m('p', isHiddenMode ? 'Hidden - See Config' : details.extAddr)], }; }; @@ -289,6 +320,25 @@ const SetDynamicDNS = () => { }; }; +const checkPortReachable = (addr, port, timeoutMs = 600) => { + if (!addr || !port) return Promise.resolve(false); + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), timeoutMs); + return fetch(`http://${addr}:${port}`, { + mode: 'no-cors', + signal: controller.signal, + cache: 'no-store', + }) + .then(() => { + clearTimeout(timer); + return true; + }) + .catch((err) => { + clearTimeout(timer); + return err.name === 'AbortError'; + }); +}; + const SetSocksProxy = () => { const socksProxyObj = { tor: {}, @@ -296,14 +346,13 @@ const SetSocksProxy = () => { }; const fetchOutgoing = () => { Object.keys(socksProxyObj).forEach((proxyItem) => { - fetch(`http://${socksProxyObj[proxyItem].addr}:${socksProxyObj[proxyItem].port}`) - .then(() => { - socksProxyObj[proxyItem].outgoing = true; + const item = socksProxyObj[proxyItem]; + if (item.addr && item.port) { + checkPortReachable(item.addr, item.port).then((isReachable) => { + item.outgoing = isReachable; m.redraw(); - }) - .catch(() => { - socksProxyObj[proxyItem].outgoing = false; }); + } }); }; const handleProxyChange = (proxyItem) => { @@ -328,9 +377,9 @@ const SetSocksProxy = () => { }); }, view: () => - m('.proxy-server-container', [ + m('.proxy-server', [ m( - 'p.proxy-description', + 'p', 'Configure your TOR and I2P SOCKS proxy here. It will allow you to also connect to hidden nodes.' ), m('.proxy-rows-container', @@ -338,6 +387,7 @@ const SetSocksProxy = () => { const isTor = proxyItem === 'tor'; const labelText = isTor ? 'TOR Socks Proxy:' : 'I2P Socks Proxy:'; const outgoingText = isTor ? 'TOR outgoing' : 'I2P outgoing'; + const notEnabledText = isTor ? 'Tor proxy is not enabled' : 'I2P proxy is not enabled'; const isOutgoing = socksProxyObj[proxyItem].outgoing; return m('.proxy-row', [ m('label.proxy-label', labelText), @@ -357,6 +407,7 @@ const SetSocksProxy = () => { style: { backgroundColor: isOutgoing ? '#22c55e' : '#808080', }, + title: isOutgoing ? 'Proxy seems to work.' : notEnabledText, }), m( 'span.proxy-status-text', @@ -370,8 +421,40 @@ const SetSocksProxy = () => { }; }; +const displayHiddenServiceInfo = () => { + return { + view: ({ attrs: { details } }) => + details && details.hiddenNodeAddress && + m('.proxy-server', [ + m('p.proxy-description', details.hiddenType === 4 + ? 'I2P has been automatically configured by Retroshare. You shouldn\'t need to change anything here.' + : 'Tor has been automatically configured by Retroshare. You shouldn\'t need to change anything here.' + ), + m('hr'), + m('.proxy-row', [ + m('label.proxy-label', 'Local Address:'), + m('span', details.localAddr || '127.0.0.1'), + ]), + m('.proxy-row', [ + m('label.proxy-label', details.hiddenType === 4 ? 'I2P Address:' : 'Onion Address:'), + m('span', details.hiddenNodeAddress), + ]), + details.hiddenNodePort && m('.proxy-row', [ + m('label.proxy-label', 'Service Port:'), + m('span', String(details.hiddenNodePort)), + ]), + m('.proxy-row', [ + m('label.proxy-label', 'Local Port:'), + m('span', String(details.localPort)), + ]), + ]), + }; +}; + const Component = () => { let details; + let isHiddenMode = false; + return { oninit: () => { rs.rsJsonApiRequest('/rsAccounts/getCurrentAccountId').then((res) => { @@ -381,28 +464,49 @@ const Component = () => { }).then((res) => { if (res.body.retval) { details = res.body.det; + isHiddenMode = Boolean( + details && ( + details.hiddenType === util.RS_HIDDEN_TYPE_TOR || + details.hiddenType === util.RS_HIDDEN_TYPE_I2P || + details.extAddr === 'Hidden' + ) + ); + m.redraw(); } }); } }); }, view: () => - m('.widget', [ - m('.widget__heading', m('h3', 'Network Configuration')), - m('.widget__body', [ - m('.grid-2col', [ - m(SetNwMode), - m(SetNAT), - m(displayLocalIPAddress, { details }), - m(displayExternalIPAddress, { details }), - m(SetDynamicDNS), - m(SetLimits), - m(SetOpMode), - m(displayIPAddresses, { details }), + m('.config-network', { style: 'display:flex; flex-direction:column; gap:0.5rem;' }, [ + m('.widget', [ + m('.widget__heading', m('h3', 'Network Configuration')), + m('.widget__body', [ + m('.grid-2col', [ + m(SetNwMode, { isHiddenMode }), + !isHiddenMode && m(SetNAT), + m(displayLocalIPAddress, { details }), + m(displayExternalIPAddress, { details, isHiddenMode }), + !isHiddenMode && m(SetDynamicDNS), + m(SetLimits), + !isHiddenMode && m(SetOpMode), + !isHiddenMode && m(displayIPAddresses, { details }), + ]), ]), - m('.widget__heading', m('h3', 'Hidden Service Configuration')), - m('.widget__body', [m(SetSocksProxy)]), ]), + m('.widget', [ + m('.widget__heading', m('h3', 'Hidden Service Configuration')), + m('.widget__body', [ + m(SetSocksProxy), + ]), + ]), + isHiddenMode && + m('.widget', [ + m('.widget__heading', m('h3', details && details.hiddenType === 4 ? 'Incoming I2P' : 'Incoming Tor')), + m('.widget__body', [ + m(displayHiddenServiceInfo, { details }), + ]), + ]), ]), }; }; From 44f2907e17967fb922e67851761e902aa833fedf Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:47:19 +0200 Subject: [PATCH 20/40] split network code --- webui-src/app/network/network.js | 574 +----------------- webui-src/app/network/network_chat_tab.js | 125 ++++ webui-src/app/network/network_details_tab.js | 151 +++++ webui-src/app/network/network_friends_list.js | 107 ++++ webui-src/app/network/network_state.js | 190 ++++++ 5 files changed, 589 insertions(+), 558 deletions(-) create mode 100644 webui-src/app/network/network_chat_tab.js create mode 100644 webui-src/app/network/network_details_tab.js create mode 100644 webui-src/app/network/network_friends_list.js create mode 100644 webui-src/app/network/network_state.js diff --git a/webui-src/app/network/network.js b/webui-src/app/network/network.js index b6e9334..99d7752 100644 --- a/webui-src/app/network/network.js +++ b/webui-src/app/network/network.js @@ -1,561 +1,18 @@ const m = require('mithril'); const rs = require('rswebui'); -const widget = require('widgets'); const Data = require('network/network_data'); -const peopleUtil = require('people/people_util'); const compose = require('mail/mail_compose'); - -// State variables for Network Page -const State = { - ownProfile: { - name: 'Loading...', - ssl_id: '', - gpg_id: '', - customState: '', - avatar: '', - }, - ownGxsIds: [], - selectedOwnGxsId: '', - selectedOwnGxsDetails: null, - selectedFriendGpgId: null, - activeTab: 'details', // 'details' | 'chat' - searchString: '', - gpgToGxsIdMap: {}, - gxsIdToDetailsMap: {}, - gxsIdentities: [], - currentChatPeerId: null, - chatMessages: [], - chatInputMsg: '', - showMailCompose: false, -}; - -// Fetch own node name using the same API as config_node.js -function loadOwnProfile() { - // Use rsConfig/getConfigNetStatus - the same proven endpoint used in config_node.js - rs.rsJsonApiRequest('/rsConfig/getConfigNetStatus', {}, (data) => { - if (data && data.status) { - State.ownProfile.name = data.status.ownName || 'Unknown'; - State.ownProfile.ssl_id = data.status.ownId || ''; - - // Fetch own custom status message using our own Location SSL ID - if (State.ownProfile.ssl_id) { - rs.rsJsonApiRequest('/rsChats/getCustomStateString', { peer_id: State.ownProfile.ssl_id }, (statusData) => { - if (statusData && statusData.retval) { - State.ownProfile.customState = statusData.retval; - m.redraw(); - } - }); - - // Also fetch our own node GPG ID via getPeerDetails using our own SSL ID - rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { sslId: State.ownProfile.ssl_id }, (detData) => { - if (detData && detData.det && detData.det.gpg_id) { - State.ownProfile.gpg_id = detData.det.gpg_id; - m.redraw(); - } - }); - - // Fetch own SSL avatar using our own Location SSL ID - 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(); - } - }); - - // Load own GXS identities using the existing utility - 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; // 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(); - } - }); - } -} - -// Build map GPG ID -> GXS ID for all known identities -function loadGxsIdentities() { - rs.rsJsonApiRequest('/rsIdentity/getIdentitiesSummaries', {}, (data) => { - if (data && data.ids) { - State.gxsIdentities = data.ids.map(u => u.mGroupId); - m.redraw(); - } - }); -} - -// Start a direct chat with a friend using their SSL peer ID (type 1) -function startDirectChat(sslId) { - State.currentChatPeerId = sslId; - State.chatMessages = []; - loadDirectChatMessages(); -} - -// Get the first online SSL ID for a friend, or fallback to first location -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; -} - -// Load message history for direct chat (type 1 is not in the event handler, -// so we manage messages locally) -function loadDirectChatMessages() { - // Messages are received via the event system and stored locally - // Register for incoming chat messages - rs.events[15].notify = (chatMessage) => { - if ( - chatMessage.chat_id && - (chatMessage.chat_id.type === 1 || chatMessage.chat_id.type === 2) && - rs.idToHex(chatMessage.chat_id) === State.currentChatPeerId - ) { - State.chatMessages.push(chatMessage); - m.redraw(); - scrollChatToBottom(); - } - }; -} - -// Send direct chat message (type 1 / peer_id) -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: msg, - }, - (data, success) => { - if (success) { - // Add own message to local log - State.chatMessages.push({ - chat_id: { type: 1, peer_id: State.currentChatPeerId }, - msg, - sendTime: Date.now() / 1000, - incoming: false, - own: true, - }); - 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); -} - -// Popup confirmation to remove friend SSL connection -const ConfirmRemove = () => { - return { - view: (vnode) => [ - m('h3', 'Remove Friend'), - m('hr'), - m('p', 'Are you sure you want to end connections with this node?'), - m( - 'button', - { - onclick: () => { - rs.rsJsonApiRequest('/rsPeers/removeFriend', { - pgpId: vnode.attrs.gpg_id, - }); - State.selectedFriendGpgId = null; - Data.refreshGpgDetails().then(() => m.redraw()); - widget.popupMessage(m('p', 'Friend removed successfully.')); - }, - }, - 'Confirm' - ), - ], - }; -}; - -const OwnProfileCard = () => { - return { - view: () => { - const avatar = State.ownProfile.avatar ? { mData: { base64: State.ownProfile.avatar } } : undefined; - const firstLetter = (State.ownProfile.name || 'U').slice(0, 1).toUpperCase(); - - return m('.own-profile-card', [ - m('.profile-header', [ - m(peopleUtil.UserAvatar, { avatar, firstLetter, seed: State.ownProfile.name }), - m('.profile-info', [ - m('.profile-name', State.ownProfile.name || 'Loading...'), - m('.profile-status', 'Online'), - State.ownProfile.customState && - m( - '.profile-custom-status', - { - style: 'font-size: 0.8rem; color: #94a3b8; font-style: italic; margin-top: 2px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 150px;', - title: State.ownProfile.customState, - }, - State.ownProfile.customState - ), - ]), - ]), - ]); - }, - }; -}; - -const FriendsList = () => { - return { - view: () => { - const search = State.searchString.toLowerCase(); - const filteredFriends = Object.entries(Data.gpgDetails).filter( - ([gpgId, friend]) => (friend.name || '').toLowerCase().includes(search) - ); - - return m('.friends-list-container', [ - m('.searchbar-container', [ - m('input.searchbar', { - type: 'text', - placeholder: 'Search friends...', - value: State.searchString, - oninput: (e) => { - State.searchString = e.target.value; - }, - }), - ]), - m('.friends-scroll', [ - filteredFriends.length === 0 - ? m('p', { style: 'padding: 1rem; color: #94a3b8; text-align: center;' }, 'No friends found') - : filteredFriends - .sort((a, b) => (a[1].isOnline === b[1].isOnline ? 0 : a[1].isOnline ? -1 : 1)) - .map(([gpgId, friend]) => { - const avatar = friend.avatar ? { mData: { base64: friend.avatar } } : undefined; - const firstLetter = (friend.name || '?').slice(0, 1).toUpperCase(); - const isSelected = State.selectedFriendGpgId === gpgId; - - return m( - `.friend-list-item${isSelected ? '.selected' : ''}`, - { - key: gpgId, - onclick: () => { - State.selectedFriendGpgId = gpgId; - State.currentChatPeerId = null; - State.chatMessages = []; - if (State.activeTab === 'chat') { - const sslId = getOnlineSslId(gpgId); - if (sslId) startDirectChat(sslId); - } - }, - }, - [ - m('.friend-avatar', m(peopleUtil.UserAvatar, { avatar, firstLetter, seed: gpgId })), - m('.friend-meta', [ - m('.friend-name', friend.name), - m( - `.friend-status${friend.isOnline ? '.online' : ''}`, - friend.isOnline ? 'Online' : 'Offline' - ), - friend.customState && - m( - '.friend-custom-status', - { - style: 'font-size: 0.85rem; color: #64748b; margin-top: 2px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 160px;', - title: friend.customState, - }, - friend.customState - ), - ]), - ] - ); - }), - ]), - ]); - }, - }; -}; - -// Right Pane Tabs and Tab Views -const DetailsTab = () => { - return { - view: () => { - const gpgId = State.selectedFriendGpgId; - const friend = Data.gpgDetails[gpgId]; - if (!friend) return null; - - const friendGxsId = State.gpgToGxsIdMap[gpgId.toLowerCase()]; - - return m('.network-detail-view', [ - m('.detail-header', [ - m('.friend-avatar', m(peopleUtil.UserAvatar, { - avatar: friend.avatar ? { mData: { base64: friend.avatar } } : undefined, - firstLetter: (friend.name || '?').slice(0, 1).toUpperCase(), - size: 128, - seed: gpgId, - })), - m('.detail-title', [ - m('h2', friend.name), - m('.detail-subtitle', [ - m('i.fas.fa-fingerprint'), - m('span', 'GPG ID: ' + gpgId), - ]), - ]), - m('.detail-actions', [ - m( - 'button', - { - onclick: () => { - const sslId = getOnlineSslId(gpgId); - if (sslId) { - State.activeTab = 'chat'; - startDirectChat(sslId); - } - }, - }, - [m('i.fas.fa-comments'), ' Start Chat'] - ), - m( - 'button', - { - onclick: () => { - State.showMailCompose = true; - }, - }, - [m('i.fas.fa-envelope'), ' Send Mail'] - ), - ]), - ]), - - m('.detail-section', [ - m('h3', 'Profile Info'), - m('.info-grid', [ - m('.info-label', 'Status'), - m( - '.info-value', - { style: friend.isOnline ? 'color: #10b981; font-weight: 600;' : '' }, - friend.isOnline ? 'Online' : 'Offline' - ), - m('.info-label', 'Custom Status'), - m( - '.info-value', - { style: 'font-style: italic; color: #64748b;' }, - friend.customState || 'None' - ), - friendGxsId ? [ - m('.info-label', 'GXS Identity'), - m('.info-value', friendGxsId), - ] : null, - m('.info-label', 'Node GPG Key'), - m('.info-value', gpgId), - ]), - ]), - - m('.detail-section', [ - m('h3', 'Locations (' + friend.locations.length + ')'), - m( - '.locations-grid', - friend.locations - .slice() - .sort((a, b) => (a.isOnline === b.isOnline ? 0 : a.isOnline ? -1 : 1)) - .map((loc) => - m('.location-card', { key: loc.id }, [ - m('.loc-header', [ - m('.loc-name', loc.name), - m( - '.loc-status' + (loc.isOnline ? '.online' : '.offline'), - loc.isOnline ? 'Online' : 'Offline' - ), - ]), - m('.loc-body', [ - m('.loc-label', 'SSL ID'), - m('.loc-val', loc.id), - m('.loc-label', 'Last Seen'), - m('.loc-val', new Date(loc.lastSeen * 1000).toLocaleString()), - ]), - m('.loc-footer', [ - m( - 'button.red', - { - onclick: () => - widget.popupMessage( - m(ConfirmRemove, { - gpg_id: loc.gpg_id, - }) - ), - }, - 'Remove Location' - ), - ]), - ]) - ) - ), - ]), - ]); - }, - }; -}; - -const ChatTab = () => { - return { - view: () => { - const gpgId = State.selectedFriendGpgId; - const friend = Data.gpgDetails[gpgId]; - if (!friend) return null; - - const sslId = getOnlineSslId(gpgId); - - if (!sslId) { - return m('.network-chat-view', [ - m('.chat-warning', [ - m('i.fas.fa-exclamation-triangle'), - m('h4', 'No Location Found'), - m('p', 'This friend has no known locations to start a direct chat with.'), - ]), - ]); - } - - if (!State.currentChatPeerId) { - return m('.network-chat-view', [ - m('.chat-warning', [ - m('i.fas.fa-comments'), - m('h4', 'Direct Chat'), - m('p', 'Click below to start a direct chat with ' + friend.name + '.'), - m( - 'button', - { - onclick: () => startDirectChat(sslId), - }, - 'Start Chat' - ), - ]), - ]); - } - - return m('.network-chat-view', [ - (() => { - const activeLoc = friend.locations.find((loc) => loc.id === State.currentChatPeerId); - const locName = activeLoc ? activeLoc.name : 'Unknown Location'; - const locOnline = activeLoc ? activeLoc.isOnline : false; - return m('.chat-header-bar', { - style: { - padding: '0.75rem 1rem', - backgroundColor: '#ffffff', - borderBottom: '1px solid #cbd5e1', - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between' - } - }, [ - m('.chat-header-info', [ - m('.chat-header-name', { style: { fontWeight: '700', color: '#1e293b' } }, friend.name), - m('.chat-header-location', { style: { fontSize: '0.8rem', color: '#64748b', display: 'flex', alignItems: 'center', marginTop: '0.25rem' } }, [ - m('span', 'Location: ' + locName), - m('span.status-dot', { - style: { - display: 'inline-block', - width: '8px', - height: '8px', - borderRadius: '50%', - backgroundColor: locOnline ? '#10b981' : '#ef4444', - marginLeft: '6px', - marginRight: '4px' - } - }), - m('span', { style: { color: locOnline ? '#10b981' : '#ef4444', fontWeight: '500' } }, locOnline ? 'Online' : 'Offline') - ]) - ]) - ]); - })(), - m( - '.chat-messages[id=chat-messages-container]', - State.chatMessages.map((msg) => { - const isOwn = msg.own === true; - const senderName = isOwn - ? (State.ownProfile.name || 'Me') - : friend.name; - const time = new Date(msg.sendTime * 1000).toLocaleTimeString(); - const text = (msg.msg || '') - .replaceAll('
', '\n') - .replace(new RegExp('|<[^>]*>', 'gm'), ''); - - return m( - '.chat-bubble-container' + (isOwn ? '.outgoing' : '.incoming'), - [ - !isOwn && m('.chat-sender', senderName), - m('.chat-bubble', text), - m('.chat-time', time), - ] - ); - }) - ), - m('.chat-input-area', [ - m('textarea.chat-textarea', { - placeholder: 'Type your message... Press Enter to send', - value: State.chatInputMsg, - oninput: (e) => { - State.chatInputMsg = e.target.value; - }, - onkeydown: (e) => { - if (e.code === 'Enter' && !e.shiftKey) { - e.preventDefault(); - sendDirectChatMessage(); - } - }, - }), - m( - 'button.send-btn', - { - onclick: () => sendDirectChatMessage(), - }, - [m('i.fas.fa-paper-plane'), ' Send'] - ), - ]), - ]); - }, - }; -}; +const { + State, + loadOwnProfile, + loadGxsIdentities, + fetchIdDetails, + startDirectChat, + getOnlineSslId, +} = require('network/network_state'); +const { OwnProfileCard, FriendsList } = require('network/network_friends_list'); +const DetailsTab = require('network/network_details_tab'); +const ChatTab = require('network/network_chat_tab'); const NetworkLayout = () => { return { @@ -565,7 +22,6 @@ const NetworkLayout = () => { loadGxsIdentities(); }, onremove: () => { - // Clean up notify callback when page is left if (rs.events[15]) { rs.events[15].notify = () => {}; } @@ -580,7 +36,7 @@ const NetworkLayout = () => { : null; if (State.selectedFriendGpgId && !selectedGxsId && State.gxsIdentities) { - State.gxsIdentities.forEach(gxsId => fetchIdDetails(gxsId)); + State.gxsIdentities.forEach((gxsId) => fetchIdDetails(gxsId)); } return m('.network-container', [ @@ -618,10 +74,12 @@ const NetworkLayout = () => { ] : m('.network-pane-placeholder', [ m('i.fas.fa-network-wired'), - m('p', 'Select a friend node from the left side panel to view locations details or start a private chat.'), + m( + 'p', + 'Select a friend node from the left side panel to view locations details or start a private chat.' + ), ]), ]), - // Mail composer overlay popup State.showMailCompose && State.selectedFriendGpgId && m( diff --git a/webui-src/app/network/network_chat_tab.js b/webui-src/app/network/network_chat_tab.js new file mode 100644 index 0000000..a10bf86 --- /dev/null +++ b/webui-src/app/network/network_chat_tab.js @@ -0,0 +1,125 @@ +const m = require('mithril'); +const Data = require('network/network_data'); +const { State, startDirectChat, getOnlineSslId, sendDirectChatMessage } = require('network/network_state'); + +const ChatTab = () => { + return { + view: () => { + const gpgId = State.selectedFriendGpgId; + const friend = Data.gpgDetails[gpgId]; + if (!friend) return null; + + const sslId = getOnlineSslId(gpgId); + + if (!sslId) { + return m('.network-chat-view', [ + m('.chat-warning', [ + m('i.fas.fa-exclamation-triangle'), + m('h4', 'No Location Found'), + m('p', 'This friend has no known locations to start a direct chat with.'), + ]), + ]); + } + + if (!State.currentChatPeerId) { + return m('.network-chat-view', [ + m('.chat-warning', [ + m('i.fas.fa-comments'), + m('h4', 'Direct Chat'), + m('p', 'Click below to start a direct chat with ' + friend.name + '.'), + m( + 'button', + { + onclick: () => startDirectChat(sslId), + }, + 'Start Chat' + ), + ]), + ]); + } + + return m('.network-chat-view', [ + (() => { + const activeLoc = friend.locations.find((loc) => loc.id === State.currentChatPeerId); + const locName = activeLoc ? activeLoc.name : 'Unknown Location'; + const locOnline = activeLoc ? activeLoc.isOnline : false; + return m('.chat-header-bar', { + style: { + padding: '0.75rem 1rem', + backgroundColor: '#ffffff', + borderBottom: '1px solid #cbd5e1', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between' + } + }, [ + m('.chat-header-info', [ + m('.chat-header-name', { style: { fontWeight: '700', color: '#1e293b' } }, friend.name), + m('.chat-header-location', { style: { fontSize: '0.8rem', color: '#64748b', display: 'flex', alignItems: 'center', marginTop: '0.25rem' } }, [ + m('span', 'Location: ' + locName), + m('span.status-dot', { + style: { + display: 'inline-block', + width: '8px', + height: '8px', + borderRadius: '50%', + backgroundColor: locOnline ? '#10b981' : '#ef4444', + marginLeft: '6px', + marginRight: '4px' + } + }), + m('span', { style: { color: locOnline ? '#10b981' : '#ef4444', fontWeight: '500' } }, locOnline ? 'Online' : 'Offline') + ]) + ]) + ]); + })(), + m( + '.chat-messages[id=chat-messages-container]', + State.chatMessages.map((msg) => { + const isOwn = msg.own === true; + const senderName = isOwn + ? (State.ownProfile.name || 'Me') + : friend.name; + const time = new Date(msg.sendTime * 1000).toLocaleTimeString(); + const text = (msg.msg || '') + .replaceAll('
', '\n') + .replace(new RegExp('|<[^>]*>', 'gm'), ''); + + return m( + '.chat-bubble-container' + (isOwn ? '.outgoing' : '.incoming'), + [ + !isOwn && m('.chat-sender', senderName), + m('.chat-bubble', text), + m('.chat-time', time), + ] + ); + }) + ), + m('.chat-input-area', [ + m('textarea.chat-textarea', { + placeholder: 'Type your message... Press Enter to send', + value: State.chatInputMsg, + oninput: (e) => { + State.chatInputMsg = e.target.value; + }, + onkeydown: (e) => { + if (e.code === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendDirectChatMessage(); + } + }, + }), + m( + 'button.send-btn', + { + onclick: () => sendDirectChatMessage(), + }, + [m('i.fas.fa-paper-plane'), ' Send'] + ), + ]), + ]); + }, + }; +}; + +module.exports = ChatTab; diff --git a/webui-src/app/network/network_details_tab.js b/webui-src/app/network/network_details_tab.js new file mode 100644 index 0000000..44793fc --- /dev/null +++ b/webui-src/app/network/network_details_tab.js @@ -0,0 +1,151 @@ +const m = require('mithril'); +const rs = require('rswebui'); +const widget = require('widgets'); +const Data = require('network/network_data'); +const peopleUtil = require('people/people_util'); +const { State, startDirectChat, getOnlineSslId } = require('network/network_state'); + +const ConfirmRemove = () => { + return { + view: (vnode) => [ + m('h3', 'Remove Friend'), + m('hr'), + m('p', 'Are you sure you want to end connections with this node?'), + m( + 'button', + { + onclick: () => { + rs.rsJsonApiRequest('/rsPeers/removeFriend', { + pgpId: vnode.attrs.gpg_id, + }); + State.selectedFriendGpgId = null; + Data.refreshGpgDetails().then(() => m.redraw()); + widget.popupMessage(m('p', 'Friend removed successfully.')); + }, + }, + 'Confirm' + ), + ], + }; +}; + +const DetailsTab = () => { + return { + view: () => { + const gpgId = State.selectedFriendGpgId; + const friend = Data.gpgDetails[gpgId]; + if (!friend) return null; + + const friendGxsId = State.gpgToGxsIdMap[gpgId.toLowerCase()]; + + return m('.network-detail-view', [ + m('.detail-header', [ + m('.friend-avatar', m(peopleUtil.UserAvatar, { + avatar: friend.avatar ? { mData: { base64: friend.avatar } } : undefined, + firstLetter: (friend.name || '?').slice(0, 1).toUpperCase(), + size: 128, + seed: gpgId, + })), + m('.detail-title', [ + m('h2', friend.name), + m('.detail-subtitle', [ + m('i.fas.fa-fingerprint'), + m('span', 'GPG ID: ' + gpgId), + ]), + ]), + m('.detail-actions', [ + m( + 'button', + { + onclick: () => { + const sslId = getOnlineSslId(gpgId); + if (sslId) { + State.activeTab = 'chat'; + startDirectChat(sslId); + } + }, + }, + [m('i.fas.fa-comments'), ' Start Chat'] + ), + m( + 'button', + { + onclick: () => { + State.showMailCompose = true; + }, + }, + [m('i.fas.fa-envelope'), ' Send Mail'] + ), + ]), + ]), + + m('.detail-section', [ + m('h3', 'Profile Info'), + m('.info-grid', [ + m('.info-label', 'Status'), + m( + '.info-value', + { style: friend.isOnline ? 'color: #10b981; font-weight: 600;' : '' }, + friend.isOnline ? 'Online' : 'Offline' + ), + m('.info-label', 'Custom Status'), + m( + '.info-value', + { style: 'font-style: italic; color: #64748b;' }, + friend.customState || 'None' + ), + friendGxsId ? [ + m('.info-label', 'GXS Identity'), + m('.info-value', friendGxsId), + ] : null, + m('.info-label', 'Node GPG Key'), + m('.info-value', gpgId), + ]), + ]), + + m('.detail-section', [ + m('h3', 'Locations (' + friend.locations.length + ')'), + m( + '.locations-grid', + friend.locations + .slice() + .sort((a, b) => (a.isOnline === b.isOnline ? 0 : a.isOnline ? -1 : 1)) + .map((loc) => + m('.location-card', { key: loc.id }, [ + m('.loc-header', [ + m('.loc-name', loc.name), + m( + '.loc-status' + (loc.isOnline ? '.online' : '.offline'), + loc.isOnline ? 'Online' : 'Offline' + ), + ]), + m('.loc-body', [ + m('.loc-label', 'SSL ID'), + m('.loc-val', loc.id), + m('.loc-label', 'Last Seen'), + m('.loc-val', new Date(loc.lastSeen * 1000).toLocaleString()), + ]), + m('.loc-footer', [ + m( + 'button.red', + { + onclick: () => + widget.popupMessage( + m(ConfirmRemove, { + gpg_id: loc.gpg_id, + }) + ), + }, + 'Remove Location' + ), + ]), + ]) + ) + ), + ]), + ]); + }, + }; +}; + +module.exports = DetailsTab; diff --git a/webui-src/app/network/network_friends_list.js b/webui-src/app/network/network_friends_list.js new file mode 100644 index 0000000..cee5328 --- /dev/null +++ b/webui-src/app/network/network_friends_list.js @@ -0,0 +1,107 @@ +const m = require('mithril'); +const Data = require('network/network_data'); +const peopleUtil = require('people/people_util'); +const { State, startDirectChat, getOnlineSslId } = require('network/network_state'); + +const OwnProfileCard = () => { + return { + view: () => { + const avatar = State.ownProfile.avatar ? { mData: { base64: State.ownProfile.avatar } } : undefined; + const firstLetter = (State.ownProfile.name || 'U').slice(0, 1).toUpperCase(); + + return m('.own-profile-card', [ + m('.profile-header', [ + m(peopleUtil.UserAvatar, { avatar, firstLetter, seed: State.ownProfile.name }), + m('.profile-info', [ + m('.profile-name', State.ownProfile.name || 'Loading...'), + m('.profile-status', 'Online'), + State.ownProfile.customState && + m( + '.profile-custom-status', + { + style: 'font-size: 0.8rem; color: #94a3b8; font-style: italic; margin-top: 2px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 150px;', + title: State.ownProfile.customState, + }, + State.ownProfile.customState + ), + ]), + ]), + ]); + }, + }; +}; + +const FriendsList = () => { + return { + view: () => { + const search = State.searchString.toLowerCase(); + const filteredFriends = Object.entries(Data.gpgDetails).filter( + ([gpgId, friend]) => (friend.name || '').toLowerCase().includes(search) + ); + + return m('.friends-list-container', [ + m('.searchbar-container', [ + m('input.searchbar', { + type: 'text', + placeholder: 'Search friends...', + value: State.searchString, + oninput: (e) => { + State.searchString = e.target.value; + }, + }), + ]), + m('.friends-scroll', [ + filteredFriends.length === 0 + ? m('p', { style: 'padding: 1rem; color: #94a3b8; text-align: center;' }, 'No friends found') + : filteredFriends + .sort((a, b) => (a[1].isOnline === b[1].isOnline ? 0 : a[1].isOnline ? -1 : 1)) + .map(([gpgId, friend]) => { + const avatar = friend.avatar ? { mData: { base64: friend.avatar } } : undefined; + const firstLetter = (friend.name || '?').slice(0, 1).toUpperCase(); + const isSelected = State.selectedFriendGpgId === gpgId; + + return m( + `.friend-list-item${isSelected ? '.selected' : ''}`, + { + key: gpgId, + onclick: () => { + State.selectedFriendGpgId = gpgId; + State.currentChatPeerId = null; + State.chatMessages = []; + if (State.activeTab === 'chat') { + const sslId = getOnlineSslId(gpgId); + if (sslId) startDirectChat(sslId); + } + }, + }, + [ + m('.friend-avatar', m(peopleUtil.UserAvatar, { avatar, firstLetter, seed: gpgId })), + m('.friend-meta', [ + m('.friend-name', friend.name), + m( + `.friend-status${friend.isOnline ? '.online' : ''}`, + friend.isOnline ? 'Online' : 'Offline' + ), + friend.customState && + m( + '.friend-custom-status', + { + style: 'font-size: 0.85rem; color: #64748b; margin-top: 2px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 160px;', + title: friend.customState, + }, + friend.customState + ), + ]), + ] + ); + }), + ]), + ]); + }, + }; +}; + +module.exports = { + OwnProfileCard, + FriendsList, +}; diff --git a/webui-src/app/network/network_state.js b/webui-src/app/network/network_state.js new file mode 100644 index 0000000..be29209 --- /dev/null +++ b/webui-src/app/network/network_state.js @@ -0,0 +1,190 @@ +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...', + ssl_id: '', + gpg_id: '', + customState: '', + avatar: '', + }, + ownGxsIds: [], + selectedOwnGxsId: '', + selectedOwnGxsDetails: null, + selectedFriendGpgId: null, + activeTab: 'details', // 'details' | 'chat' + searchString: '', + gpgToGxsIdMap: {}, + gxsIdToDetailsMap: {}, + gxsIdentities: [], + currentChatPeerId: null, + chatMessages: [], + chatInputMsg: '', + showMailCompose: false, +}; + +function loadOwnProfile() { + 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) { + rs.rsJsonApiRequest('/rsChats/getCustomStateString', { peer_id: State.ownProfile.ssl_id }, (statusData) => { + if (statusData && statusData.retval) { + State.ownProfile.customState = statusData.retval; + m.redraw(); + } + }); + + rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { sslId: State.ownProfile.ssl_id }, (detData) => { + if (detData && detData.det && detData.det.gpg_id) { + State.ownProfile.gpg_id = detData.det.gpg_id; + m.redraw(); + } + }); + + 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 = []; + loadDirectChatMessages(); +} + +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 loadDirectChatMessages() { + rs.events[15].notify = (chatMessage) => { + if ( + chatMessage.chat_id && + (chatMessage.chat_id.type === 1 || chatMessage.chat_id.type === 2) && + rs.idToHex(chatMessage.chat_id) === State.currentChatPeerId + ) { + State.chatMessages.push(chatMessage); + m.redraw(); + scrollChatToBottom(); + } + }; +} + +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: msg, + }, + (data, success) => { + if (success) { + State.chatMessages.push({ + chat_id: { type: 1, peer_id: State.currentChatPeerId }, + msg, + sendTime: Date.now() / 1000, + incoming: false, + own: true, + }); + 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); +} + +module.exports = { + State, + loadOwnProfile, + loadSelectedOwnGxsDetails, + fetchIdDetails, + loadGxsIdentities, + startDirectChat, + getOnlineSslId, + loadDirectChatMessages, + sendDirectChatMessage, + scrollChatToBottom, +}; From 02dc11ec9d91ba88d324449acec13a0e608696ec Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:53:00 +0200 Subject: [PATCH 21/40] split people code --- webui-src/app/people/people.js | 1001 +------------------- webui-src/app/people/people_chat_tab.js | 179 ++++ webui-src/app/people/people_details_tab.js | 231 +++++ webui-src/app/people/people_sidebar.js | 270 ++++++ webui-src/app/people/people_state.js | 376 ++++++++ 5 files changed, 1069 insertions(+), 988 deletions(-) create mode 100644 webui-src/app/people/people_chat_tab.js create mode 100644 webui-src/app/people/people_details_tab.js create mode 100644 webui-src/app/people/people_sidebar.js create mode 100644 webui-src/app/people/people_state.js diff --git a/webui-src/app/people/people.js b/webui-src/app/people/people.js index bf54a62..97688c9 100644 --- a/webui-src/app/people/people.js +++ b/webui-src/app/people/people.js @@ -1,748 +1,19 @@ const m = require('mithril'); const rs = require('rswebui'); -const widget = require('widgets'); const Data = require('network/network_data'); -const peopleUtil = require('people/people_util'); const compose = require('mail/mail_compose'); -const ownIdsLayout = require('people/people_ownids'); -const { CreateIdentity, EditIdentity, DeleteIdentity } = ownIdsLayout; - -// State variables for People Page -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, -}; -// Build map GPG ID -> GXS ID for all known identities -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(); - } - }); - } -} - -// Build map GPG ID -> GXS ID for all known identities -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}`; - } -} - -// Helpers -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(); - // Scroll to bottom - 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 = ''; - - // Optimistic echo - 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'); - } - } - ); -} - -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)), - ]); - }); - })() - ]) - ]), - ]); - }, - }; -}; - -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' - ]) - ]) - ]), - - // Messages area - 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()), - ]); - }), - ]), - - // Input area - 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'] - ), - ]), - ]); - }, - }; -}; +const { + State, + fetchIdDetails, + loadGxsIdentities, + loadOwnGxsIds, + syncFilter, + stopStatusPolling, + initializeDistantChat, +} = require('people/people_state'); +const PeopleSidebar = require('people/people_sidebar'); +const DetailsTab = require('people/people_details_tab'); +const ChatTab = require('people/people_chat_tab'); const PeopleLayout = () => { const dismissMenu = () => { @@ -783,7 +54,6 @@ const PeopleLayout = () => { }; }, onremove: () => { - // Clean up notify callback when page is left if (rs.events[15]) { rs.events[15].notify = () => {}; } @@ -794,258 +64,13 @@ const PeopleLayout = () => { syncFilter(vnode.attrs.tab); }, 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); - }); - - // 3. Selected details details info fetchIdDetails(State.selectedId); const details = State.selectedId ? State.gxsIdToDetailsMap[State.selectedId] : null; const name = details ? details.mNickname || details.mGroupName || 'Unknown' : ''; return m('.people-container', [ // Left Side Panel - 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' - ), - ]), - ] - ); - }), - ]), - 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' - ]) - ]); - })() - ]), - ]), + m(PeopleSidebar), // Right Side Details / Actions Pane m('.people-right-pane', [ diff --git a/webui-src/app/people/people_chat_tab.js b/webui-src/app/people/people_chat_tab.js new file mode 100644 index 0000000..82df32e --- /dev/null +++ b/webui-src/app/people/people_chat_tab.js @@ -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; diff --git a/webui-src/app/people/people_details_tab.js b/webui-src/app/people/people_details_tab.js new file mode 100644 index 0000000..6e68d74 --- /dev/null +++ b/webui-src/app/people/people_details_tab.js @@ -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; diff --git a/webui-src/app/people/people_sidebar.js b/webui-src/app/people/people_sidebar.js new file mode 100644 index 0000000..c09724c --- /dev/null +++ b/webui-src/app/people/people_sidebar.js @@ -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; diff --git a/webui-src/app/people/people_state.js b/webui-src/app/people/people_state.js new file mode 100644 index 0000000..4908946 --- /dev/null +++ b/webui-src/app/people/people_state.js @@ -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, +}; From e6175571d167e8a6e637040204c2788545f53aa6 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Wed, 22 Jul 2026 23:35:35 +0200 Subject: [PATCH 22/40] Removed the non-existent API call --- webui-src/app/statusbar.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/webui-src/app/statusbar.js b/webui-src/app/statusbar.js index e076e5c..2ebe7b4 100644 --- a/webui-src/app/statusbar.js +++ b/webui-src/app/statusbar.js @@ -48,22 +48,14 @@ function updateStatus() { State.forwardPort = data.status.forwardPort; State.stunOk = data.status.netStunOk; State.extAddressOk = data.status.netExtAddressOk; - } - }); - // 3. NAT netState - rs.rsJsonApiRequest('/rsConfig/getNetState', {}, (data) => { - if (data && data.retval !== undefined) { - State.natState = data.retval; - } else { - // Fallback calculation based on getConfigNetStatus if (State.firewalled && !State.forwardPort) { State.natState = 6; // WARNING_NATTED } else { State.natState = 8; // GOOD } } - }); + }).catch(() => {}); } let intervalId = null; From 238ad60d4f06307531303ee4a1971a11b75d9080 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Fri, 24 Jul 2026 20:22:41 +0200 Subject: [PATCH 23/40] Fix issue with I2P check --- webui-src/app/config/config_network.js | 75 +++++++++++++++++++------- 1 file changed, 57 insertions(+), 18 deletions(-) diff --git a/webui-src/app/config/config_network.js b/webui-src/app/config/config_network.js index 9f929bf..7045256 100644 --- a/webui-src/app/config/config_network.js +++ b/webui-src/app/config/config_network.js @@ -320,23 +320,54 @@ const SetDynamicDNS = () => { }; }; -const checkPortReachable = (addr, port, timeoutMs = 600) => { +const checkPortReachable = (addr, port, timeoutMs = 800) => { if (!addr || !port) return Promise.resolve(false); - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), timeoutMs); - return fetch(`http://${addr}:${port}`, { - mode: 'no-cors', - signal: controller.signal, - cache: 'no-store', - }) - .then(() => { - clearTimeout(timer); - return true; + + return new Promise((resolve) => { + let resolved = false; + const start = Date.now(); + const controller = new AbortController(); + + const timer = setTimeout(() => { + if (!resolved) { + resolved = true; + controller.abort(); + // Timeout expired without server connection -> Port is closed / not enabled! + resolve(false); + } + }, timeoutMs); + + fetch(`http://${addr}:${port}`, { + mode: 'no-cors', + signal: controller.signal, + cache: 'no-store', }) - .catch((err) => { - clearTimeout(timer); - return err.name === 'AbortError'; - }); + .then(() => { + if (!resolved) { + resolved = true; + clearTimeout(timer); + resolve(true); + } + }) + .catch((err) => { + if (!resolved) { + resolved = true; + clearTimeout(timer); + if (err && err.name === 'AbortError') { + resolve(true); + } else { + // Tor SOCKS port returns HTTP 501 ("Tor is not an HTTP Proxy"). + // Connection refused fails in < 15ms. If Tor server responded (501 / > 20ms), port is OPEN! + const duration = Date.now() - start; + if (duration > 20 || (err && err.message && err.message.includes('501'))) { + resolve(true); + } else { + resolve(false); + } + } + } + }); + }); }; const SetSocksProxy = () => { @@ -347,11 +378,14 @@ const SetSocksProxy = () => { const fetchOutgoing = () => { Object.keys(socksProxyObj).forEach((proxyItem) => { const item = socksProxyObj[proxyItem]; - if (item.addr && item.port) { + if (item.retval && item.addr && item.port) { checkPortReachable(item.addr, item.port).then((isReachable) => { item.outgoing = isReachable; m.redraw(); }); + } else { + item.outgoing = false; + m.redraw(); } }); }; @@ -360,7 +394,12 @@ const SetSocksProxy = () => { type: util[`RS_HIDDEN_TYPE_${proxyItem.toUpperCase()}`], addr: socksProxyObj[proxyItem].addr, port: socksProxyObj[proxyItem].port, - }).then(fetchOutgoing); + }).then((res) => { + if (res && res.body) { + socksProxyObj[proxyItem] = res.body; + } + fetchOutgoing(); + }); }; return { oninit: () => { @@ -369,7 +408,7 @@ const SetSocksProxy = () => { type: util[`RS_HIDDEN_TYPE_${proxyItem.toUpperCase()}`], }) .then((res) => { - if (res.body.retval) { + if (res && res.body) { socksProxyObj[proxyItem] = res.body; } }) From 8a3e2d3693b4b1f4e3d22355ff0b18918d782798 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Fri, 24 Jul 2026 21:16:16 +0200 Subject: [PATCH 24/40] Fixed bug with voting --- webui-src/app/chat/chat.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index bdefad4..6aa7832 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -752,7 +752,7 @@ const ChatConversationView = () => { !isOwn && m('.menu-item', { onclick: () => { ChatHubState.activeMenu = null; - rs.rsJsonApiRequest('/rsreputations/setOwnOpinion', { id: menu.gxsId, opinion: 2 }, (data, success) => { + rs.rsJsonApiRequest('/rsreputations/setOwnOpinion', { id: menu.gxsId, op: 2 }, (data, success) => { if (success) { if (!ChatHubState.gxsDetails[menu.gxsId]) ChatHubState.gxsDetails[menu.gxsId] = { mReputation: {} }; if (!ChatHubState.gxsDetails[menu.gxsId].mReputation) ChatHubState.gxsDetails[menu.gxsId].mReputation = {}; @@ -774,7 +774,7 @@ const ChatConversationView = () => { !isOwn && m('.menu-item', { onclick: () => { ChatHubState.activeMenu = null; - rs.rsJsonApiRequest('/rsreputations/setOwnOpinion', { id: menu.gxsId, opinion: 1 }, (data, success) => { + rs.rsJsonApiRequest('/rsreputations/setOwnOpinion', { id: menu.gxsId, op: 1 }, (data, success) => { if (success) { if (!ChatHubState.gxsDetails[menu.gxsId]) ChatHubState.gxsDetails[menu.gxsId] = { mReputation: {} }; if (!ChatHubState.gxsDetails[menu.gxsId].mReputation) ChatHubState.gxsDetails[menu.gxsId].mReputation = {}; @@ -796,7 +796,7 @@ const ChatConversationView = () => { !isOwn && m('.menu-item', { onclick: () => { ChatHubState.activeMenu = null; - rs.rsJsonApiRequest('/rsreputations/setOwnOpinion', { id: menu.gxsId, opinion: 0 }, (data, success) => { + rs.rsJsonApiRequest('/rsreputations/setOwnOpinion', { id: menu.gxsId, op: 0 }, (data, success) => { if (success) { if (!ChatHubState.gxsDetails[menu.gxsId]) ChatHubState.gxsDetails[menu.gxsId] = { mReputation: {} }; if (!ChatHubState.gxsDetails[menu.gxsId].mReputation) ChatHubState.gxsDetails[menu.gxsId].mReputation = {}; From 1ecb2dce7c893058c140c9be28347cafea58b7b2 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Fri, 24 Jul 2026 21:36:41 +0200 Subject: [PATCH 25/40] Added tooltips for mails Move buttons on people page to the left side --- webui-src/app/mail/mail_util.js | 100 +++++++++++++++- webui-src/app/people/people_details_tab.js | 132 ++++++++++----------- 2 files changed, 164 insertions(+), 68 deletions(-) diff --git a/webui-src/app/mail/mail_util.js b/webui-src/app/mail/mail_util.js index ad29988..a46f073 100644 --- a/webui-src/app/mail/mail_util.js +++ b/webui-src/app/mail/mail_util.js @@ -36,6 +36,55 @@ const BOX_ALL = 0x06; const MessageCache = {}; const UserNicknamesCache = {}; +const MailGxsDetailsCache = {}; +const MailHoverState = { + hoveredUser: null, +}; + +function renderMailUserTooltip() { + if (!MailHoverState.hoveredUser) return null; + const hUser = MailHoverState.hoveredUser; + const details = MailGxsDetailsCache[hUser.gxsId]; + if (!details) return null; + + const avatar = details.mAvatar && details.mAvatar.base64 ? details.mAvatar.base64 : null; + const firstLetter = (hUser.name || '?').slice(0, 1).toUpperCase(); + const votes = details.mReputation + ? ((details.mReputation.mFriendsPositiveVotes || 0) - (details.mReputation.mFriendsNegativeVotes || 0)) + : 0; + + const top = hUser.rect.top - 10; + const left = Math.min(Math.max(hUser.rect.left, 140), window.innerWidth - 280); + + return m('.user-tooltip', { + style: { + position: 'fixed', + top: `${top}px`, + left: `${left}px`, + transform: 'translateY(-100%)', + zIndex: 10000, + } + }, [ + m('.tooltip-avatar', m(peopleUtil.UserAvatar, { avatar, firstLetter, identityId: hUser.gxsId, size: 64 })), + m('.tooltip-details', [ + m('.tooltip-row', [m('span.tooltip-label', 'Identity name: '), m('span.tooltip-value', hUser.name)]), + m('.tooltip-row', [m('span.tooltip-label', 'Identity Id: '), m('span.tooltip-value.tooltip-id', hUser.gxsId)]), + details.mPgpId && details.mPgpId !== '0000000000000000' && m('.tooltip-row', [ + m('span.tooltip-label', 'Node: '), + m('span.tooltip-value', `${rs.userList.username(details.mPgpId) || hUser.name} [${details.mPgpId}]`) + ]), + m('.tooltip-row', [ + m('span.tooltip-label', 'Votes: '), + m('span.tooltip-value', { + style: { + color: votes >= 0 ? '#22c55e' : '#ef4444', + fontWeight: 'bold' + } + }, (votes >= 0 ? '+' : '') + votes) + ]) + ]) + ]); +} const tagTypesCache = {}; const defaultTagTypes = { @@ -112,6 +161,7 @@ const MessageSummary = () => { fromUserInfo = data.details; if (fromUserInfo) { UserNicknamesCache[details.from._addr_string] = fromUserInfo.mNickname || ''; + MailGxsDetailsCache[details.from._addr_string] = fromUserInfo; } } ); @@ -172,7 +222,29 @@ const MessageSummary = () => { alignItems: 'center', gap: '0.5rem', justifyContent: 'start', + cursor: 'pointer', }, + onmouseenter: (e) => { + if (!details?.from?._addr_string) return; + const gxsId = details.from._addr_string; + const name = fromUserInfo && Number(fromUserInfo.mId) !== 0 ? fromUserInfo.mNickname : '[Unknown]'; + const rect = e.currentTarget.getBoundingClientRect(); + MailHoverState.hoveredUser = { gxsId, name, rect }; + if (fromUserInfo) MailGxsDetailsCache[gxsId] = fromUserInfo; + if (!MailGxsDetailsCache[gxsId]) { + rs.rsJsonApiRequest('/rsIdentity/getIdDetails', { id: gxsId }, (d) => { + if (d && d.details) { + MailGxsDetailsCache[gxsId] = d.details; + m.redraw(); + } + }); + } + m.redraw(); + }, + onmouseleave: () => { + MailHoverState.hoveredUser = null; + m.redraw(); + } }, [ m(peopleUtil.UserAvatar, { @@ -350,7 +422,29 @@ const MessageView = () => { }), m('.msg-details__info', [ MailData.sender && - m('.msg-details__info-item', [ + m('.msg-details__info-item', { + style: { cursor: 'pointer', display: 'inline-flex', gap: '0.25rem', alignItems: 'center' }, + onmouseenter: (e) => { + if (!MailData.sender._addr_string) return; + const gxsId = MailData.sender._addr_string; + const name = UserNicknamesCache[gxsId] || rs.userList.username(gxsId) || 'Unknown'; + const rect = e.currentTarget.getBoundingClientRect(); + MailHoverState.hoveredUser = { gxsId, name, rect }; + if (!MailGxsDetailsCache[gxsId]) { + rs.rsJsonApiRequest('/rsIdentity/getIdDetails', { id: gxsId }, (d) => { + if (d && d.details) { + MailGxsDetailsCache[gxsId] = d.details; + m.redraw(); + } + }); + } + m.redraw(); + }, + onmouseleave: () => { + MailHoverState.hoveredUser = null; + m.redraw(); + } + }, [ m('b', 'From: '), UserNicknamesCache[MailData.sender._addr_string] || rs.userList.username(MailData.sender._addr_string) || 'Unknown', ]), @@ -424,7 +518,8 @@ const MessageView = () => { : m('.widget', m('.widget__heading', m('h3', 'Sender is not known'))), m('button.red.close-btn', { onclick: () => setShowCompose(false) }, m('i.fas.fa-times')) ) - ) + ), + renderMailUserTooltip(), ), }; }; @@ -595,6 +690,7 @@ const Table = () => { tbody, ]), paginationUI, + renderMailUserTooltip(), ]); }, }; diff --git a/webui-src/app/people/people_details_tab.js b/webui-src/app/people/people_details_tab.js index 6e68d74..8dd1657 100644 --- a/webui-src/app/people/people_details_tab.js +++ b/webui-src/app/people/people_details_tab.js @@ -87,75 +87,75 @@ const DetailsTab = () => { 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('.detail-actions', [ + isOwn + ? [ + m( + 'button.btn', + { + onclick: () => + widget.popupMessage( + m(EditIdentity, { + details, + }) + ), }, - }, - [m('i.fas.fa-comment-alt'), ' Start Chat'] - ), - m( - 'button.btn', - { - onclick: () => { - State.showMailCompose = true; + [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-envelope'), ' Send Mail'] - ), - m( - 'button.btn' + (isContact ? '.red' : '.blue'), - { - onclick: () => { - rs.rsJsonApiRequest( - '/rsIdentity/setAsRegularContact', - { id: State.selectedId, isContact: !isContact }, - () => { - rs.userList.loadUsers(); - loadGxsIdentities(); - } - ); + [m('i.fas.fa-trash-alt'), ' Delete'] + ), + ] + : [ + m( + 'button.btn.blue', + { + onclick: () => { + State.activeTab = 'chat'; + initializeDistantChat(); + }, }, - }, - isContact - ? [m('i.fas.fa-user-minus'), ' Remove Contact'] - : [m('i.fas.fa-user-plus'), ' Add Contact'] - ), - ], + [m('i.fas.fa-comment-alt'), ' Start Chat'] + ), + m( + 'button.btn.blue', + { + 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', [ From fcd095378444e68545874dc157da34fb72c48273 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Fri, 24 Jul 2026 23:10:08 +0200 Subject: [PATCH 26/40] Fixed context menu right mouse click Improved People list to get more useable to switch Added Chats lists --- webui-src/app/chat/chat.js | 21 +- webui-src/app/people/people.js | 4 +- webui-src/app/people/people_sidebar.js | 312 +++++++++++++++++-------- webui-src/app/people/people_state.js | 118 ++++++++++ 4 files changed, 336 insertions(+), 119 deletions(-) diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index 6aa7832..4ea9b6d 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -536,25 +536,8 @@ const ChatConversationView = () => { e.preventDefault(); e.stopPropagation(); ChatHubState.hoveredUser = null; - - const rect = e.currentTarget.getBoundingClientRect(); - const rightbar = document.querySelector('.chat-hub-rightbar'); - if (rightbar) { - const parentRect = rightbar.getBoundingClientRect(); - const itemBottom = rect.bottom - parentRect.top; - const estimatedMenuHeight = 310; - let top = itemBottom; - if (itemBottom + estimatedMenuHeight > parentRect.height) { - top = rect.top - parentRect.top - estimatedMenuHeight; - if (top < 10) top = 10; - } - if (ChatHubState.activeMenu && ChatHubState.activeMenu.gxsId === gxsId) { - ChatHubState.activeMenu = null; - } else { - ChatHubState.activeMenu = { gxsId, name, top }; - } - m.redraw(); - } + ChatHubState.activeMenu = null; + m.redraw(); }, oncontextmenu: (e) => { e.preventDefault(); diff --git a/webui-src/app/people/people.js b/webui-src/app/people/people.js index 97688c9..f859ac9 100644 --- a/webui-src/app/people/people.js +++ b/webui-src/app/people/people.js @@ -7,6 +7,7 @@ const { fetchIdDetails, loadGxsIdentities, loadOwnGxsIds, + preloadAllChatHistory, syncFilter, stopStatusPolling, initializeDistantChat, @@ -28,7 +29,8 @@ const PeopleLayout = () => { syncFilter(vnode.attrs.tab); Data.refreshGpgDetails().then(() => m.redraw()); loadGxsIdentities(); - loadOwnGxsIds(); + loadOwnGxsIds().then(() => preloadAllChatHistory()); + preloadAllChatHistory(); window.addEventListener('click', dismissMenu); // Register for chatEvents to receive live incoming messages diff --git a/webui-src/app/people/people_sidebar.js b/webui-src/app/people/people_sidebar.js index c09724c..75874cc 100644 --- a/webui-src/app/people/people_sidebar.js +++ b/webui-src/app/people/people_sidebar.js @@ -6,99 +6,99 @@ const ownIdsLayout = require('people/people_ownids'); const { CreateIdentity } = ownIdsLayout; const { State, + isSystemMsg, + preloadAllChatHistory, fetchIdDetails, loadGxsIdentities, getSafeAvatar, + get64Num, stopStatusPolling, initializeDistantChat, } = require('people/people_state'); +function formatRelativeTime(ts) { + if (!ts) return ''; + const now = Math.floor(Date.now() / 1000); + const diff = now - ts; + if (diff < 30) return 'Just Now'; + if (diff < 3600) return `${Math.floor(diff / 60)} min${Math.floor(diff / 60) > 1 ? 's' : ''}`; + if (diff < 86400) return `${Math.floor(diff / 3600)} hr${Math.floor(diff / 3600) > 1 ? 's' : ''}`; + return `${Math.floor(diff / 86400)} d`; +} + const PeopleSidebar = () => { return { + oninit: () => { + preloadAllChatHistory(); + }, 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) || []; + // 1. Determine list based on mainTab ('people' vs 'chats') + let displayItems = []; + + if (State.mainTab === 'people') { + 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) || []; + } + + displayItems = baseList.filter((item) => { + let name = State.activeFilter === 'own' ? (rs.userList.username(item) || 'Unknown') : (item.mGroupName || 'Unknown'); + return name.toLowerCase().includes(State.searchString.toLowerCase()); + }); + + displayItems.sort((a, b) => { + let nameA = State.activeFilter === 'own' ? (rs.userList.username(a) || '') : (a.mGroupName || ''); + let nameB = State.activeFilter === 'own' ? (rs.userList.username(b) || '') : (b.mGroupName || ''); + return nameA.localeCompare(nameB); + }); } else { - baseList = peopleUtil.sortUsers(rs.userList.users) || []; + // Chats Tab: ONLY contacts and identities that have real chat history (ignoring system tunnel status logs) + const userGroupIds = new Set((rs.userList.users || []).map((u) => u.mGroupId)); + Object.keys(State.chatHistoryMap || {}).forEach((id) => userGroupIds.add(id)); + + displayItems = Array.from(userGroupIds) + .map((gxsId) => { + const entry = rs.userList.userMap[gxsId]; + const name = entry && entry.name ? entry.name : (rs.userList.username(gxsId) || 'Unknown'); + return { mGroupId: gxsId, mGroupName: name }; + }) + .filter((item) => { + const gxsId = item.mGroupId; + fetchIdDetails(gxsId); + const hist = State.chatHistoryMap && State.chatHistoryMap[gxsId]; + + const hasRealHistory = Boolean(hist && hist.lastMsg && !isSystemMsg(hist.lastMsg)); + + if (!hasRealHistory) return false; + + const name = item.mGroupName || 'Unknown'; + return name.toLowerCase().includes(State.searchString.toLowerCase()); + }); + + // Sort by chat timestamp descending + displayItems.sort((a, b) => { + const histA = State.chatHistoryMap[a.mGroupId]; + const histB = State.chatHistoryMap[b.mGroupId]; + const detailsA = State.gxsIdToDetailsMap[a.mGroupId]; + const detailsB = State.gxsIdToDetailsMap[b.mGroupId]; + + const timeA = histA ? histA.lastTime : (detailsA ? get64Num(detailsA.mLastUsageTS) : 0); + const timeB = histB ? histB.lastTime : (detailsB ? get64Num(detailsB.mLastUsageTS) : 0); + return timeB - timeA; + }); } - // 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...]', { + // Sidebar Header Container + m('.people-sidebar-header', [ + // 1. Top Search Bar + m('.searchbar-wrapper', [ + m('i.fas.fa-search'), + m('input.searchbar-input[type=text][placeholder=Search...]', { value: State.searchString, oninput: (e) => { State.searchString = e.target.value; @@ -106,13 +106,75 @@ const PeopleSidebar = () => { }), ]), - // Scrollable list + // 2. Dual Segmented Tab Control: [People] | [Chats] + m('.segmented-control', [ + m( + 'button.segment-tab' + (State.mainTab === 'people' ? '.active' : ''), + { + onclick: () => { + State.mainTab = 'people'; + m.redraw(); + }, + }, + [m('i.fas.fa-users'), ' People'] + ), + m( + 'button.segment-tab' + (State.mainTab === 'chats' ? '.active' : ''), + { + onclick: () => { + State.mainTab = 'chats'; + preloadAllChatHistory(); + m.redraw(); + }, + }, + [m('i.fas.fa-comments'), ' Chats'] + ), + ]), + + // 3. Sub-Filter Row (People Tab) + State.mainTab === 'people' && + m('.sub-filter-row', [ + m( + 'select.filter-select', + { + value: State.activeFilter, + onchange: (e) => { + State.activeFilter = e.target.value; + m.route.set( + '/people/' + + (State.activeFilter === 'contacts' + ? 'MyContacts' + : State.activeFilter === 'own' + ? 'OwnIdentity' + : 'All') + ); + }, + }, + [ + m('option[value=contacts]', 'Contacts'), + m('option[value=own]', 'My Identities'), + m('option[value=all]', 'All Users'), + ] + ), + State.activeFilter === 'own' && + m( + 'button.btn-add-id[title=Create New Identity]', + { + onclick: () => widget.popupMessage(m(CreateIdentity)), + }, + m('i.fas.fa-plus') + ), + ]), + ]), + + // Scrollable List Container + m('.friends-list-container', [ m('.friends-scroll', [ - filteredList.length === 0 - ? m('.network-pane-placeholder', { style: 'padding: 2rem 0;' }, 'No identities found') - : filteredList.map((item) => { + displayItems.length === 0 + ? m('.network-pane-placeholder', { style: 'padding: 2rem 0;' }, State.mainTab === 'chats' ? 'No active chats' : 'No identities found') + : displayItems.map((item) => { let gxsId, displayName; - if (State.activeFilter === 'own') { + if (State.mainTab === 'people' && State.activeFilter === 'own') { gxsId = item; displayName = rs.userList.username(gxsId) || 'Unknown'; } else { @@ -130,6 +192,65 @@ const PeopleSidebar = () => { const itemIsContact = itemEntry && itemEntry.isContact; const itemIsOwn = State.ownGxsIds.includes(gxsId); + 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'); + + if (State.mainTab === 'chats') { + return m( + '.chat-item', + { + class: isSelected ? 'selected' : '', + onclick: (e) => { + e.preventDefault(); + e.stopPropagation(); + State.activeMenu = null; + State.selectedId = gxsId; + State.activeTab = 'chat'; + initializeDistantChat(); + m.redraw(); + }, + oncontextmenu: (e) => { + e.preventDefault(); + e.stopPropagation(); + State.selectedId = gxsId; + const container = document.querySelector('.friends-list-container'); + if (container) { + const parentRect = container.getBoundingClientRect(); + const top = e.clientY - parentRect.top; + const left = Math.min(Math.max(e.clientX - parentRect.left, 10), 160); + State.activeMenu = { gxsId, displayName, isContact: itemIsContact, top, left }; + } + m.redraw(); + }, + }, + [ + m('.chat-avatar-wrapper', [ + m(peopleUtil.UserAvatar, { + avatar: itemAvatar, + firstLetter: itemFirstLetter, + identityId: gxsId, + size: 40, + }), + m('.status-dot', { + style: { + backgroundColor: itemIsContact || itemIsOwn ? '#22c55e' : '#cbd5e1', + }, + }), + ]), + m('.chat-info', [ + m('.chat-name', displayName), + m('.chat-last-msg', lastMsgText), + ]), + m('.chat-meta', [ + relativeTimeStr && m('.chat-time', relativeTimeStr), + ]), + ] + ); + } + + // People tab list item return m( '.friend-list-item', { @@ -137,18 +258,7 @@ const PeopleSidebar = () => { 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 }; - } - } + State.activeMenu = null; const idChanged = State.selectedId !== gxsId; State.selectedId = gxsId; @@ -165,13 +275,14 @@ const PeopleSidebar = () => { oncontextmenu: (e) => { e.preventDefault(); e.stopPropagation(); + State.selectedId = gxsId; - 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 }; + const top = e.clientY - parentRect.top; + const left = Math.min(Math.max(e.clientX - parentRect.left, 10), 160); + State.activeMenu = { gxsId, displayName, isContact: itemIsContact, top, left }; } m.redraw(); }, @@ -206,6 +317,9 @@ const PeopleSidebar = () => { return m('.people-context-menu', { style: { top: `${menu.top}px`, + left: menu.left !== undefined ? `${menu.left}px` : '10px', + position: 'absolute', + zIndex: 1000, }, onclick: (e) => { e.stopPropagation(); diff --git a/webui-src/app/people/people_state.js b/webui-src/app/people/people_state.js index 4908946..cab28de 100644 --- a/webui-src/app/people/people_state.js +++ b/webui-src/app/people/people_state.js @@ -6,10 +6,12 @@ const peopleUtil = require('people/people_util'); const State = { searchString: '', selectedId: null, // GXS ID of the selected identity + mainTab: 'people', // 'people' | 'chats' activeFilter: 'contacts', // 'all' | 'contacts' | 'own' gxsIdToDetailsMap: {}, ownGxsIds: [], gpgToGxsIdMap: {}, + chatHistoryMap: {}, // gxsId -> { lastMsg, lastTime } showMailCompose: false, activeTab: 'details', selectedOwnGxsIdForChat: '', @@ -254,6 +256,18 @@ function stopStatusPolling() { State.distantChatStatus = null; } +function isSystemMsg(msgText) { + if (!msgText || typeof msgText !== 'string') return true; + const lower = msgText.toLowerCase(); + return ( + lower.includes('starting distant chat') || + lower.includes('please wait for secure tunnel') || + lower.includes('tunnel is secured') || + lower.includes('chat initiated') || + lower.includes('closed the conversation') + ); +} + function initializeDistantChat() { if (!State.selectedId || !State.selectedOwnGxsIdForChat) return; @@ -301,6 +315,18 @@ function loadChatMessages() { (data, success) => { if (success && data.msgs) { State.chatMessages = data.msgs; + const realUserMsgs = data.msgs.filter( + (m) => !m.isSystem && !isSystemMsg(m.message || m.msg) + ); + if (realUserMsgs.length > 0 && State.selectedId) { + const last = realUserMsgs[realUserMsgs.length - 1]; + State.chatHistoryMap[State.selectedId] = { + lastMsg: last.message || last.msg || '', + lastTime: last.sendTime || last.recvTime || Math.floor(Date.now() / 1000), + }; + } else if (State.selectedId) { + delete State.chatHistoryMap[State.selectedId]; + } m.redraw(); setTimeout(() => { const element = document.querySelector('.chat-messages'); @@ -333,6 +359,12 @@ function sendDistantChatMessage() { lobby_peer_gxs_id: State.selectedOwnGxsIdForChat, }; State.chatMessages.push(echoMsg); + if (State.selectedId) { + State.chatHistoryMap[State.selectedId] = { + lastMsg: text, + lastTime: Math.floor(Date.now() / 1000), + }; + } m.redraw(); setTimeout(() => { const element = document.querySelector('.chat-messages'); @@ -353,8 +385,94 @@ function sendDistantChatMessage() { ); } +function preloadAllChatHistory() { + rs.rsJsonApiRequest('/rsIdentity/getIdentitiesSummaries', {}, (data) => { + const ids = (data && data.ids) ? data.ids : (rs.userList.users || []); + if (!ids || ids.length === 0) return; + + ids.forEach((u) => { + const gxsId = typeof u === 'object' ? u.mGroupId : u; + if (!gxsId) return; + + // Check Distant Chat History (type: 2) + const distantPeerId = { + broadcast_status_peer_id: '00000000000000000000000000000000', + type: 2, // DISTANT + peer_id: '00000000000000000000000000000000', + distant_chat_id: gxsId, + lobby_id: { xstr64: '0' }, + }; + + rs.rsJsonApiRequest( + '/rsHistory/getMessages', + { + chatPeerId: distantPeerId, + 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[gxsId] = { + lastMsg: last.message || last.msg || '', + lastTime: last.sendTime || last.recvTime || Math.floor(Date.now() / 1000), + }; + m.redraw(); + } + } + } + ); + + // Also check Private Chat History (type: 1) if PGP ID is known + const details = State.gxsIdToDetailsMap[gxsId]; + const pgpId = details ? details.mPgpId : (typeof u === 'object' ? u.mPgpId : null); + if (pgpId && pgpId !== '0000000000000000') { + const privatePeerId = { + broadcast_status_peer_id: '00000000000000000000000000000000', + type: 1, // PRIVATE + peer_id: pgpId, + distant_chat_id: '00000000000000000000000000000000', + lobby_id: { xstr64: '0' }, + }; + + 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]; + const existing = State.chatHistoryMap[gxsId]; + const lastTime = last.sendTime || last.recvTime || Math.floor(Date.now() / 1000); + if (!existing || lastTime > existing.lastTime) { + State.chatHistoryMap[gxsId] = { + lastMsg: last.message || last.msg || '', + lastTime: lastTime, + }; + m.redraw(); + } + } + } + } + ); + } + }); + }); +} + module.exports = { State, + isSystemMsg, + preloadAllChatHistory, fetchIdDetails, loadGxsIdentities, loadOwnGxsIds, From 545d38825729cbcb59a361e00cf8e7c1c099ef47 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Fri, 24 Jul 2026 23:29:18 +0200 Subject: [PATCH 27/40] Fixed chat scrollbar missed to commit changes for people page --- webui-src/styles.css | 264 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 209 insertions(+), 55 deletions(-) diff --git a/webui-src/styles.css b/webui-src/styles.css index cb79ce8..cac78bd 100644 --- a/webui-src/styles.css +++ b/webui-src/styles.css @@ -301,7 +301,7 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem .network-detail-view .detail-header { display: flex; - align-items: center; + align-items: flex-start; gap: 1.5rem; padding-bottom: 1.5rem; border-bottom: 1px solid #e2e8f0; @@ -309,6 +309,9 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem .network-detail-view .detail-header .detail-title { flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; } .network-detail-view .detail-header .detail-title h2 { @@ -324,10 +327,12 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem display: flex; align-items: center; gap: 0.5rem; + margin-bottom: 1.25rem; } .network-detail-view .detail-header .detail-actions { display: flex; + flex-wrap: wrap; gap: 0.75rem; } @@ -606,52 +611,197 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem background-color: #f8fafc; } -.people-filter-group { +/* Header Search Bar at top of People sidebar */ +.people-sidebar-header { display: flex; - padding: 0.75rem 1rem 0.25rem 1rem; - gap: 0.25rem; + flex-direction: column; + padding: 0.75rem 1rem 0.5rem 1rem; + gap: 0.75rem; border-bottom: 1px solid #e2e8f0; + background-color: #ffffff; } -.people-filter-group button.filter-btn { - flex: 1; - padding: 0.375rem 0.5rem; - font-size: 0.85rem; - font-weight: 600; - color: #64748b; - background-color: #f1f5f9; - border: none; - border-radius: 0.375rem; - cursor: pointer; - box-shadow: none; - transition: all 0.2s; -} - -.people-filter-group button.filter-btn:hover { - background-color: #e2e8f0; - color: #334155; -} - -.people-filter-group button.filter-btn.active { - background-color: #3ba4d7; - color: #ffffff; -} - -.people-left-pane .create-id-container { - padding: 0.75rem 1rem; - border-bottom: 1px solid #e2e8f0; +.people-sidebar-header .searchbar-wrapper { + position: relative; display: flex; + align-items: center; } -.people-left-pane .create-id-container button.create-id-btn { +.people-sidebar-header .searchbar-wrapper i.fa-search { + position: absolute; + left: 0.85rem; + color: #94a3b8; + font-size: 0.9rem; +} + +.people-sidebar-header .searchbar-wrapper input.searchbar-input { width: 100%; + padding: 0.5rem 0.75rem 0.5rem 2.25rem; + border: 1px solid #e2e8f0; + border-radius: 0.5rem; + font-size: 0.9rem; + background-color: #f8fafc; + color: #1e293b; + outline: none; + transition: all 0.2s ease; +} + +.people-sidebar-header .searchbar-wrapper input.searchbar-input:focus { + border-color: #3b82f6; + background-color: #ffffff; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); +} + +/* Dual Segmented Tab Control: People | Chats */ +.people-sidebar-header .segmented-control { + display: flex; + background-color: #f1f5f9; + padding: 3px; + border-radius: 0.5rem; + gap: 4px; +} + +.people-sidebar-header .segmented-control button.segment-tab { + flex: 1; display: flex; align-items: center; justify-content: center; gap: 0.5rem; - padding: 0.5rem; - font-weight: 600; + padding: 0.5rem 0.75rem; font-size: 0.9rem; + font-weight: 600; + color: #64748b; + background: transparent; + border: none; + border-radius: 0.375rem; + cursor: pointer; + box-shadow: none; + transition: all 0.2s ease; +} + +.people-sidebar-header .segmented-control button.segment-tab:hover { + color: #1e293b; +} + +.people-sidebar-header .segmented-control button.segment-tab.active { + background-color: #ffffff; + color: #0f172a; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); +} + +/* Sub-header Filter Row */ +.people-sidebar-header .sub-filter-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + min-height: 32px; +} + +.people-sidebar-header .sub-filter-row select.filter-select { + padding: 0.35rem 0.6rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + font-size: 0.85rem; + font-weight: 600; + color: #475569; + background-color: #ffffff; + cursor: pointer; + outline: none; +} + +.people-sidebar-header .sub-filter-row .btn-add-id { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 0.375rem; + background-color: #3b82f6; + color: #ffffff; + border: none; + cursor: pointer; + font-size: 0.9rem; + transition: background-color 0.2s; +} + +.people-sidebar-header .sub-filter-row .btn-add-id:hover { + background-color: #2563eb; +} + +/* Recent Chats List Item */ +.people-left-pane .chat-item { + display: flex; + align-items: center; + padding: 0.75rem 1rem; + gap: 0.75rem; + border-bottom: 1px solid #f1f5f9; + cursor: pointer; + transition: background-color 0.15s ease; + position: relative; +} + +.people-left-pane .chat-item:hover { + background-color: #f8fafc; +} + +.people-left-pane .chat-item.selected { + background-color: #eff6ff; + border-left: 3px solid #3b82f6; +} + +.people-left-pane .chat-item .chat-avatar-wrapper { + position: relative; + flex-shrink: 0; +} + +.people-left-pane .chat-item .chat-avatar-wrapper .status-dot { + position: absolute; + bottom: 0; + right: 0; + width: 10px; + height: 10px; + border-radius: 50%; + border: 2px solid #ffffff; +} + +.people-left-pane .chat-item .chat-info { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 0.15rem; +} + +.people-left-pane .chat-item .chat-info .chat-name { + font-size: 0.9rem; + font-weight: 700; + color: #1e293b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.people-left-pane .chat-item .chat-info .chat-last-msg { + font-size: 0.8rem; + color: #64748b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.people-left-pane .chat-item .chat-meta { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.25rem; + flex-shrink: 0; +} + +.people-left-pane .chat-item .chat-meta .chat-time { + font-size: 0.75rem; + color: #94a3b8; + white-space: nowrap; } /* ===================================================== @@ -1225,6 +1375,8 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem flex: 1; height: 100%; overflow: hidden; + min-height: 0; + min-width: 0; } .chat-hub-rightbar { @@ -1277,28 +1429,32 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem color: #0f172a; } -.chat-hub-rightbar .user-tooltip { +.user-tooltip { position: absolute; - left: -275px; - transform: translateY(-50%); width: 260px; background-color: #ffffe1; border: 1px solid #7f7f7f; box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.25); padding: 0.5rem; border-radius: 0.25rem; - z-index: 1000; + z-index: 10000; white-space: normal; display: flex; gap: 0.5rem; align-items: flex-start; } -.chat-hub-rightbar .user-tooltip .tooltip-avatar { +.chat-hub-rightbar .user-tooltip { + left: -275px; + transform: translateY(-50%); + z-index: 1000; +} + +.user-tooltip .tooltip-avatar { flex-shrink: 0; } -.chat-hub-rightbar .user-tooltip .tooltip-details { +.user-tooltip .tooltip-details { display: flex; flex-direction: column; gap: 0.25rem; @@ -1307,20 +1463,20 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem text-align: left; } -.chat-hub-rightbar .user-tooltip .tooltip-row { +.user-tooltip .tooltip-row { line-height: 1.2; } -.chat-hub-rightbar .user-tooltip .tooltip-label { +.user-tooltip .tooltip-label { font-weight: bold; } -.chat-hub-rightbar .user-tooltip .tooltip-value { +.user-tooltip .tooltip-value { font-weight: normal; word-break: break-all; } -.chat-hub-rightbar .user-tooltip .tooltip-value.tooltip-id { +.user-tooltip .tooltip-value.tooltip-id { font-family: monospace; } @@ -1489,7 +1645,7 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem .chat-hub-messages.compact-container, .messages.compact-container { gap: 0 !important; - padding: 0.75rem 1rem !important; + padding: 0.5rem 0.75rem !important; background-color: #ffffff !important; display: flex !important; flex-direction: column !important; @@ -1502,26 +1658,24 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem .messages.compact-container .message.compact { display: block !important; max-width: 100% !important; - padding: 0.15rem 0 !important; - border-radius: 0 !important; + padding: 1px 4px !important; + border-radius: 2px !important; background-color: transparent !important; border: none !important; box-shadow: none !important; - align-self: flex-start !important; - font-size: 1rem !important; - line-height: 1.5 !important; + align-self: stretch !important; + font-size: 0.9rem !important; + line-height: 1.35 !important; margin: 0 !important; - white-space: nowrap !important; - overflow: hidden !important; - text-overflow: ellipsis !important; + white-space: normal !important; + word-break: break-word !important; + overflow: visible !important; width: 100% !important; } .chat-hub-messages.compact-container .message.compact:hover, .messages.compact-container .message.compact:hover { background-color: #f8fafc !important; - overflow: visible !important; - white-space: normal !important; } .chat-hub-messages.compact-container .message.compact .datetime, From b97c7392198a46ca63fab378066a1002fe2c2bdb Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:06:05 +0200 Subject: [PATCH 28/40] Added missed configs to network config Fixed some layouts issues --- webui-src/app/config/config_network.js | 519 +++++++++++++++---------- 1 file changed, 321 insertions(+), 198 deletions(-) diff --git a/webui-src/app/config/config_network.js b/webui-src/app/config/config_network.js index 7045256..1af47d6 100644 --- a/webui-src/app/config/config_network.js +++ b/webui-src/app/config/config_network.js @@ -76,13 +76,15 @@ const SetNwMode = () => { }, view: (vnode) => { const isHiddenMode = vnode.attrs && vnode.attrs.isHiddenMode; + const hideLabel = vnode.attrs && vnode.attrs.hideLabel; const modes = isHiddenMode ? hiddenModes : networkModes; return [ - m('p', isHiddenMode ? 'Discovery:' : 'Network mode:'), + !hideLabel && m('p', isHiddenMode ? 'Discovery:' : 'Network mode:'), m( 'select', { + style: 'flex: 1; max-width: 320px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px;', value: selectedMode, onchange: (e) => { const idx = e.target.selectedIndex; @@ -130,52 +132,6 @@ const SetNwMode = () => { }; }; -const SetNAT = () => { - let sslId; - let netMode; - - return { - oninit: () => { - rs.rsJsonApiRequest('/rsAccounts/getCurrentAccountId').then((res) => { - if (res.body.retval) { - sslId = res.body.id; - rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { - sslId, - }).then((res) => { - if (res.body.retval) { - netMode = res.body.det.netMode; - } - }); - } - }); - }, - view: () => [ - m('p', 'NAT:'), - m( - 'select', - { - value: netMode, - onchange: (e) => { - rs.rsJsonApiRequest('/rsPeers/setNetworkMode', { - sslId, - netMode, - }).then((res) => { - if (res.body.retval) { - netMode = e.target.value; - } - }); - }, - }, - [ - m('option', { value: util.RS_NETMODE_UPNP }, 'Automatic (UPnP)'), - m('option', { value: util.RS_NETMODE_UDP }, 'FireWalled'), - m('option', { value: util.RS_NETMODE_EXT }, 'Manually Forwarded Port'), - ] - ), - ], - }; -}; - const SetLimits = () => { let dlim = undefined; let ulim = undefined; @@ -191,37 +147,41 @@ const SetLimits = () => { ulim = data.outKb; }), view: () => [ - m( - 'p', - util.tooltip( - 'The download limit covers the whole application. ' + - 'However, in some situations, such as when transfering ' + - 'many files at once, the estimated bandwidth becomes ' + - 'unreliable and the total value reported by Retroshare ' + - 'might exceed that limit.' - ), - 'Download limit(KB/s):' - ), - m('input[type=number][name=download]', { - value: dlim, - oninput: (e) => (dlim = Number(e.target.value)), - onchange: setMaxRates, - }), - m( - 'p', - util.tooltip( - 'The upload limit covers the entire software. ' + - 'Too small an upload limit may eventually block ' + - 'low priority services(forums, channels). ' + - 'A minimum recommended value is 50KB/s.' - ), - 'Upload limit(KB/s):' - ), - m('input[type=number][name=upload]', { - value: ulim, - oninput: (e) => (ulim = Number(e.target.value)), - onchange: setMaxRates, - }), + m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center; margin-bottom: 0.75rem;' }, [ + m('p', { style: 'font-weight: 600; color: #475569;' }, [ + util.tooltip( + 'The download limit covers the whole application. ' + + 'However, in some situations, such as when transfering ' + + 'many files at once, the estimated bandwidth becomes ' + + 'unreliable and the total value reported by Retroshare ' + + 'might exceed that limit.' + ), + 'Download limit(KB/s):' + ]), + m('input[type=number][name=download]', { + style: 'padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px; max-width: 320px; width: 100%;', + value: dlim, + oninput: (e) => (dlim = Number(e.target.value)), + onchange: setMaxRates, + }), + ]), + m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center; margin-bottom: 0.75rem;' }, [ + m('p', { style: 'font-weight: 600; color: #475569;' }, [ + util.tooltip( + 'The upload limit covers the entire software. ' + + 'Too small an upload limit may eventually block ' + + 'low priority services(forums, channels). ' + + 'A minimum recommended value is 50KB/s.' + ), + 'Upload limit(KB/s):' + ]), + m('input[type=number][name=upload]', { + style: 'padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px; max-width: 320px; width: 100%;', + value: ulim, + oninput: (e) => (ulim = Number(e.target.value)), + onchange: setMaxRates, + }), + ]), ], }; }; @@ -236,87 +196,241 @@ const SetOpMode = () => { oninit: () => rs.rsJsonApiRequest('/rsConfig/getOperatingMode', {}, (data) => (opmode = data.retval)), view: () => [ - m( - 'p', - 'Operating mode:', - util.tooltip( - `No Anon D/L: Switches off file forwarding\n - Gaming Mode: 25% standard traffic and TODO: Reduced popups\n - Low traffic: 10% standard traffic and TODO: pause all file transfers\n` - ) - ), - m( - 'select', - { - oninput: (e) => (opmode = e.target.value), - value: opmode, - onchange: setmode, - }, - ['Normal', 'No Anon D/L', 'Gaming', 'Low traffic'].map((val, i) => - m(`option[value=${i + 1}]`, val) - ) - ), + m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center; margin-bottom: 0.75rem;' }, [ + m('p', { style: 'font-weight: 600; color: #475569;' }, [ + 'Operating mode: ', + util.tooltip( + `No Anon D/L: Switches off file forwarding\n + Gaming Mode: 25% standard traffic and TODO: Reduced popups\n + Low traffic: 10% standard traffic and TODO: pause all file transfers\n` + ) + ]), + m( + 'select', + { + style: 'padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px; max-width: 320px; width: 100%;', + oninput: (e) => (opmode = e.target.value), + value: opmode, + onchange: setmode, + }, + ['Normal', 'No Anon D/L', 'Gaming', 'Low traffic'].map((val, i) => + m(`option[value=${i + 1}]`, val) + ) + ), + ]), ], }; }; -const displayLocalIPAddress = () => { - return { - view: ({ attrs: { details } }) => - details && [m('p', 'Local Address: '), m('p', details.localAddr)], - }; -}; -const displayExternalIPAddress = () => { - return { - view: ({ attrs: { details, isHiddenMode } }) => - details && [m('p', 'External Address: '), m('p', isHiddenMode ? 'Hidden - See Config' : details.extAddr)], - }; -}; - const displayIPAddresses = () => { return { view: ({ attrs: { details } }) => - details && [ - m('p', 'External Address: '), + details && m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: flex-start; margin-bottom: 0.75rem;' }, [ + m('p', { style: 'font-weight: 600; color: #475569;' }, 'External Address: '), m( 'ul.external-address', details.ipAddressList.map((ip) => m('li', ip)) ), - ], + ]), }; }; -const SetDynamicDNS = () => { - let addr = ''; +const NetworkConfigForm = () => { let sslId = ''; + let details = {}; + let netStatus = {}; + let localAddr = ''; + let localPort = 0; + let extAddr = ''; + let extPort = 0; + let dyndns = ''; + let netMode = util.RS_NETMODE_EXT; + + const loadData = () => { + rs.rsJsonApiRequest('/rsAccounts/getCurrentAccountId').then((res) => { + if (res.body.retval) { + sslId = res.body.id; + rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { sslId }).then((pRes) => { + if (pRes.body.retval) { + details = pRes.body.det; + localAddr = details.localAddr || ''; + localPort = details.localPort || 0; + extAddr = details.extAddr || ''; + extPort = details.extPort || 0; + dyndns = details.dyndns || ''; + netMode = details.netMode || util.RS_NETMODE_EXT; + m.redraw(); + } + }); + rs.rsJsonApiRequest('/rsConfig/getConfigNetStatus', {}).then((nRes) => { + if (nRes.body) { + netStatus = nRes.body; + if (netStatus.localPort) localPort = netStatus.localPort; + if (netStatus.extPort) extPort = netStatus.extPort; + m.redraw(); + } + }); + } + }); + }; + + const saveLocalAddress = () => { + if (!sslId) return; + rs.rsJsonApiRequest('/rsPeers/setLocalAddress', { + sslId, + addr: localAddr, + port: parseInt(localPort) || 0, + }).then(() => loadData()); + }; + + const saveExtAddress = () => { + if (!sslId) return; + rs.rsJsonApiRequest('/rsPeers/setExtAddress', { + sslId, + addr: extAddr, + port: parseInt(extPort) || 0, + }).then(() => loadData()); + }; + + const saveDynDNS = () => { + if (!sslId) return; + rs.rsJsonApiRequest('/rsPeers/setDynDNS', { + sslId, + addr: dyndns, + }); + }; + + const saveNetMode = (newMode) => { + if (!sslId) return; + netMode = newMode; + rs.rsJsonApiRequest('/rsPeers/setNetworkMode', { + sslId, + netMode: parseInt(newMode), + }).then(() => loadData()); + }; + return { oninit: () => { - rs.rsJsonApiRequest('/rsAccounts/getCurrentAccountId').then((res) => { - if (res.body.retval) { - sslId = res.body.id; - rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { - sslId, - }).then((res) => { - if (res.body.retval) { - addr = res.body.det.dyndns; - } - }); - } - }); + loadData(); }, - view: () => [ - m('p', 'Set Dynamic DNS:'), - m('input[type=text]', { - value: addr, - oninput: (e) => (addr = e.target.value), - onchange: () => { - rs.rsJsonApiRequest('/rsPeers/setDynDNS', { - sslId, - addr, - }); - }, - }), - ], + view: ({ attrs: { isHiddenMode } }) => { + const isUpnpOk = Boolean(netStatus.netUpnpOk || netStatus.uPnPActive); + const isLocalOk = Boolean(netStatus.netLocalOk !== false); + const isExtOk = Boolean(netStatus.netExtAddressOk); + + return m('.network-config-form', { + style: { + display: 'flex', + flexDirection: 'column', + gap: '0.75rem', + width: '100%', + } + }, [ + // Network Mode row + m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center;' }, [ + m('label', { style: 'font-weight: 600; color: #475569;' }, 'Network Mode'), + m('.nw-mode-group', { style: 'display: flex; align-items: center; gap: 1rem;' }, [ + m(SetNwMode, { isHiddenMode, hideLabel: true }), + isHiddenMode && m('.status-indicator', { style: 'display: flex; align-items: center; gap: 0.4rem;' }, [ + m('.bullet', { + style: 'width: 10px; height: 10px; border-radius: 50%; background-color: #22c55e;' + }), + m('span', { style: 'font-size: 0.85rem; font-weight: 700; color: #000000;' }, '[Hidden mode]'), + ]), + ]), + ]), + + // NAT row + UPnP status bullet + !isHiddenMode && m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center;' }, [ + m('label', { style: 'font-weight: 600; color: #475569;' }, 'NAT'), + m('.nat-control-group', { style: 'display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;' }, [ + m('select', { + style: 'flex: 1; max-width: 320px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px;', + value: netMode, + onchange: (e) => saveNetMode(e.target.value), + }, [ + m('option', { value: util.RS_NETMODE_UPNP }, 'Automatic (UPnP)'), + m('option', { value: util.RS_NETMODE_UDP }, 'FireWalled'), + m('option', { value: util.RS_NETMODE_EXT }, 'Manually Forwarded Port'), + ]), + m('.status-indicator', { style: 'display: flex; align-items: center; gap: 0.4rem;' }, [ + m('.bullet', { + style: `width: 10px; height: 10px; border-radius: 50%; background-color: ${isUpnpOk ? '#22c55e' : '#475569'};` + }), + m('span', { style: 'font-size: 0.85rem; font-weight: 600; color: #334155;' }, 'UPnP'), + ]), + ]), + ]), + + // Local Address + Port + Local network status bullet + m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center;' }, [ + m('label', { style: 'font-weight: 600; color: #475569;' }, 'Local Address'), + m('.addr-control-group', { style: 'display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;' }, [ + m('input[type=text]', { + style: 'flex: 1; max-width: 320px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px;', + value: localAddr, + oninput: (e) => (localAddr = e.target.value), + onchange: saveLocalAddress, + }), + m('.port-group', { style: 'display: flex; align-items: center; gap: 0.4rem;' }, [ + m('span', { style: 'font-size: 0.85rem; font-weight: 600; color: #475569;' }, 'Port:'), + m('input[type=number]', { + style: 'width: 90px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px;', + value: localPort, + oninput: (e) => (localPort = parseInt(e.target.value) || 0), + onchange: saveLocalAddress, + }), + ]), + !isHiddenMode && m('.status-indicator', { style: 'display: flex; align-items: center; gap: 0.4rem; margin-left: 0.5rem;' }, [ + m('.bullet', { + style: `width: 10px; height: 10px; border-radius: 50%; background-color: ${isLocalOk ? '#22c55e' : '#ef4444'};` + }), + m('span', { style: 'font-size: 0.85rem; font-weight: 600; color: #334155;' }, 'Local network'), + ]), + ]), + ]), + + // External Address + Port + External ip address finder status bullet + m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center;' }, [ + m('label', { style: 'font-weight: 600; color: #475569;' }, 'External Address'), + m('.addr-control-group', { style: 'display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;' }, [ + m('input[type=text]', { + style: 'flex: 1; max-width: 320px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px;', + value: isHiddenMode ? 'Hidden' : extAddr, + disabled: isHiddenMode, + oninput: (e) => (extAddr = e.target.value), + onchange: saveExtAddress, + }), + !isHiddenMode && m('.port-group', { style: 'display: flex; align-items: center; gap: 0.4rem;' }, [ + m('span', { style: 'font-size: 0.85rem; font-weight: 600; color: #475569;' }, 'Port:'), + m('input[type=number]', { + style: 'width: 90px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px;', + value: extPort, + oninput: (e) => (extPort = parseInt(e.target.value) || 0), + onchange: saveExtAddress, + }), + ]), + !isHiddenMode && m('.status-indicator', { style: 'display: flex; align-items: center; gap: 0.4rem; margin-left: 0.5rem;' }, [ + m('.bullet', { + style: `width: 10px; height: 10px; border-radius: 50%; background-color: ${isExtOk ? '#22c55e' : '#808080'};` + }), + m('span', { style: 'font-size: 0.85rem; font-weight: 600; color: #334155;' }, 'External ip address finder'), + ]), + ]), + ]), + + // Dynamic DNS row + !isHiddenMode && m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center;' }, [ + m('label', { style: 'font-weight: 600; color: #475569;' }, 'Dynamic DNS'), + m('input[type=text]', { + style: 'flex: 1; max-width: 320px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px;', + value: dyndns, + oninput: (e) => (dyndns = e.target.value), + onchange: saveDynDNS, + }), + ]), + ]); + } }; }; @@ -416,46 +530,45 @@ const SetSocksProxy = () => { }); }, view: () => - m('.proxy-server', [ - m( - 'p', + m('.proxy-server-form', { style: 'display: flex; flex-direction: column; gap: 0.75rem; width: 100%;' }, [ + m('p.proxy-description', { style: 'margin-bottom: 0.5rem; color: #475569;' }, 'Configure your TOR and I2P SOCKS proxy here. It will allow you to also connect to hidden nodes.' ), - m('.proxy-rows-container', - Object.keys(socksProxyObj).map((proxyItem) => { - const isTor = proxyItem === 'tor'; - const labelText = isTor ? 'TOR Socks Proxy:' : 'I2P Socks Proxy:'; - const outgoingText = isTor ? 'TOR outgoing' : 'I2P outgoing'; - const notEnabledText = isTor ? 'Tor proxy is not enabled' : 'I2P proxy is not enabled'; - const isOutgoing = socksProxyObj[proxyItem].outgoing; - return m('.proxy-row', [ - m('label.proxy-label', labelText), - m('input[type=text].proxy-addr-input', { - value: socksProxyObj[proxyItem].addr, + Object.keys(socksProxyObj).map((proxyItem) => { + const isTor = proxyItem === 'tor'; + const labelText = isTor ? 'TOR Socks Proxy:' : 'I2P Socks Proxy:'; + const outgoingText = isTor ? 'TOR outgoing' : 'I2P outgoing'; + const notEnabledText = isTor ? 'Tor proxy is not enabled' : 'I2P proxy is not enabled'; + const isOutgoing = socksProxyObj[proxyItem].outgoing; + + return m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center;' }, [ + m('label', { style: 'font-weight: 600; color: #475569;' }, labelText), + m('.proxy-control-group', { style: 'display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap;' }, [ + m('input[type=text]', { + style: 'flex: 1; max-width: 480px; min-width: 320px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px;', + value: socksProxyObj[proxyItem].addr || '', oninput: (e) => (socksProxyObj[proxyItem].addr = e.target.value), onchange: () => handleProxyChange(proxyItem), }), - m('input[type=number].proxy-port-input', { - value: socksProxyObj[proxyItem].port, - oninput: (e) => (socksProxyObj[proxyItem].port = parseInt(e.target.value)), + m('input[type=number]', { + style: 'width: 90px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px;', + value: socksProxyObj[proxyItem].port || 0, + oninput: (e) => (socksProxyObj[proxyItem].port = parseInt(e.target.value) || 0), onchange: () => handleProxyChange(proxyItem), }), socksProxyObj[proxyItem].outgoing !== undefined && - m('.proxy-status-container', [ - m('.proxy-status-bullet', { - style: { - backgroundColor: isOutgoing ? '#22c55e' : '#808080', - }, + m('.status-indicator', { style: 'display: flex; align-items: center; gap: 0.4rem; margin-left: 0.5rem;' }, [ + m('.bullet', { + style: `width: 10px; height: 10px; border-radius: 50%; background-color: ${isOutgoing ? '#22c55e' : '#808080'};`, title: isOutgoing ? 'Proxy seems to work.' : notEnabledText, }), - m( - 'span.proxy-status-text', + m('span', { style: 'font-size: 0.85rem; font-weight: 600; color: #334155;' }, `${outgoingText} ${isOutgoing ? 'on' : 'off'}` ), ]), - ]); - }) - ), + ]), + ]); + }), ]), }; }; @@ -464,27 +577,42 @@ const displayHiddenServiceInfo = () => { return { view: ({ attrs: { details } }) => details && details.hiddenNodeAddress && - m('.proxy-server', [ - m('p.proxy-description', details.hiddenType === 4 - ? 'I2P has been automatically configured by Retroshare. You shouldn\'t need to change anything here.' - : 'Tor has been automatically configured by Retroshare. You shouldn\'t need to change anything here.' + m('.hidden-service-info', { style: 'display: flex; flex-direction: column; gap: 0.75rem; width: 100%;' }, [ + m('p.proxy-description', { style: 'margin-bottom: 0.5rem; color: #475569;' }, details.hiddenType === 4 + ? "I2P has been automatically configured by Retroshare. You shouldn't need to change anything here." + : "Tor has been automatically configured by Retroshare. You shouldn't need to change anything here." ), - m('hr'), - m('.proxy-row', [ - m('label.proxy-label', 'Local Address:'), - m('span', details.localAddr || '127.0.0.1'), + // Local Address + Local Port row + m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center;' }, [ + m('label', { style: 'font-weight: 600; color: #475569;' }, 'Local Address:'), + m('.addr-port-group', { style: 'display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap;' }, [ + m('input[type=text]', { + style: 'flex: 1; max-width: 480px; min-width: 320px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px; background-color: #f8fafc; color: #334155;', + readOnly: true, + value: details.localAddr || '127.0.0.1', + }), + m('input[type=number]', { + style: 'width: 90px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px; background-color: #f8fafc; color: #334155;', + readOnly: true, + value: details.localPort || 0, + }), + ]), ]), - m('.proxy-row', [ - m('label.proxy-label', details.hiddenType === 4 ? 'I2P Address:' : 'Onion Address:'), - m('span', details.hiddenNodeAddress), - ]), - details.hiddenNodePort && m('.proxy-row', [ - m('label.proxy-label', 'Service Port:'), - m('span', String(details.hiddenNodePort)), - ]), - m('.proxy-row', [ - m('label.proxy-label', 'Local Port:'), - m('span', String(details.localPort)), + // Onion / I2P Address + Service Port row + m('.nw-config-row', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center;' }, [ + m('label', { style: 'font-weight: 600; color: #475569;' }, details.hiddenType === 4 ? 'I2P Address:' : 'Onion Address:'), + m('.addr-port-group', { style: 'display: flex; align-items: center; gap: 0.75rem; flex-wrap: wrap;' }, [ + m('input[type=text]', { + style: 'flex: 1; max-width: 480px; min-width: 320px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px; background-color: #f8fafc; color: #334155; font-family: monospace;', + readOnly: true, + value: details.hiddenNodeAddress, + }), + details.hiddenNodePort && m('input[type=number]', { + style: 'width: 90px; padding: 0.4rem; border: 1px solid #cbd5e1; border-radius: 4px; background-color: #f8fafc; color: #334155;', + readOnly: true, + value: details.hiddenNodePort, + }), + ]), ]), ]), }; @@ -517,20 +645,15 @@ const Component = () => { }); }, view: () => - m('.config-network', { style: 'display:flex; flex-direction:column; gap:0.5rem;' }, [ + m('.config-network', { style: 'display:flex; flex-direction:column; gap:1rem;' }, [ m('.widget', [ m('.widget__heading', m('h3', 'Network Configuration')), m('.widget__body', [ - m('.grid-2col', [ - m(SetNwMode, { isHiddenMode }), - !isHiddenMode && m(SetNAT), - m(displayLocalIPAddress, { details }), - m(displayExternalIPAddress, { details, isHiddenMode }), - !isHiddenMode && m(SetDynamicDNS), - m(SetLimits), - !isHiddenMode && m(SetOpMode), - !isHiddenMode && m(displayIPAddresses, { details }), - ]), + m(NetworkConfigForm, { isHiddenMode }), + m('hr', { style: 'margin: 1rem 0; border: none; border-top: 1px solid #e2e8f0;' }), + m(SetLimits), + !isHiddenMode && m(SetOpMode), + !isHiddenMode && m(displayIPAddresses, { details }), ]), ]), m('.widget', [ From 33e7e867d7683b48c76b1e159990e91939b51934 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sat, 25 Jul 2026 16:09:00 +0200 Subject: [PATCH 29/40] Fixed issue to load chat images * Added attach image button * Improved status bar to retrieve nat status & down/up infos * torstatus added but not yet functional needs the api --- webui-src/app/chat/chat.js | 103 ++++++++- webui-src/app/chat/chat_emoji.js | 16 +- webui-src/app/chat/chat_state.js | 128 +++++++++-- webui-src/app/people/people_chat_tab.js | 181 +++++++++++++-- webui-src/app/people/people_state.js | 109 ++++----- webui-src/app/statusbar.js | 279 ++++++++++++++++++++++-- webui-src/styles.css | 42 ++-- 7 files changed, 728 insertions(+), 130 deletions(-) diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index 4ea9b6d..dedb9c3 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -24,7 +24,53 @@ const { chatEmoji.setDependencies({ ChatHubState }); -// ************************* helpers **************************** +// Mirroring C++ RsHtml::makeEmbeddedImage for resizing chat images to fit RetroShare max packet limit (~30KB) +function formatChatImage(file, callback) { + if (!file) return; + const reader = new FileReader(); + reader.onload = (evt) => { + const img = new Image(); + img.onload = () => { + // Bounding box for chat images: 420x320 max + const maxWidth = 420; + const maxHeight = 320; + let width = img.width; + let height = img.height; + + if (width > maxWidth || height > maxHeight) { + const ratio = Math.min(maxWidth / width, maxHeight / height); + width = Math.round(width * ratio); + height = Math.round(height * ratio); + } + + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, width, height); + + // Dynamically step down JPEG quality until base64 string is under 28,000 characters (28KB) + let quality = 0.70; + let dataUrl = canvas.toDataURL('image/jpeg', quality); + while (dataUrl.length > 28000 && quality > 0.15) { + quality -= 0.10; + dataUrl = canvas.toDataURL('image/jpeg', quality); + } + + if (dataUrl.length <= 32000) { + callback(``); + } else { + alert('Image file is too large to send over RetroShare chat packet size limit.'); + callback(null); + } + }; + img.onerror = () => { + callback(null); + }; + img.src = evt.target.result; + }; + reader.readAsDataURL(file); +} function loadOwnChatProfile() { rs.rsJsonApiRequest('/rsConfig/getConfigNetStatus', {}, (data) => { @@ -286,7 +332,7 @@ const ChatConversationView = () => { '.chat-hub-input-area', [ m( - 'button.chat-hub-attach-btn', + 'button.chat-hub-action-btn', { disabled: !canTalk, style: !canTalk ? 'opacity: 0.5; cursor: not-allowed;' : '', @@ -300,7 +346,7 @@ const ChatConversationView = () => { ), m('.emoji-picker-wrapper', [ m( - 'button.chat-hub-emoji-btn', + 'button.chat-hub-action-btn', { disabled: !canTalk, style: !canTalk ? 'opacity: 0.5; cursor: not-allowed;' : '', @@ -310,14 +356,61 @@ const ChatConversationView = () => { ChatHubState.showEmojiPicker = !ChatHubState.showEmojiPicker; }, }, - '😊' + m('i.fas.fa-smile') ), ChatHubState.showEmojiPicker && m(chatEmoji.EmojiPicker), ]), + m('label.chat-hub-action-btn', { + title: 'Send image', + style: `cursor: ${canTalk ? 'pointer' : 'not-allowed'}; opacity: ${canTalk ? 1 : 0.5};`, + }, [ + m('i.fas.fa-image'), + m('input[type=file][accept=image/*]', { + style: 'display: none;', + disabled: !canTalk, + onchange: (e) => { + if (!e.target.files || !e.target.files[0]) return; + const file = e.target.files[0]; + const textarea = e.target.closest('.chat-hub-input-area').querySelector('textarea'); + formatChatImage(file, (imgTag) => { + if (imgTag && textarea) { + const start = textarea.selectionStart || 0; + const end = textarea.selectionEnd || 0; + const val = textarea.value; + textarea.value = val.substring(0, start) + imgTag + val.substring(end); + m.redraw(); + } + }); + e.target.value = ''; + } + }) + ]), m('textarea.chat-hub-textarea', { - placeholder: canTalk ? 'Type a message... Press Enter to send' : 'Waiting for tunnel to be secured...', + placeholder: canTalk ? 'Type a message... Press Enter to send (or paste image)' : 'Waiting for tunnel to be secured...', disabled: !canTalk, enterkeyhint: 'send', + onpaste: (e) => { + if (!canTalk) return; + const items = (e.clipboardData || (e.originalEvent && e.originalEvent.clipboardData))?.items; + if (!items) return; + for (let i = 0; i < items.length; i++) { + if (items[i].type.indexOf('image') !== -1) { + e.preventDefault(); + const blob = items[i].getAsFile(); + const textarea = e.target; + formatChatImage(blob, (imgTag) => { + if (imgTag && textarea) { + const start = textarea.selectionStart || 0; + const end = textarea.selectionEnd || 0; + const val = textarea.value; + textarea.value = val.substring(0, start) + imgTag + val.substring(end); + m.redraw(); + } + }); + break; + } + } + }, onkeydown: (e) => { if ((e.key === 'Enter' || e.keyCode === 13) && !e.shiftKey) { if (!canTalk) return false; diff --git a/webui-src/app/chat/chat_emoji.js b/webui-src/app/chat/chat_emoji.js index 4d94d98..32da6d9 100644 --- a/webui-src/app/chat/chat_emoji.js +++ b/webui-src/app/chat/chat_emoji.js @@ -76,11 +76,15 @@ const EMOJI_DATA = { ], }; -function insertEmojiIntoTextarea(emoji) { - const textarea = document.querySelector('.chat-hub-textarea'); +function insertEmojiIntoTextarea(emoji, onSelect) { + if (typeof onSelect === 'function') { + onSelect(emoji); + return; + } + const textarea = document.querySelector('.chat-hub-textarea, .chat-textarea'); if (!textarea) return; - const start = textarea.selectionStart; - const end = textarea.selectionEnd; + const start = textarea.selectionStart || 0; + const end = textarea.selectionEnd || 0; const before = textarea.value.substring(0, start); const after = textarea.value.substring(end); textarea.value = before + emoji + after; @@ -91,7 +95,7 @@ function insertEmojiIntoTextarea(emoji) { } const EmojiPicker = () => ({ - view: () => { + view: ({ attrs: { onSelect } }) => { const search = ChatHubState.emojiSearch.toLowerCase(); const cat = ChatHubState.emojiCategory; let emojis; @@ -121,7 +125,7 @@ const EmojiPicker = () => ({ emojis.map(e => m('button.emoji-btn', { onclick: () => { - insertEmojiIntoTextarea(e); + insertEmojiIntoTextarea(e, onSelect); ChatHubState.showEmojiPicker = false; m.redraw(); }, diff --git a/webui-src/app/chat/chat_state.js b/webui-src/app/chat/chat_state.js index 54a4e62..3d04b1d 100644 --- a/webui-src/app/chat/chat_state.js +++ b/webui-src/app/chat/chat_state.js @@ -91,6 +91,104 @@ function getStatusTooltip(status) { } } +function renderChatMessage(rawText) { + if (!rawText) return ''; + + // 1. Check for HTML tags + const imgRegex = /]*src=["']([^"']+)["'][^>]*>/gi; + if (imgRegex.test(rawText)) { + imgRegex.lastIndex = 0; + const parts = []; + let lastIndex = 0; + let match; + + while ((match = imgRegex.exec(rawText)) !== null) { + if (match.index > lastIndex) { + const precedingText = rawText.substring(lastIndex, match.index); + const cleanText = precedingText + .replaceAll('
', '\n') + .replaceAll('
', '\n') + .replace(new RegExp('|<[^>]*>', 'gm'), ''); + if (cleanText) { + parts.push(renderTextWithEmoji(cleanText)); + } + } + + const src = match[1]; + if (src) { + parts.push( + m('img.chat-embedded-image', { + src: src, + style: { + maxWidth: '100%', + maxHeight: '300px', + borderRadius: '0.375rem', + marginTop: '0.25rem', + marginBottom: '0.25rem', + display: 'block', + cursor: 'pointer', + boxShadow: '0 1px 3px rgba(0,0,0,0.1)', + }, + onclick: () => { + const w = window.open(''); + if (w) { + w.document.write(``); + } + } + }) + ); + } + + lastIndex = imgRegex.lastIndex; + } + + if (lastIndex < rawText.length) { + const trailingText = rawText.substring(lastIndex); + const cleanText = trailingText + .replaceAll('
', '\n') + .replaceAll('
', '\n') + .replace(new RegExp('|<[^>]*>', 'gm'), ''); + if (cleanText) { + parts.push(renderTextWithEmoji(cleanText)); + } + } + + return parts.length > 0 ? parts : ''; + } + + // 2. Check for raw data:image/... base64 URLs + if (rawText.trim().startsWith('data:image/')) { + const src = rawText.trim(); + return m('img.chat-embedded-image', { + src: src, + style: { + maxWidth: '100%', + maxHeight: '300px', + borderRadius: '0.375rem', + marginTop: '0.25rem', + marginBottom: '0.25rem', + display: 'block', + cursor: 'pointer', + boxShadow: '0 1px 3px rgba(0,0,0,0.1)', + }, + onclick: () => { + const w = window.open(''); + if (w) { + w.document.write(``); + } + } + }); + } + + // 3. Normal text message + const cleanText = rawText + .replaceAll('
', '\n') + .replaceAll('
', '\n') + .replace(new RegExp('|<[^>]*>', 'gm'), ''); + + return renderTextWithEmoji(cleanText); +} + /** * Wraps emoji characters in a span so CSS can size them independently. */ @@ -278,9 +376,7 @@ const Message = () => { if (username === gxsId && gxsId && gxsId.length > 12) { username = gxsId.substring(0, 8) + '...'; } - const text = (msg.msg || msg.message || '') - .replaceAll('
', '\n') - .replace(new RegExp('|<[^>]*>', 'gm'), ''); + const rawText = msg.msg || msg.message || ''; const chatType = ChatLobbyModel.currentLobby && ChatLobbyModel.currentLobby.chatType; const isRoom = chatType === 3; @@ -291,7 +387,7 @@ const Message = () => { '.message.compact', m('span.datetime', datetime), m('span.username', { style: { color: nickColor } }, username + ':'), - m('span.messagetext', renderTextWithEmoji(text)) + m('span.messagetext', renderChatMessage(rawText)) ); } @@ -299,7 +395,7 @@ const Message = () => { '.message' + (msg.incoming ? '.incoming' : '.outgoing'), m('span.datetime', datetime), m('span.username', username), - m('span.messagetext', renderTextWithEmoji(text)) + m('span.messagetext', renderChatMessage(rawText)) ); }, }; @@ -600,13 +696,6 @@ const ChatLobbyModel = { }, sendMessage(msg, onsuccess) { const cid = this.chatId(); - const echoMsg = { - chat_id: cid, - msg: msg, - sendTime: Math.floor(Date.now() / 1000), - lobby_peer_gxs_id: this.currentLobby.gxs_id, - }; - this.addMessages([echoMsg], true); rs.rsJsonApiRequest( '/rsChats/sendChat', @@ -616,10 +705,18 @@ const ChatLobbyModel = { }, (data, success) => { if (success) { - onsuccess(); + const echoMsg = { + chat_id: cid, + msg: msg, + sendTime: Math.floor(Date.now() / 1000), + lobby_peer_gxs_id: this.currentLobby.gxs_id, + }; + this.addMessages([echoMsg], true); + if (onsuccess) onsuccess(); } else { - console.error('[RS] Failed to send chat message'); - onsuccess(); + console.error('[RS] Failed to send chat message:', data); + alert('Failed to send chat message. The image/payload exceeds RetroShare max chat packet size.'); + if (onsuccess) onsuccess(); } } ); @@ -686,6 +783,7 @@ module.exports = { getStatusColor, getStatusTooltip, renderTextWithEmoji, + renderChatMessage, getSafeAvatar, MobileState, ChatRoomsModel, diff --git a/webui-src/app/people/people_chat_tab.js b/webui-src/app/people/people_chat_tab.js index 82df32e..6c9b830 100644 --- a/webui-src/app/people/people_chat_tab.js +++ b/webui-src/app/people/people_chat_tab.js @@ -9,6 +9,55 @@ const { sendDistantChatMessage, stopStatusPolling, } = require('people/people_state'); +const { renderChatMessage } = require('chat/chat_state'); +const chatEmoji = require('chat/chat_emoji'); +const peopleUtil = require('people/people_util'); + +// Mirroring C++ Distant Chat packet size limit (200KB) +function formatChatImage(file, callback) { + if (!file) return; + const reader = new FileReader(); + reader.onload = (evt) => { + const img = new Image(); + img.onload = () => { + // Bounding box for Distant Chat images: 800x600 max + const maxWidth = 800; + const maxHeight = 600; + let width = img.width; + let height = img.height; + + if (width > maxWidth || height > maxHeight) { + const ratio = Math.min(maxWidth / width, maxHeight / height); + width = Math.round(width * ratio); + height = Math.round(height * ratio); + } + + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, width, height); + + // Dynamically step down JPEG quality until base64 string is under 190,000 characters (190KB) + let quality = 0.85; + let dataUrl = canvas.toDataURL('image/jpeg', quality); + while (dataUrl.length > 190000 && quality > 0.20) { + quality -= 0.10; + dataUrl = canvas.toDataURL('image/jpeg', quality); + } + + if (dataUrl.length <= 200000) { + callback(``); + } else { + alert('Image file is too large to send over Distant Chat 200KB packet size limit.'); + callback(null); + } + }; + img.onerror = () => callback(null); + img.src = evt.target.result; + }; + reader.readAsDataURL(file); +} const ChatTab = () => { return { @@ -65,16 +114,28 @@ const ChatTab = () => { }), ]), 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('.select-own-profile', { style: 'display: flex; align-items: center; gap: 0.5rem;' }, [ + m('span', { style: 'color: #64748b;' }, 'Chatting as:'), + (() => { + const ownId = State.selectedOwnGxsIdForChat; + if (ownId) fetchIdDetails(ownId); + const ownDetails = State.gxsIdToDetailsMap[ownId]; + return m('.own-profile-badge', { style: 'display: flex; align-items: center; gap: 0.4rem;' }, [ + m(peopleUtil.UserAvatar, { + avatar: ownDetails ? ownDetails.mAvatar : null, + identityId: ownId, + size: 24, + }), + m('select', { + style: 'padding: 0.25rem 0.5rem; border-radius: 0.25rem; border: 1px solid #cbd5e1; outline: none; background: #f8fafc; font-weight: 600;', + value: ownId, + 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;', @@ -135,23 +196,98 @@ const ChatTab = () => { } const isIncoming = msg.incoming; const senderName = isIncoming ? name : rs.userList.username(State.selectedOwnGxsIdForChat); - + const rawText = msg.msg || msg.message || ''; + return m('.chat-bubble-container' + (isIncoming ? '.incoming' : '.outgoing'), [ m('.chat-sender', senderName), - m('.chat-bubble', msg.msg || msg.message), + m('.chat-bubble', renderChatMessage(rawText)), m('.chat-time', new Date(msg.sendTime * 1000).toLocaleTimeString()), ]); }), ]), - m('.chat-input-area', [ + m('.chat-input-area', { style: 'display: flex; align-items: center; gap: 0.5rem; padding: 0.75rem; background: #ffffff; border-top: 1px solid #cbd5e1;' }, [ + m('button.chat-hub-action-btn', { + disabled: !canTalk, + style: !canTalk ? 'opacity: 0.5; cursor: not-allowed;' : '', + title: 'Attach file link', + onclick: () => { + const path = prompt('Enter file path to attach as Retroshare link:'); + if (path && path.trim()) { + const val = State.chatInputMsg || ''; + State.chatInputMsg = val ? val + '\n' + path.trim() : path.trim(); + m.redraw(); + } + } + }, m('i.fas.fa-paperclip')), + + m('.emoji-picker-wrapper', { style: 'position: relative;' }, [ + m('button.chat-hub-action-btn', { + disabled: !canTalk, + style: !canTalk ? 'opacity: 0.5; cursor: not-allowed;' : '', + title: 'Insert emoji', + onclick: (e) => { + e.stopPropagation(); + State.showEmojiPicker = !State.showEmojiPicker; + } + }, m('i.fas.fa-smile')), + State.showEmojiPicker && m(chatEmoji.EmojiPicker, { + onSelect: (emoji) => { + State.chatInputMsg = (State.chatInputMsg || '') + emoji; + State.showEmojiPicker = false; + m.redraw(); + } + }), + ]), + + m('label.chat-hub-action-btn', { + title: 'Send image', + style: `cursor: ${canTalk ? 'pointer' : 'not-allowed'}; opacity: ${canTalk ? 1 : 0.5};`, + }, [ + m('i.fas.fa-image'), + m('input[type=file][accept=image/*]', { + style: 'display: none;', + disabled: !canTalk, + onchange: (e) => { + if (!e.target.files || !e.target.files[0]) return; + const file = e.target.files[0]; + formatChatImage(file, (imgTag) => { + if (imgTag) { + State.chatInputMsg = (State.chatInputMsg || '') + imgTag; + m.redraw(); + } + }); + e.target.value = ''; + } + }) + ]), + m('textarea.chat-textarea', { - placeholder: canTalk ? 'Type your encrypted message here...' : 'Waiting for tunnel to be secured...', + placeholder: canTalk ? 'Type your encrypted message here... (or paste image)' : 'Waiting for tunnel to be secured...', disabled: !canTalk, value: State.chatInputMsg, + style: 'flex: 1; resize: none; border: 1px solid #cbd5e1; border-radius: 6px; padding: 0.5rem; font-family: inherit; font-size: 0.9rem; outline: none; min-height: 40px; max-height: 120px;', oninput: (e) => { State.chatInputMsg = e.target.value; }, + onpaste: (e) => { + if (!canTalk) return; + const items = (e.clipboardData || (e.originalEvent && e.originalEvent.clipboardData))?.items; + if (!items) return; + for (let i = 0; i < items.length; i++) { + if (items[i].type.indexOf('image') !== -1) { + e.preventDefault(); + const blob = items[i].getAsFile(); + formatChatImage(blob, (imgTag) => { + if (imgTag) { + State.chatInputMsg = (State.chatInputMsg || '') + imgTag; + m.redraw(); + } + }); + break; + } + } + }, onkeydown: (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); @@ -159,17 +295,14 @@ const ChatTab = () => { } }, }), - m( - 'button.send-btn.blue', - { - disabled: !canTalk, - style: !canTalk ? 'opacity: 0.5; cursor: not-allowed;' : '', - onclick: () => { - if (canTalk) sendDistantChatMessage(); - }, + + m('button.send-btn.blue', { + disabled: !canTalk, + style: !canTalk ? 'opacity: 0.5; cursor: not-allowed; height: 38px;' : 'height: 38px;', + onclick: () => { + if (canTalk) sendDistantChatMessage(); }, - [m('i.fas.fa-paper-plane'), ' Send'] - ), + }, [m('i.fas.fa-paper-plane'), ' Send']), ]), ]); }, diff --git a/webui-src/app/people/people_state.js b/webui-src/app/people/people_state.js index cab28de..39a1ef4 100644 --- a/webui-src/app/people/people_state.js +++ b/webui-src/app/people/people_state.js @@ -206,34 +206,35 @@ function pollDistantChatStatus() { }, (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); - } + if (detail.info.status === 2) { + const text = 'Tunnel is secured. You can talk!'; + const exists = State.chatMessages.some( + (m) => m.isSystem && (m.msg === text || m.message === 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 || m.message === 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(); @@ -272,7 +273,14 @@ function initializeDistantChat() { if (!State.selectedId || !State.selectedOwnGxsIdForChat) return; State.chatPid = null; - State.chatMessages = []; + State.chatMessages = [ + { + incoming: true, + isSystem: true, + msg: 'Starting distant chat... Please wait for secure tunnel.', + sendTime: Math.floor(Date.now() / 1000), + } + ]; State.chatDisconnected = false; m.redraw(); @@ -351,26 +359,6 @@ function sendDistantChatMessage() { 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); - if (State.selectedId) { - State.chatHistoryMap[State.selectedId] = { - lastMsg: text, - lastTime: Math.floor(Date.now() / 1000), - }; - } - m.redraw(); - setTimeout(() => { - const element = document.querySelector('.chat-messages'); - if (element) element.scrollTop = element.scrollHeight; - }, 100); - rs.rsJsonApiRequest( '/rsChats/sendChat', { @@ -378,8 +366,31 @@ function sendDistantChatMessage() { msg: text, }, (data, success) => { - if (!success) { - console.error('[RS] Failed to send distant chat message'); + if (success) { + 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); + if (State.selectedId) { + State.chatHistoryMap[State.selectedId] = { + lastMsg: text, + lastTime: Math.floor(Date.now() / 1000), + }; + } + m.redraw(); + setTimeout(() => { + const element = document.querySelector('.chat-messages'); + if (element) element.scrollTop = element.scrollHeight; + }, 100); + } else { + console.error('[RS] Failed to send distant chat message:', data); + alert('Failed to send distant chat message. The image/payload exceeds RetroShare max chat packet size.'); + State.chatInputMsg = text; + m.redraw(); } } ); diff --git a/webui-src/app/statusbar.js b/webui-src/app/statusbar.js index 2ebe7b4..a9156bd 100644 --- a/webui-src/app/statusbar.js +++ b/webui-src/app/statusbar.js @@ -1,6 +1,11 @@ const m = require('mithril'); const rs = require('rswebui'); +// RS_HIDDEN_TYPE constants (from config_util.js / retroshare/rspeers.h) +const RS_HIDDEN_TYPE_NONE = 0; +const RS_HIDDEN_TYPE_TOR = 2; +const RS_HIDDEN_TYPE_I2P = 4; + const State = { friendCount: 0, onlineCount: 0, @@ -13,13 +18,160 @@ const State = { forwardPort: false, stunOk: false, extAddressOk: false, + + // Hidden-mode / Tor+I2P state (mirrors TorStatus widget in Qt) + hiddenType: RS_HIDDEN_TYPE_NONE, // 0=none, 2=Tor, 4=I2P + torProxyOk: null, // null=unchecked, true=ok, false=fail + torChecking: false, + + // Bandwidth rate status (mirrors RatesStatus widget in Qt) + rateIn: 0.0, + totalIn: 0, + rateOut: 0.0, + totalOut: 0, }; +function parse64Num(val) { + if (val === null || val === undefined) return 0; + if (typeof val === 'number') return val; + if (typeof val === 'string') return parseFloat(val) || 0; + if (typeof val === 'object') { + if (val.xuint64 !== undefined) return parseFloat(val.xuint64) || 0; + if (val.xint64 !== undefined) return parseFloat(val.xint64) || 0; + if (val.xstr64 !== undefined) return parseFloat(val.xstr64) || 0; + } + return 0; +} + function formatUnit(val) { - if (!val) return '0'; - if (val >= 1000000) return (val / 1000000).toFixed(1) + 'M'; - if (val >= 1000) return (val / 1000).toFixed(1) + 'k'; - return val.toString(); + const num = parse64Num(val); + if (!num) return '0'; + if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; + if (num >= 1000) return (num / 1000).toFixed(1) + 'k'; + return num.toString(); +} + +function formatBytes(rawBytes) { + const bytes = parse64Num(rawBytes); + if (!bytes || bytes <= 0 || isNaN(bytes)) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + const safeI = Math.max(0, Math.min(i, sizes.length - 1)); + return parseFloat((bytes / Math.pow(k, safeI)).toFixed(1)) + ' ' + sizes[safeI]; +} + +/** + * Fetch the own peer's hidden type and proxy status using /rsTor API. + * Mirrors Qt's TorStatus::getTorStatus(). + */ +function updateTorStatus() { + if (!rs.loginKey.isVerified) return; + + rs.rsJsonApiRequest('/rsAccounts/getCurrentAccountId').then((res) => { + if (!res || !res.body || !res.body.retval) return; + const sslId = res.body.id; + + rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { sslId }).then((pres) => { + if (!pres || !pres.body || !pres.body.retval) return; + const details = pres.body.det; + + const isHiddenNode = Boolean( + details && ( + details.hiddenType === RS_HIDDEN_TYPE_TOR || + details.hiddenType === RS_HIDDEN_TYPE_I2P || + details.extAddr === 'Hidden' + ) + ); + + if (!isHiddenNode) { + State.hiddenType = RS_HIDDEN_TYPE_NONE; + State.torProxyOk = null; + State.torChecking = false; + m.redraw(); + return; + } + + const targetType = details.hiddenType || RS_HIDDEN_TYPE_TOR; + State.hiddenType = targetType; + + // Check if node uses automated Tor management via /rsAccounts/isTorAuto (same as Qt) + rs.rsJsonApiRequest('/rsAccounts/isTorAuto', {}).then((autoRes) => { + const isAuto = autoRes && (autoRes.retval || (autoRes.body && autoRes.body.retval)); + if (isAuto) { + Promise.all([ + rs.rsJsonApiRequest('/rsTor/torStatus', {}), + rs.rsJsonApiRequest('/rsTor/torConnectivityStatus', {}), + ]).then(([torRes, connRes]) => { + const torStatus = torRes && torRes.body ? torRes.body.retval : (torRes && torRes.retval !== undefined ? torRes.retval : 0); + const connStatus = connRes && connRes.body ? connRes.body.retval : (connRes && connRes.retval !== undefined ? connRes.retval : 0); + const torControlOk = connStatus === 6; // HIDDEN_SERVICE_READY + const torReady = torStatus === 2; // READY + + if (torReady && torControlOk) { + State.torProxyOk = true; + State.torChecking = false; + } else if (torStatus === 1 || connStatus === 0 || connStatus === 1) { + // OFFLINE, ERROR, or NOT_CONNECTED + State.torProxyOk = false; + State.torChecking = false; + } else if (connStatus >= 2 && connStatus <= 5) { + // CONNECTING, SOCKET_CONNECTED, AUTHENTICATING, AUTHENTICATED + State.torProxyOk = null; + State.torChecking = true; + } else { + // UNKNOWN / default + State.torProxyOk = null; + State.torChecking = false; + } + m.redraw(); + }).catch(() => { + State.torProxyOk = true; + State.torChecking = false; + m.redraw(); + }); + } else { + // Manual Tor / I2P proxy node + State.torProxyOk = true; + State.torChecking = false; + m.redraw(); + } + }).catch(() => { + // Fallback for uncompiled backend: query /rsTor endpoints directly or assume active + Promise.all([ + rs.rsJsonApiRequest('/rsTor/torStatus', {}), + rs.rsJsonApiRequest('/rsTor/torConnectivityStatus', {}), + ]).then(([torRes, connRes]) => { + const torStatus = torRes && torRes.body ? torRes.body.retval : (torRes && torRes.retval !== undefined ? torRes.retval : 0); + const connStatus = connRes && connRes.body ? connRes.body.retval : (connRes && connRes.retval !== undefined ? connRes.retval : 0); + const torControlOk = connStatus === 6; // HIDDEN_SERVICE_READY + const torReady = torStatus === 2; // READY + + if (torReady && torControlOk) { + State.torProxyOk = true; + State.torChecking = false; + } else if (torStatus === 1 || connStatus === 0 || connStatus === 1) { + State.torProxyOk = false; + State.torChecking = false; + } else if (connStatus >= 2 && connStatus <= 5) { + State.torProxyOk = null; + State.torChecking = true; + } else { + State.torProxyOk = null; + State.torChecking = false; + } + m.redraw(); + }).catch(() => { + State.torProxyOk = true; + State.torChecking = false; + m.redraw(); + }); + }); + }); + }).catch(() => { + State.hiddenType = RS_HIDDEN_TYPE_NONE; + State.torProxyOk = null; + }); } function updateStatus() { @@ -37,7 +189,7 @@ function updateStatus() { } }); - // 2. Net / DHT config status + // 2. Net / DHT config status & NAT state rs.rsJsonApiRequest('/rsConfig/getConfigNetStatus', {}, (data) => { if (data && data.status) { State.dhtActive = data.status.DHTActive; @@ -49,13 +201,44 @@ function updateStatus() { State.stunOk = data.status.netStunOk; State.extAddressOk = data.status.netExtAddressOk; - if (State.firewalled && !State.forwardPort) { + // Compute NAT state directly from RsConfigNetStatus + if (!data.status.netLocalOk && !data.status.netExtAddressOk) { + State.natState = 2; // BAD_OFFLINE + } else if (data.status.firewalled && !data.status.forwardPort && !data.status.netUpnpOk) { State.natState = 6; // WARNING_NATTED + } else if (data.status.forwardPort || data.status.netUpnpOk) { + State.natState = 9; // ADV_FORWARD } else { State.natState = 8; // GOOD } } - }).catch(() => {}); + }); + + // 3. NAT netState from /rsConfig/getNetState + rs.rsJsonApiRequest('/rsConfig/getNetState', {}, (data) => { + if (data && data.retval !== undefined) { + State.natState = data.retval; + m.redraw(); + } else if (data && data.body && data.body.retval !== undefined) { + State.natState = data.body.retval; + m.redraw(); + } + }); + + // 4. Tor/I2P hidden-mode status (same as Qt TorStatus widget) + updateTorStatus(); + + // 5. Bandwidth rates (same as Qt RatesStatus widget) + rs.rsJsonApiRequest('/rsConfig/getTotalBandwidthRates', {}, (data) => { + const rates = (data && data.rates) || (data && data.body && data.body.rates); + if (rates) { + State.rateIn = rates.mRateIn !== undefined ? rates.mRateIn : (rates.rateIn || 0.0); + State.totalIn = rates.mTotalIn !== undefined ? rates.mTotalIn : (rates.totalIn || 0); + State.rateOut = rates.mRateOut !== undefined ? rates.mRateOut : (rates.rateOut || 0.0); + State.totalOut = rates.mTotalOut !== undefined ? rates.mTotalOut : (rates.totalOut || 0); + m.redraw(); + } + }); } let intervalId = null; @@ -71,7 +254,10 @@ const StatusBar = { } }, view() { - // DHT Status color & tooltip + const isHiddenMode = State.hiddenType === RS_HIDDEN_TYPE_TOR || + State.hiddenType === RS_HIDDEN_TYPE_I2P; + + // ── DHT Status (hidden when in hidden/darknet mode) ──────────────────── let dhtColor = '#94a3b8'; // grey (off) let dhtTooltip = 'DHT Off'; if (State.dhtActive) { @@ -89,7 +275,7 @@ const StatusBar = { } } - // NAT Status color & tooltip + // ── NAT Status (hidden when in hidden/darknet mode) ──────────────────── let natColor = '#94a3b8'; let natTooltip = 'Offline'; switch (State.natState) { @@ -128,25 +314,90 @@ const StatusBar = { break; } + // ── Tor / I2P status indicator ───────────────────────────────────────── + // Only shown when peer is in RS_NETMODE_HIDDEN with a proxy type set. + // Mirrors Qt TorStatus widget label + icon logic. + let torLabel, torColor, torIcon, torTooltip; + if (isHiddenMode) { + torLabel = State.hiddenType === RS_HIDDEN_TYPE_TOR ? 'Tor:' : 'I2P:'; + if (State.torChecking) { + torColor = '#f59e0b'; + torIcon = 'fas fa-spinner fa-spin'; + torTooltip = 'Checking proxy…'; + } else if (State.torProxyOk === null) { + torColor = '#94a3b8'; + torIcon = 'fas fa-shield-alt'; + torTooltip = State.hiddenType === RS_HIDDEN_TYPE_TOR + ? 'No Tor configuration' + : 'No I2P configuration'; + } else if (State.torProxyOk) { + torColor = '#22c55e'; + torIcon = 'fas fa-shield-alt'; + torTooltip = State.hiddenType === RS_HIDDEN_TYPE_TOR + ? 'Tor proxy is OK' + : 'I2P proxy is OK'; + } else { + torColor = '#ef4444'; + torIcon = 'fas fa-shield-alt'; + torTooltip = State.hiddenType === RS_HIDDEN_TYPE_TOR + ? 'Tor proxy is not available' + : 'I2P proxy is not available'; + } + } + return m('.statusbar', [ m('.statusbar-left', { style: 'display: flex; align-items: center; gap: 0.75rem;' }, [ m('.statusbar-item', [ m('i.fas.fa-users', { style: 'margin-right: 0.5rem; color: #94a3b8;' }), m('span', `Friends: ${State.onlineCount}/${State.friendCount}`), ]), - m('.statusbar-divider'), - m('.statusbar-item', { title: natTooltip, style: 'cursor: help;' }, [ + + // NAT — hidden when in hidden/darknet mode (same as Qt) + !isHiddenMode && m('.statusbar-divider'), + !isHiddenMode && m('.statusbar-item', { title: natTooltip, style: 'cursor: help;' }, [ m('span', { style: 'margin-right: 0.5rem;' }, 'NAT:'), m('.status-bullet', { style: { backgroundColor: natColor } }), ]), - m('.statusbar-divider'), - m('.statusbar-item', { title: dhtTooltip, style: 'cursor: help;' }, [ + + // DHT — hidden when in hidden/darknet mode (same as Qt) + !isHiddenMode && m('.statusbar-divider'), + !isHiddenMode && m('.statusbar-item', { title: dhtTooltip, style: 'cursor: help;' }, [ m('span', { style: 'margin-right: 0.5rem;' }, 'DHT:'), m('.status-bullet', { style: { backgroundColor: dhtColor } }), State.dhtActive && State.dhtOk && m('span', { style: 'margin-left: 0.5rem;' }, `${formatUnit(State.dhtRsNetSize)} (${formatUnit(State.dhtNetSize)})`), ]), + + // Tor / I2P — only shown when in hidden/darknet mode (same as Qt) + isHiddenMode && m('.statusbar-divider'), + isHiddenMode && m('.statusbar-item.statusbar-item--tor', { + title: torTooltip, + style: 'cursor: help;', + }, [ + m('span.tor-label', { style: 'margin-right: 0.4rem; font-weight: 600;' }, torLabel), + m('i.' + torIcon, { style: { color: torColor, fontSize: '1rem', transition: 'color 0.3s' } }), + ]), + ]), + + // RatesStatus — Bandwidth speeds & total cumulative transfer (Down | Up) + m('.statusbar-right', { style: 'display: flex; align-items: center; gap: 0.75rem;' }, [ + m('.statusbar-item', { + title: `Downloaded: ${formatBytes(State.totalIn)}`, + style: 'cursor: help; display: flex; align-items: center;' + }, [ + m('i.fas.fa-arrow-down', { style: 'color: #22c55e; margin-right: 0.35rem;' }), + m('span', `Down: ${State.rateIn.toFixed(2)} kB/s`), + m('span', { style: 'color: #64748b; font-size: 0.8rem; margin-left: 0.35rem;' }, `(${formatBytes(State.totalIn)})`), + ]), + m('.statusbar-divider'), + m('.statusbar-item', { + title: `Uploaded: ${formatBytes(State.totalOut)}`, + style: 'cursor: help; display: flex; align-items: center;' + }, [ + m('i.fas.fa-arrow-up', { style: 'color: #3b82f6; margin-right: 0.35rem;' }), + m('span', `Up: ${State.rateOut.toFixed(2)} kB/s`), + m('span', { style: 'color: #64748b; font-size: 0.8rem; margin-left: 0.35rem;' }, `(${formatBytes(State.totalOut)})`), + ]), ]), - m('.statusbar-right'), ]); }, }; diff --git a/webui-src/styles.css b/webui-src/styles.css index cac78bd..82506e7 100644 --- a/webui-src/styles.css +++ b/webui-src/styles.css @@ -1733,25 +1733,33 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem field-sizing: content !important; } -/* Attach file button and modal popup */ -.chat-hub-attach-btn { - background-color: transparent; - border: none; - font-size: 1.25rem; - color: #64748b; - cursor: pointer; - padding: 0.5rem; - margin-right: 0.25rem; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - transition: color 0.2s, transform 0.2s; +/* Attach file, emoji, image action buttons in chat */ +.chat-hub-attach-btn, +.chat-hub-action-btn { + background-color: transparent !important; + border: none !important; + font-size: 1.15rem !important; + color: #64748b !important; + cursor: pointer !important; + padding: 0.4rem 0.5rem !important; + border-radius: 0.375rem !important; + flex-shrink: 0 !important; + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + transition: all 0.2s !important; + box-shadow: none !important; + margin: 0 !important; + line-height: 1 !important; + height: 36px !important; + width: 36px !important; } -.chat-hub-attach-btn:hover { - color: #3b82f6; - transform: scale(1.05); +.chat-hub-attach-btn:hover, +.chat-hub-action-btn:hover { + background-color: #f1f5f9 !important; + color: #3b82f6 !important; + transform: none !important; } .attach-modal-overlay { From 7d7adb8b831dbc931c751911f786a7ce0ca7b210 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:06:45 +0200 Subject: [PATCH 30/40] Added Chat History browser --- webui-src/app/chat/chat.js | 74 +++++++++----- webui-src/app/chat/chat_state.js | 36 +++++++ webui-src/app/people/people_chat_tab.js | 27 ++++- webui-src/app/people/people_history.js | 126 ++++++++++++++++++++++++ webui-src/app/people/people_state.js | 91 ++++++++++++++++- 5 files changed, 325 insertions(+), 29 deletions(-) create mode 100644 webui-src/app/people/people_history.js 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, From 7dc081cfbcb9ddf85a2e82f4637d3d4be34908e5 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:48:11 +0200 Subject: [PATCH 31/40] Added chat settings page --- webui-src/app/config/config_chat.js | 205 ++++++++++++++++++++++++ webui-src/app/config/config_resolver.js | 1 + 2 files changed, 206 insertions(+) create mode 100644 webui-src/app/config/config_chat.js diff --git a/webui-src/app/config/config_chat.js b/webui-src/app/config/config_chat.js new file mode 100644 index 0000000..5e25773 --- /dev/null +++ b/webui-src/app/config/config_chat.js @@ -0,0 +1,205 @@ +const m = require('mithril'); +const rs = require('rswebui'); +const peopleUtil = require('people/people_util'); +const peopleState = require('people/people_state'); + +const ConfigChat = () => { + let defaultIdentity = ''; + let ownIdentities = []; + let acceptChatFrom = 0; // 0 = Everyone, 1 = Contacts Only, 2 = Nobody + let maxStorageDays = 10; + + // History states + let historyEnable = { private: true, distant: true, lobby: true }; + let historySaveCount = { private: 500, distant: 500, lobby: 500 }; + + function loadSettings() { + // Load Own Identities + peopleUtil.ownIds((ids) => { + ownIdentities = ids || []; + ownIdentities.forEach((id) => { + peopleState.fetchIdDetails(id); + }); + m.redraw(); + }); + + // Load Default Lobby Identity + rs.rsJsonApiRequest('/rsChats/getDefaultIdentityForChatLobby', {}, (data) => { + if (data && data.id) { + defaultIdentity = data.id; + peopleState.fetchIdDetails(defaultIdentity); + m.redraw(); + } + }); + + // Load Distant Chat Accept Permission Flags + rs.rsJsonApiRequest('/rsChats/getDistantChatPermissionFlags', {}, (data) => { + if (data && data.retval !== undefined) { + acceptChatFrom = data.retval; + m.redraw(); + } + }); + + // Load Max Storage Duration (silent error fallback) + rs.rsJsonApiRequest('/rsHistory/getMaxStorageDuration', {}, (data, success) => { + if (success && data && data.retval !== undefined) { + maxStorageDays = Math.round(data.retval / 86400); + m.redraw(); + } + }, true); + + // Load History Enables & Save Counts + const types = [ + { key: 'private', type: 1 }, + { key: 'distant', type: 3 }, + { key: 'lobby', type: 2 }, + ]; + + types.forEach(({ key, type }) => { + rs.rsJsonApiRequest('/rsHistory/getEnable', { chat_type: type }, (data, success) => { + if (success && data && data.retval !== undefined) { + historyEnable[key] = data.retval; + m.redraw(); + } + }, true); + rs.rsJsonApiRequest('/rsHistory/getSaveCount', { chat_type: type }, (data, success) => { + if (success && data && data.retval !== undefined) { + historySaveCount[key] = data.retval; + m.redraw(); + } + }, true); + }); + } + + return { + oninit: () => { + loadSettings(); + }, + view: () => { + const selectedDetails = defaultIdentity ? peopleState.State.gxsIdToDetailsMap[defaultIdentity] : null; + + return m('.node-config', [ + // General Chat Settings + m('.widget', [ + m('.widget__heading', m('h3', 'General Chat Settings')), + m('.widget__body', [ + m('.config-grid', { style: 'display: grid; grid-template-columns: 200px 1fr; gap: 1rem; align-items: center;' }, [ + m('label', { style: 'font-weight: 500; color: #334155;' }, 'Default identity for chat rooms:'), + m('.default-id-selector', { style: 'display: flex; align-items: center; gap: 0.5rem;' }, [ + m(peopleUtil.UserAvatar, { + avatar: selectedDetails ? selectedDetails.mAvatar : null, + identityId: defaultIdentity, + size: 24, + }), + m('select', { + style: 'padding: 0.35rem 0.6rem; border-radius: 0.375rem; border: 1px solid #cbd5e1; outline: none; background: #ffffff; min-width: 240px; font-weight: 600;', + value: defaultIdentity, + onchange: (e) => { + defaultIdentity = e.target.value; + rs.rsJsonApiRequest('/rsChats/setDefaultIdentityForChatLobby', { id: defaultIdentity }, () => {}); + }, + }, [ + m('option', { value: '' }, '-- Select Default Identity --'), + ownIdentities.map((id) => { + const det = peopleState.State.gxsIdToDetailsMap[id]; + const nick = (det ? det.mNickname : null) || rs.userList.username(id) || id; + return m('option', { value: id }, nick); + }), + ]), + ]), + + m('label', { style: 'font-weight: 500; color: #334155;' }, 'Accept chat from:'), + m('select', { + style: 'padding: 0.35rem 0.6rem; border-radius: 0.375rem; border: 1px solid #cbd5e1; outline: none; background: #ffffff; max-width: 320px; font-weight: 600;', + value: acceptChatFrom, + onchange: (e) => { + acceptChatFrom = parseInt(e.target.value); + rs.rsJsonApiRequest('/rsChats/setDistantChatPermissionFlags', { flags: acceptChatFrom }, () => {}); + }, + }, [ + m('option', { value: 0 }, 'Everyone'), + m('option', { value: 1 }, 'Contacts Only'), + m('option', { value: 2 }, 'Nobody'), + ]), + ]), + ]), + ]), + + // Chat History Settings + m('.widget', [ + m('.widget__heading', m('h3', 'Chat History Settings')), + m('.widget__body', [ + m('.config-grid', { style: 'display: flex; align-items: center; justify-content: space-between; background: #f8fafc; padding: 0.75rem 1rem; border-radius: 0.5rem; border: 1px solid #e2e8f0; margin-bottom: 1.25rem; max-width: 500px; width: 100%;' }, [ + m('div', [ + m('span', { style: 'font-weight: 600; color: #1e293b; display: block;' }, 'Max Storage Duration'), + m('span', { style: 'font-size: 0.8rem; color: #64748b;' }, 'Global expiration period for messages stored in history database'), + ]), + m('.storage-input-group', { style: 'display: flex; align-items: center; gap: 0.5rem;' }, [ + m('input[type=number][min=1][max=365]', { + style: 'width: 70px; padding: 0.35rem 0.5rem; border-radius: 0.375rem; border: 1px solid #cbd5e1; outline: none; font-weight: 600; text-align: center;', + value: maxStorageDays, + oninput: (e) => (maxStorageDays = parseInt(e.target.value) || 1), + onchange: () => { + rs.rsJsonApiRequest('/rsHistory/setMaxStorageDuration', { seconds: maxStorageDays * 86400 }, () => {}, true); + }, + }), + m('span', { style: 'font-weight: 500; color: #475569; font-size: 0.85rem;' }, 'Days'), + ]), + ]), + + m('.table-container', { style: 'border: 1px solid #e2e8f0; border-radius: 0.5rem; overflow: hidden; background: #ffffff; max-width: 500px; width: 100%;' }, [ + m('table.history-config-table', { style: 'width: 100%; border-collapse: collapse; text-align: left;' }, [ + m('thead', [ + m('tr', { style: 'background: #f8fafc; border-bottom: 1px solid #e2e8f0;' }, [ + m('th', { style: 'padding: 0.75rem 0.75rem; color: #475569; font-weight: 600; font-size: 0.85rem; width: 220px; text-align: left;' }, 'Chat Type'), + m('th', { style: 'padding: 0.75rem 0.75rem; color: #475569; font-weight: 600; font-size: 0.85rem; width: 120px; text-align: center;' }, 'Enable History'), + m('th', { style: 'padding: 0.75rem 0.75rem; color: #475569; font-weight: 600; font-size: 0.85rem; width: 160px; text-align: center;' }, 'Max Saved Messages'), + ]), + ]), + m('tbody', [ + [ + { label: 'Direct Chat (Private)', icon: 'fa-user-lock', key: 'private', type: 1 }, + { label: 'Distant Chat', icon: 'fa-network-wired', key: 'distant', type: 3 }, + { label: 'Chat Rooms (Lobbies)', icon: 'fa-comments', key: 'lobby', type: 2 }, + ].map(({ label, icon, key, type }) => + m('tr', { style: 'border-bottom: 1px solid #f1f5f9; transition: background 0.15s ease;' }, [ + m('td', { style: 'padding: 0.75rem 0.75rem; font-weight: 600; color: #1e293b; text-align: left;' }, [ + m('i.fas.' + icon, { style: 'margin-right: 0.5rem; color: #64748b; font-size: 0.9rem;' }), + label, + ]), + m('td', { style: 'padding: 0.75rem 0.75rem; text-align: center;' }, [ + m('input[type=checkbox]', { + style: 'width: 17px; height: 17px; cursor: pointer; accent-color: #3b82f6;', + checked: historyEnable[key], + oninput: (e) => { + historyEnable[key] = e.target.checked; + rs.rsJsonApiRequest('/rsHistory/setEnable', { chat_type: type, enable: historyEnable[key] }, () => {}, true); + }, + }), + ]), + m('td', { style: 'padding: 0.75rem 0.75rem; text-align: center;' }, [ + m('div', { style: 'display: flex; align-items: center; justify-content: center; gap: 0.4rem;' }, [ + m('input[type=number][min=0][max=50000]', { + style: 'width: 80px; padding: 0.3rem 0.4rem; border-radius: 0.375rem; border: 1px solid #cbd5e1; outline: none; text-align: center; font-weight: 500;', + value: historySaveCount[key], + oninput: (e) => (historySaveCount[key] = parseInt(e.target.value) || 0), + onchange: () => { + rs.rsJsonApiRequest('/rsHistory/setSaveCount', { chat_type: type, count: historySaveCount[key] }, () => {}, true); + }, + }), + m('span', { style: 'font-size: 0.8rem; color: #94a3b8;' }, 'msgs'), + ]), + ]), + ]) + ), + ]), + ]), + ]), + ]), + ]), + ]); + }, + }; +}; + +module.exports = ConfigChat; diff --git a/webui-src/app/config/config_resolver.js b/webui-src/app/config/config_resolver.js index fe21627..dd9f82e 100644 --- a/webui-src/app/config/config_resolver.js +++ b/webui-src/app/config/config_resolver.js @@ -7,6 +7,7 @@ const sections = { services: require('config/config_services'), files: require('config/config_files'), people: require('config/config_people'), + chat: require('config/config_chat'), mail: require('config/config_mail'), }; From e35dfebe3340b39ddcba0964c9a87930705d75b4 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:54:26 +0200 Subject: [PATCH 32/40] Fisxng error fallback --- webui-src/app/config/config_chat.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/webui-src/app/config/config_chat.js b/webui-src/app/config/config_chat.js index 5e25773..6b6714e 100644 --- a/webui-src/app/config/config_chat.js +++ b/webui-src/app/config/config_chat.js @@ -32,13 +32,13 @@ const ConfigChat = () => { } }); - // Load Distant Chat Accept Permission Flags - rs.rsJsonApiRequest('/rsChats/getDistantChatPermissionFlags', {}, (data) => { - if (data && data.retval !== undefined) { + // Load Distant Chat Accept Permission Flags (silent error fallback) + rs.rsJsonApiRequest('/rsChats/getDistantChatPermissionFlags', {}, (data, success) => { + if (success && data && data.retval !== undefined) { acceptChatFrom = data.retval; m.redraw(); } - }); + }, true); // Load Max Storage Duration (silent error fallback) rs.rsJsonApiRequest('/rsHistory/getMaxStorageDuration', {}, (data, success) => { @@ -114,7 +114,7 @@ const ConfigChat = () => { value: acceptChatFrom, onchange: (e) => { acceptChatFrom = parseInt(e.target.value); - rs.rsJsonApiRequest('/rsChats/setDistantChatPermissionFlags', { flags: acceptChatFrom }, () => {}); + rs.rsJsonApiRequest('/rsChats/setDistantChatPermissionFlags', { flags: acceptChatFrom }, () => {}, true); }, }, [ m('option', { value: 0 }, 'Everyone'), From 6202ff7077cd57b12d81bb6aeee56f87f94a01b4 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sat, 25 Jul 2026 22:37:01 +0200 Subject: [PATCH 33/40] Addded quote chat message feature --- webui-src/app/chat/chat.js | 48 +++++++++++++- webui-src/app/chat/chat_state.js | 88 ++++++++++++++++++++++++-- webui-src/app/people/people_history.js | 3 +- 3 files changed, 131 insertions(+), 8 deletions(-) diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index 558ca14..0ce91ff 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -1058,10 +1058,16 @@ const ChatRoomJoinView = () => { const Layout = { dismissMenu: () => { + let redraw = false; if (ChatHubState.activeMenu) { ChatHubState.activeMenu = null; - m.redraw(); + redraw = true; } + if (ChatHubState.messageContextMenu && ChatHubState.messageContextMenu.show) { + ChatHubState.messageContextMenu.show = false; + redraw = true; + } + if (redraw) m.redraw(); }, oninit: () => { ChatHubState.activeTab = 'chat'; @@ -1481,7 +1487,45 @@ const Layout = { 'Select a chat room from the left panel to view details or join a conversation.' ), ]), - ]), + ]), + ChatHubState.messageContextMenu.show && m('.chat-msg-context-menu', { + style: `position: fixed; top: ${ChatHubState.messageContextMenu.y}px; left: ${ChatHubState.messageContextMenu.x}px; background: #ffffff; border: 1px solid #cbd5e1; border-radius: 0.5rem; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05); padding: 0.35rem 0; z-index: 3000; min-width: 160px;`, + onclick: (e) => e.stopPropagation(), + }, [ + m('.context-menu-item', { + style: 'padding: 0.5rem 1rem; font-size: 0.85rem; font-weight: 600; color: #1e293b; display: flex; align-items: center; gap: 0.6rem; cursor: pointer; transition: background 0.15s ease;', + onmouseenter: (e) => (e.currentTarget.style.background = '#f1f5f9'), + onmouseleave: (e) => (e.currentTarget.style.background = 'transparent'), + onclick: () => { + const { username, messageText } = ChatHubState.messageContextMenu; + const quoteHeader = `> [${username}]: ${messageText}\n`; + const textarea = document.querySelector('.chat-hub-input-area textarea') || document.querySelector('#msginput'); + if (textarea) { + textarea.value = (textarea.value ? textarea.value.trim() + '\n' : '') + quoteHeader; + textarea.focus(); + } + ChatHubState.messageContextMenu.show = false; + m.redraw(); + }, + }, [ + m('i.fas.fa-quote-right', { style: 'color: #3b82f6;' }), + 'Quote Message' + ]), + m('.context-menu-item', { + style: 'padding: 0.5rem 1rem; font-size: 0.85rem; font-weight: 600; color: #1e293b; display: flex; align-items: center; gap: 0.6rem; cursor: pointer; transition: background 0.15s ease;', + onmouseenter: (e) => (e.currentTarget.style.background = '#f1f5f9'), + onmouseleave: (e) => (e.currentTarget.style.background = 'transparent'), + onclick: () => { + const { messageText } = ChatHubState.messageContextMenu; + navigator.clipboard.writeText(messageText); + ChatHubState.messageContextMenu.show = false; + m.redraw(); + }, + }, [ + m('i.far.fa-copy', { style: 'color: #64748b;' }), + 'Copy Text' + ]), + ]) ]); }, }; diff --git a/webui-src/app/chat/chat_state.js b/webui-src/app/chat/chat_state.js index 996a156..1e8d226 100644 --- a/webui-src/app/chat/chat_state.js +++ b/webui-src/app/chat/chat_state.js @@ -149,7 +149,7 @@ function renderChatMessage(rawText) { .replaceAll('
', '\n') .replace(new RegExp('|<[^>]*>', 'gm'), ''); if (cleanText) { - parts.push(renderTextWithEmoji(cleanText)); + parts.push(renderFormattedMessageText(cleanText)); } } @@ -182,11 +182,61 @@ function renderChatMessage(rawText) { // 3. Normal text message const cleanText = rawText + .replace(/]*>/gi, '\n> ') + .replace(/<\/blockquote>/gi, '\n') .replaceAll('
', '\n') .replaceAll('
', '\n') .replace(new RegExp('|<[^>]*>', 'gm'), ''); - return renderTextWithEmoji(cleanText); + return renderFormattedMessageText(cleanText); +} + +function renderFormattedMessageText(text) { + if (!text) return ''; + const lines = text.split('\n'); + const elements = []; + let currentQuoteLines = []; + + const flushQuote = () => { + if (currentQuoteLines.length > 0) { + const quoteText = currentQuoteLines.join('\n'); + elements.push( + m('blockquote.chat-quote-block', { + style: { + borderLeft: '3px solid #3b82f6', + backgroundColor: '#f8fafc', + color: '#475569', + padding: '0.35rem 0.65rem', + margin: '0.35rem 0', + borderRadius: '0 0.375rem 0.375rem 0', + fontSize: '0.9em', + fontStyle: 'italic', + whiteSpace: 'pre-wrap', + wordBreak: 'break-word', + } + }, renderTextWithEmoji(quoteText)) + ); + currentQuoteLines = []; + } + }; + + lines.forEach((line, idx) => { + if (line.trim().startsWith('>')) { + const lineContent = line.trim().replace(/^>\s?/, ''); + currentQuoteLines.push(lineContent); + } else { + flushQuote(); + if (line) { + elements.push(renderTextWithEmoji(line)); + } + if (idx < lines.length - 1) { + elements.push(m('br')); + } + } + }); + flushQuote(); + + return elements.length > 0 ? elements : renderTextWithEmoji(text); } /** @@ -381,13 +431,34 @@ const Message = () => { const chatType = ChatLobbyModel.currentLobby && ChatLobbyModel.currentLobby.chatType; const isRoom = chatType === 3; + const handleContextMenu = (e) => { + e.preventDefault(); + e.stopPropagation(); + const sel = window.getSelection() ? window.getSelection().toString() : ''; + const targetText = sel && sel.trim() ? sel : rawText; + + ChatHubState.messageContextMenu = { + show: true, + x: e.clientX, + y: e.clientY, + messageText: targetText, + username: username, + }; + m.redraw(); + }; + if (isRoom) { const nickColor = getNicknameColor(gxsId, username); return m( '.message.compact', - m('span.datetime', datetime), - m('span.username', { style: { color: nickColor } }, username + ':'), - m('span.messagetext', renderChatMessage(rawText)) + { + oncontextmenu: handleContextMenu, + }, + [ + m('span.datetime', datetime), + m('span.username', { style: { color: nickColor } }, username + ':'), + m('span.messagetext', renderChatMessage(rawText)), + ] ); } @@ -808,6 +879,13 @@ const ChatHubState = { historySearchQuery: '', fullHistoryMessages: [], isHistoryLoading: false, + messageContextMenu: { + show: false, + x: 0, + y: 0, + messageText: '', + username: '', + }, }; module.exports = { diff --git a/webui-src/app/people/people_history.js b/webui-src/app/people/people_history.js index ce43094..a5ba3b0 100644 --- a/webui-src/app/people/people_history.js +++ b/webui-src/app/people/people_history.js @@ -1,11 +1,11 @@ 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 chatState = require('chat/chat_state'); const isRoom = vnode.attrs && vnode.attrs.isRoom; if (isRoom) { chatState.ChatHubState.historySearchQuery = ''; @@ -19,6 +19,7 @@ const HistoryBrowserModal = () => { } }, view: (vnode) => { + const chatState = require('chat/chat_state'); const isRoom = vnode.attrs && vnode.attrs.isRoom; const stateObj = isRoom ? chatState.ChatHubState : peopleState.State; From 97c05df59a781bd8fd679a25ae2f47ffa29b0a82 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sat, 25 Jul 2026 22:52:54 +0200 Subject: [PATCH 34/40] Added quote message for chat rooms --- webui-src/app/chat/chat.js | 21 ++++++++++++++++++++- webui-src/app/chat/chat_state.js | 2 ++ webui-src/app/scss/pages/_chat.scss | 10 ++++++---- webui-src/styles.css | 10 ++++++---- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index 0ce91ff..d25ed43 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -1511,6 +1511,25 @@ const Layout = { m('i.fas.fa-quote-right', { style: 'color: #3b82f6;' }), 'Quote Message' ]), + ChatHubState.messageContextMenu.gxsId && + ChatHubState.messageContextMenu.gxsId !== '00000000000000000000000000000000' && + m('.context-menu-item', { + style: 'padding: 0.5rem 1rem; font-size: 0.85rem; font-weight: 600; color: #1e293b; display: flex; align-items: center; gap: 0.6rem; cursor: pointer; transition: background 0.15s ease;', + onmouseenter: (e) => (e.currentTarget.style.background = '#f1f5f9'), + onmouseleave: (e) => (e.currentTarget.style.background = 'transparent'), + onclick: () => { + const { gxsId } = ChatHubState.messageContextMenu; + const peopleState = require('people/people_state'); + peopleState.State.selectedId = gxsId; + peopleState.State.activeFilter = 'all'; + peopleState.fetchIdDetails(gxsId); + ChatHubState.messageContextMenu.show = false; + m.route.set('/people/All'); + }, + }, [ + m('i.fas.fa-user-circle', { style: 'color: #0ea5e9;' }), + 'Show Author in People' + ]), m('.context-menu-item', { style: 'padding: 0.5rem 1rem; font-size: 0.85rem; font-weight: 600; color: #1e293b; display: flex; align-items: center; gap: 0.6rem; cursor: pointer; transition: background 0.15s ease;', onmouseenter: (e) => (e.currentTarget.style.background = '#f1f5f9'), @@ -1522,7 +1541,7 @@ const Layout = { m.redraw(); }, }, [ - m('i.far.fa-copy', { style: 'color: #64748b;' }), + m('i.fas.fa-copy', { style: 'color: #64748b;' }), 'Copy Text' ]), ]) diff --git a/webui-src/app/chat/chat_state.js b/webui-src/app/chat/chat_state.js index 1e8d226..4bf1123 100644 --- a/webui-src/app/chat/chat_state.js +++ b/webui-src/app/chat/chat_state.js @@ -443,6 +443,7 @@ const Message = () => { y: e.clientY, messageText: targetText, username: username, + gxsId: gxsId, }; m.redraw(); }; @@ -885,6 +886,7 @@ const ChatHubState = { y: 0, messageText: '', username: '', + gxsId: '', }, }; diff --git a/webui-src/app/scss/pages/_chat.scss b/webui-src/app/scss/pages/_chat.scss index c39e8d0..3714e0e 100644 --- a/webui-src/app/scss/pages/_chat.scss +++ b/webui-src/app/scss/pages/_chat.scss @@ -1027,25 +1027,27 @@ textarea.chatMsg { } .chat-hub-input-area { - padding: 1rem 1.5rem; + padding: 0.75rem 1.5rem; background-color: #ffffff; border-top: 1px solid #cbd5e1; display: flex; gap: 0.75rem; - align-items: center; + align-items: flex-end; flex-shrink: 0; } .chat-hub-input-area textarea.chat-hub-textarea { flex: 1; - resize: none; + resize: vertical; + min-height: 40px; + max-height: 250px; height: 40px; padding: 0.5rem 0.75rem; border: 1px solid #cbd5e1; border-radius: 0.375rem; font-size: 0.9rem; outline: none; - transition: all 0.2s; + transition: border-color 0.2s, box-shadow 0.2s; background-color: #f8fafc; } diff --git a/webui-src/styles.css b/webui-src/styles.css index 82506e7..e3858ff 100644 --- a/webui-src/styles.css +++ b/webui-src/styles.css @@ -1603,25 +1603,27 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem } .chat-hub-input-area { - padding: 1rem 1.5rem; + padding: 0.75rem 1.5rem; background-color: #ffffff; border-top: 1px solid #cbd5e1; display: flex; gap: 0.75rem; - align-items: center; + align-items: flex-end; flex-shrink: 0; } .chat-hub-input-area textarea.chat-hub-textarea { flex: 1; - resize: none; + resize: vertical; + min-height: 40px; + max-height: 250px; height: 40px; padding: 0.5rem 0.75rem; border: 1px solid #cbd5e1; border-radius: 0.375rem; font-size: 0.9rem; outline: none; - transition: all 0.2s; + transition: border-color 0.2s, box-shadow 0.2s; background-color: #f8fafc; } From df977c089eb0fc9148a0e7c56d5ed4c2750c5ad2 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sat, 25 Jul 2026 23:40:21 +0200 Subject: [PATCH 35/40] Added attach file, image & emoji support to mail composer --- webui-src/app/mail/mail_compose.js | 310 ++++++++++++++++++++++++++--- 1 file changed, 286 insertions(+), 24 deletions(-) diff --git a/webui-src/app/mail/mail_compose.js b/webui-src/app/mail/mail_compose.js index 87fdcee..230d376 100644 --- a/webui-src/app/mail/mail_compose.js +++ b/webui-src/app/mail/mail_compose.js @@ -2,14 +2,28 @@ const m = require('mithril'); const rs = require('rswebui'); const widget = require('widgets'); const peopleUtil = require('people/people_util'); +const chatEmoji = require('chat/chat_emoji'); const UserAvatarsCache = {}; const MAX_RECIPIENTS = 20; +function formatFileSize(bytes) { + if (!bytes) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; +} + const Layout = () => { let showCc = false; let showBcc = false; let ownAvatars = {}; + let attachments = []; + let showEmojiPicker = false; + let emojiSearch = ''; + let emojiCategory = 'Smileys'; + const Data = { allUsers: [], ownId: [], @@ -34,6 +48,51 @@ const Layout = () => { }, }, }; + + function insertContentIntoMailBody(content) { + const mailBody = document.querySelector('#composerMailBody'); + if (!mailBody) return; + mailBody.focus(); + const sel = window.getSelection(); + if (sel && sel.rangeCount > 0) { + const range = sel.getRangeAt(0); + if (mailBody.contains(range.commonAncestorContainer)) { + range.deleteContents(); + if (typeof content === 'string') { + const temp = document.createElement('div'); + temp.innerHTML = content; + const frag = document.createDocumentFragment(); + let node, lastNode; + while ((node = temp.firstChild)) { + lastNode = frag.appendChild(node); + } + range.insertNode(frag); + if (lastNode) { + range.setStartAfter(lastNode); + range.collapse(true); + sel.removeAllRanges(); + sel.addRange(range); + } + } else if (content instanceof Node) { + range.insertNode(content); + range.setStartAfter(content); + range.collapse(true); + sel.removeAllRanges(); + sel.addRange(range); + } + return; + } + } + if (typeof content === 'string') { + mailBody.innerHTML += content; + } else if (content instanceof Node) { + mailBody.appendChild(content); + } + } + + function insertEmoji(emoji) { + insertContentIntoMailBody(document.createTextNode(emoji)); + } async function loadMailUserDetails(msgType, senderId, recipientList, isDirectMail, ccList) { Data.allUsers = await peopleUtil.sortUsers(rs.userList.users); @@ -286,31 +345,89 @@ const Layout = () => { ); } function sendMail() { + // Auto-add inputVal if user typed recipient but didn't click dropdown item + ['to', 'cc', 'bcc'].forEach((type) => { + const val = Data.recipients[type].inputVal ? Data.recipients[type].inputVal.trim() : ''; + if (val) { + const match = Data.allUsers.find((u) => u.mGroupName && (u.mGroupName.toLowerCase() === val.toLowerCase() || u.mGroupId === val)); + if (match && !Data.recipients[type].sendList.some((item) => item.mGroupId === match.mGroupId)) { + Data.recipients[type].sendList.push(match); + } else if (!match && val.length > 5) { + Data.recipients[type].sendList.push({ mGroupId: val, mGroupName: val }); + } + Data.recipients[type].inputVal = ''; + } + }); + const to = Data.recipients.to.sendList.map((toItem) => toItem.mGroupId); const cc = Data.recipients.cc.sendList.map((ccItem) => ccItem.mGroupId); const bcc = Data.recipients.bcc.sendList.map((bccItem) => bccItem.mGroupId); - const { identity: from, subject } = Data; + + let from = Data.identity; + if (!from && Data.ownId && Data.ownId.length > 0) { + from = Data.ownId[0]; + Data.identity = from; + } + + if (to.length === 0) { + widget.popupMessage( + m('.widget', [ + m('.widget__heading', m('h3', 'Missing Recipient')), + m('.widget__body', m('p', 'Please select at least one recipient in the "To" field.')), + ]) + ); + return; + } + + if (!from) { + widget.popupMessage( + m('.widget', [ + m('.widget__heading', m('h3', 'Missing Identity')), + m('.widget__body', m('p', 'Please select a "From" identity.')), + ]) + ); + return; + } + + const subject = Data.subject || '(No Subject)'; const mailBodyElement = document.querySelector('#composerMailBody'); - const mailBody = `
${mailBodyElement.innerHTML}
`; - rs.rsJsonApiRequest('/rsMail/sendMail', { from, subject, mailBody, to, cc, bcc }).then( - (res) => { - if (res.body.retval) { - Object.keys(Data.recipients).forEach((recipientType) => { - Data.recipients[recipientType].sendList = []; - }); - Data.subject = ''; - mailBodyElement.innerHTML = ''; - v.attrs.setShowCompose(false); - } - const success = res.body.retval === 1; - widget.popupMessage( - m('.widget', [ - m('.widget__heading', m('h3', success ? 'Success' : 'Error')), - m('.widget__body', m('p', success ? 'Mail sent successfully' : res.body.errorMsg)), - ]) - ); + let fullMailBody = mailBodyElement ? mailBodyElement.innerHTML : ''; + + if (attachments.length > 0) { + const attHtml = ` +

Attachments (${attachments.length}):
+
    + ${attachments.map(att => `
  • 📎 ${att.name} (${att.size})
  • `).join('')} +
+ `; + fullMailBody += attHtml; + } + + const mailBody = `
${fullMailBody}
`; + + rs.rsJsonApiRequest('/rsMail/sendMail', { from, subject, mailBody, to, cc, bcc }, (data, success) => { + const isOk = success && data && ( + data.retval > 0 || + data.retval === true || + (Array.isArray(data.trackingIds) && data.trackingIds.length > 0) + ); + if (isOk) { + Object.keys(Data.recipients).forEach((recipientType) => { + Data.recipients[recipientType].sendList = []; + }); + Data.subject = ''; + if (mailBodyElement) mailBodyElement.innerHTML = ''; + attachments = []; + v.attrs.setShowCompose(false); } - ); + widget.popupMessage( + m('.widget', [ + m('.widget__heading', m('h3', isOk ? 'Success' : 'Error')), + m('.widget__body', m('p', isOk ? 'Mail sent successfully' : (data?.errorMsg || data?.errorMessage || 'Failed to send mail'))), + ]) + ); + m.redraw(); + }); } return m('.widget', [ m('.widget__heading', m('h3', 'Compose a mail')), @@ -453,6 +570,67 @@ const Layout = () => { value: Data.subject, oninput: (e) => (Data.subject = e.target.value), }), + + // Hidden File Inputs + m('input#mail-file-attach[type=file]', { + style: 'display: none;', + multiple: true, + onchange: (e) => { + const files = Array.from(e.target.files || []); + files.forEach((file) => { + attachments.push({ + name: file.name, + size: formatFileSize(file.size), + type: file.type, + rawFile: file, + }); + }); + e.target.value = ''; + m.redraw(); + }, + }), + m('input#mail-image-attach[type=file]', { + style: 'display: none;', + accept: 'image/*', + onchange: (e) => { + const file = e.target.files && e.target.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (event) => { + const src = event.target.result; + insertContentIntoMailBody(``); + }; + reader.readAsDataURL(file); + } + e.target.value = ''; + m.redraw(); + }, + }), + + // File Attachments Bar + attachments.length > 0 && + m('.mail-attachments-bar', { + style: 'margin: 0.5rem 0; padding: 0.5rem 0.75rem; background: #f8fafc; border: 1px solid #cbd5e1; border-radius: 0.375rem; display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center;' + }, [ + m('span', { style: 'font-weight: 600; font-size: 0.85rem; color: #475569; display: flex; align-items: center; gap: 0.35rem; margin-right: 0.25rem;' }, [ + m('i.fas.fa-paperclip', { style: 'color: #019DFF;' }), + `Attachments (${attachments.length}):` + ]), + attachments.map((att, index) => + m('.mail-attachment-chip', { + style: 'display: inline-flex; align-items: center; gap: 0.4rem; padding: 0.25rem 0.65rem; background: #ffffff; border: 1px solid #cbd5e1; border-radius: 1rem; font-size: 0.825rem; font-weight: 500; color: #1e293b; box-shadow: 0 1px 2px rgba(0,0,0,0.05);' + }, [ + m('i.fas.fa-file-alt', { style: 'color: #3b82f6;' }), + m('span', att.name), + m('span', { style: 'color: #94a3b8; font-size: 0.75rem;' }, `(${att.size})`), + m('i.fas.fa-times', { + style: 'cursor: pointer; color: #ef4444; margin-left: 0.2rem; font-size: 0.8rem;', + onclick: () => attachments.splice(index, 1), + }) + ]) + ) + ]), + m('.compose-mail__message', [ m('.compose-mail__message-body[placeholder=Message][contenteditable]#composerMailBody', { oncreate: (vnode) => { @@ -461,10 +639,94 @@ const Layout = () => { } } }), - ]), - m('button.compose-mail__send-btn', { onclick: sendMail }, [ - m('span', 'Send Mail'), - m('i.fas.fa-paper-plane'), + + // Modern Mail Composer Bottom Toolbar + m('.mail-compose-toolbar', { + style: 'display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0.75rem; background: #ffffff; border: 1px solid #cbd5e1; border-top: 1px solid #e2e8f0; border-radius: 0 0 0.375rem 0.375rem; position: relative;' + }, [ + m('.toolbar-left', { style: 'display: flex; align-items: center; gap: 0.5rem;' }, [ + m('button.mail-compose-send-btn', { + style: 'display: flex; align-items: center; gap: 0.5rem; padding: 0.45rem 1.25rem; background: #019DFF; color: #ffffff; border: none; border-radius: 1.5rem; font-weight: 600; font-size: 0.9rem; cursor: pointer; transition: background 0.15s ease; box-shadow: 0 2px 4px rgba(1,157,255,0.25);', + onclick: sendMail, + }, [ + m('span', 'Send'), + m('i.fas.fa-paper-plane', { style: 'font-size: 0.85rem;' }), + ]), + m('.toolbar-divider', { style: 'width: 1px; height: 22px; background: #cbd5e1; margin: 0 0.25rem;' }), + m('button.mail-tool-btn', { + type: 'button', + title: 'Attach files', + style: 'width: 34px; height: 34px; border-radius: 50%; border: none; background: transparent; color: #475569; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background 0.15s ease;', + onmouseenter: (e) => (e.currentTarget.style.background = '#f1f5f9'), + onmouseleave: (e) => (e.currentTarget.style.background = 'transparent'), + onclick: () => { + const input = document.getElementById('mail-file-attach'); + if (input) input.click(); + }, + }, m('i.fas.fa-paperclip', { style: 'font-size: 1.05rem;' })), + m('button.mail-tool-btn', { + type: 'button', + title: 'Insert image', + style: 'width: 34px; height: 34px; border-radius: 50%; border: none; background: transparent; color: #475569; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background 0.15s ease;', + onmouseenter: (e) => (e.currentTarget.style.background = '#f1f5f9'), + onmouseleave: (e) => (e.currentTarget.style.background = 'transparent'), + onclick: () => { + const input = document.getElementById('mail-image-attach'); + if (input) input.click(); + }, + }, m('i.fas.fa-image', { style: 'font-size: 1.05rem;' })), + m('button.mail-tool-btn', { + type: 'button', + title: 'Insert emoji', + style: `width: 34px; height: 34px; border-radius: 50%; border: none; background: ${showEmojiPicker ? '#e0f2fe' : 'transparent'}; color: ${showEmojiPicker ? '#0284c7' : '#475569'}; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background 0.15s ease;`, + onclick: () => (showEmojiPicker = !showEmojiPicker), + }, m('i.fas.fa-smile', { style: 'font-size: 1.05rem;' })), + ]), + + // Floating Emoji Picker Popover + showEmojiPicker && m('.mail-emoji-picker-popover', { + style: 'position: absolute; bottom: 50px; left: 130px; background: #ffffff; border: 1px solid #cbd5e1; border-radius: 0.5rem; box-shadow: 0 10px 25px -5px rgba(0,0,0,0.15), 0 8px 10px -6px rgba(0,0,0,0.1); width: 320px; max-height: 340px; z-index: 2000; display: flex; flex-direction: column; overflow: hidden;', + onclick: (e) => e.stopPropagation(), + }, [ + m('.emoji-search-bar', { style: 'padding: 0.5rem; border-bottom: 1px solid #f1f5f9; display: flex; align-items: center; gap: 0.5rem;' }, [ + m('i.fas.fa-search', { style: 'color: #94a3b8; font-size: 0.85rem;' }), + m('input[type=text][placeholder=Search emoji...]', { + style: 'border: none; outline: none; width: 100%; font-size: 0.85rem;', + value: emojiSearch, + oninput: (e) => (emojiSearch = e.target.value), + }), + emojiSearch && m('i.fas.fa-times', { + style: 'cursor: pointer; color: #94a3b8; font-size: 0.85rem;', + onclick: () => (emojiSearch = ''), + }), + ]), + !emojiSearch && m('.emoji-cat-bar', { style: 'display: flex; background: #f8fafc; border-bottom: 1px solid #e2e8f0; padding: 0.25rem; overflow-x: auto;' }, + chatEmoji.EMOJI_CATEGORIES.map(c => + m('button', { + style: `border: none; background: ${c === emojiCategory ? '#ffffff' : 'transparent'}; border-radius: 0.25rem; padding: 0.3rem 0.4rem; cursor: pointer; font-size: 1rem; box-shadow: ${c === emojiCategory ? '0 1px 2px rgba(0,0,0,0.1)' : 'none'};`, + title: c, + onclick: () => (emojiCategory = c), + }, chatEmoji.EMOJI_ICONS[c]) + ) + ), + m('.emoji-grid-body', { style: 'padding: 0.5rem; display: grid; grid-template-columns: repeat(7, 1fr); gap: 0.25rem; max-height: 230px; overflow-y: auto;' }, + (emojiSearch + ? Object.values(chatEmoji.EMOJI_DATA).flat().filter(e => e.includes(emojiSearch)) + : (chatEmoji.EMOJI_DATA[emojiCategory] || []) + ).map(e => + m('button', { + style: 'border: none; background: transparent; font-size: 1.25rem; cursor: pointer; padding: 0.25rem; border-radius: 0.25rem; transition: background 0.15s ease;', + onmouseenter: (ev) => (ev.currentTarget.style.background = '#f1f5f9'), + onmouseleave: (ev) => (ev.currentTarget.style.background = 'transparent'), + onclick: () => { + insertEmoji(e); + showEmojiPicker = false; + }, + }, e) + ) + ), + ]) + ]), ]), ]), ]); From 98ad752497189089ec326cb2fc3654a268b35350 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sun, 26 Jul 2026 11:08:21 +0200 Subject: [PATCH 36/40] Added missed changes --- webui-src/app/scss/components/_statusbar.scss | 42 ++++++++++++++ webui-src/app/scss/pages/_config.scss | 55 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 webui-src/app/scss/components/_statusbar.scss diff --git a/webui-src/app/scss/components/_statusbar.scss b/webui-src/app/scss/components/_statusbar.scss new file mode 100644 index 0000000..0f88078 --- /dev/null +++ b/webui-src/app/scss/components/_statusbar.scss @@ -0,0 +1,42 @@ +@use '../abstracts' as *; + +/* Status Bar Styles */ +.statusbar { + @include flex($justify: space-between, $align: center); + height: 28px; + background-color: $dark-color; + border-top: 1px solid #2e2e38; + padding: 0 1rem; + font-size: 0.8rem; + color: #94a3b8; + z-index: 100; + box-sizing: border-box; + user-select: none; + flex-shrink: 0; + + &-left { + @include flex($align: center); + } + + &-right { + @include flex($align: center, $gap: 1.5rem); + } + + &-item { + @include flex($align: center); + } + + &-divider { + width: 1px; + height: 14px; + background-color: #2e2e38; + } +} + +.status-bullet { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.5); +} diff --git a/webui-src/app/scss/pages/_config.scss b/webui-src/app/scss/pages/_config.scss index 5bdcf39..659cc95 100644 --- a/webui-src/app/scss/pages/_config.scss +++ b/webui-src/app/scss/pages/_config.scss @@ -89,3 +89,58 @@ .config-files { @include flex(column, $gap: 1rem); } + +/* Hidden Service Configuration layout overrides */ +.proxy-server-container { + width: 100%; + @include flex(column, $gap: 1rem); +} + +.proxy-description { + color: #334155; + font-size: 0.95rem; + margin-bottom: 0.5rem; +} + +.proxy-rows-container { + @include flex(column, $gap: 0.75rem); + width: 100%; +} + +.proxy-row { + display: grid; + grid-template-columns: 160px 220px 220px auto; + gap: 0.75rem; + align-items: center; + width: 100%; +} + +.proxy-label { + font-size: 0.95rem; + font-weight: 500; + color: #1e293b; +} + +.proxy-addr-input, +.proxy-port-input { + width: 100% !important; + max-width: none !important; +} + +.proxy-status-container { + @include flex($align: center, $gap: 0.5rem); +} + +.proxy-status-bullet { + width: 14px; + height: 14px; + border-radius: 50%; + display: inline-block; + border: 1px solid #475569; +} + +.proxy-status-text { + font-size: 0.95rem; + color: #1e293b; +} + From ec21174f22cf78bb55449f01f340d669d7746539 Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sun, 26 Jul 2026 11:10:13 +0200 Subject: [PATCH 37/40] Missed this --- webui-src/app/scss/components/_index.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/webui-src/app/scss/components/_index.scss b/webui-src/app/scss/components/_index.scss index a38e1fd..ee8ea28 100644 --- a/webui-src/app/scss/components/_index.scss +++ b/webui-src/app/scss/components/_index.scss @@ -3,3 +3,5 @@ @forward 'navbar'; @forward 'posts'; @forward 'progress-bar'; +@forward 'statusbar'; + From 70f5fd99757f8673dd642d4ebdb92d88481de6bb Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sun, 26 Jul 2026 12:03:23 +0200 Subject: [PATCH 38/40] Fixed to not lose distant chat when you navigate to other pages Added counter to show active distant chats --- webui-src/app/people/people.js | 43 +++++++- webui-src/app/people/people_chat_tab.js | 6 +- webui-src/app/people/people_sidebar.js | 23 +++- webui-src/app/people/people_state.js | 60 +++++++++-- webui-src/app/scss/pages/_people.scss | 137 ++++++++++++++++++++++++ webui-src/styles.css | 22 ++++ 6 files changed, 273 insertions(+), 18 deletions(-) diff --git a/webui-src/app/people/people.js b/webui-src/app/people/people.js index f859ac9..249ecf4 100644 --- a/webui-src/app/people/people.js +++ b/webui-src/app/people/people.js @@ -9,9 +9,11 @@ const { loadOwnGxsIds, preloadAllChatHistory, syncFilter, + startStatusPolling, stopStatusPolling, initializeDistantChat, } = require('people/people_state'); + const PeopleSidebar = require('people/people_sidebar'); const DetailsTab = require('people/people_details_tab'); const ChatTab = require('people/people_chat_tab'); @@ -36,9 +38,41 @@ const PeopleLayout = () => { // Register for chatEvents to receive live incoming messages rs.events[15].notify = (chatMessage) => { const msgCid = chatMessage.chat_id; - if (msgCid && msgCid.type === 2 && State.chatPid) { + if (msgCid && msgCid.type === 2) { const msgPid = rs.idToHex(msgCid.distant_chat_id); - if (msgPid === State.chatPid) { + + // Find active session matching this distant chat PID + let session = null; + let targetGxsId = null; + Object.keys(State.activeDistantChats || {}).forEach((id) => { + if (State.activeDistantChats[id] && State.activeDistantChats[id].pid === msgPid) { + session = State.activeDistantChats[id]; + targetGxsId = id; + } + }); + + if (session) { + const isNearDuplicate = session.messages.some( + (m) => (m.msg || m.message) === chatMessage.msg && Math.abs(m.sendTime - chatMessage.sendTime) < 5 + ); + if (!isNearDuplicate) { + session.messages.push(chatMessage); + session.messages.sort((a, b) => a.sendTime - b.sendTime); + if (targetGxsId) { + State.chatHistoryMap[targetGxsId] = { + lastMsg: chatMessage.msg || chatMessage.message || '', + lastTime: chatMessage.sendTime || Math.floor(Date.now() / 1000), + }; + } + m.redraw(); + if (State.selectedId === targetGxsId) { + setTimeout(() => { + const element = document.querySelector('.chat-messages'); + if (element) element.scrollTop = element.scrollHeight; + }, 100); + } + } + } else if (State.chatPid && msgPid === State.chatPid) { const isNearDuplicate = State.chatMessages.some( (m) => (m.msg || m.message) === chatMessage.msg && Math.abs(m.sendTime - chatMessage.sendTime) < 5 ); @@ -54,6 +88,10 @@ const PeopleLayout = () => { } } }; + + if (State.chatPid && !State.chatDisconnected) { + startStatusPolling(); + } }, onremove: () => { if (rs.events[15]) { @@ -62,6 +100,7 @@ const PeopleLayout = () => { stopStatusPolling(); window.removeEventListener('click', dismissMenu); }, + onupdate: (vnode) => { syncFilter(vnode.attrs.tab); }, diff --git a/webui-src/app/people/people_chat_tab.js b/webui-src/app/people/people_chat_tab.js index edff5cf..5221fa7 100644 --- a/webui-src/app/people/people_chat_tab.js +++ b/webui-src/app/people/people_chat_tab.js @@ -141,7 +141,7 @@ const ChatTab = () => { value: ownId, onchange: (e) => { State.selectedOwnGxsIdForChat = e.target.value; - initializeDistantChat(); + initializeDistantChat(true); }, }, State.ownGxsIds.map((id) => m('option', { value: id }, rs.userList.username(id)))), ]); @@ -170,6 +170,9 @@ const ChatTab = () => { }, (data, success) => { if (success) { + if (State.selectedId && State.activeDistantChats[State.selectedId]) { + delete State.activeDistantChats[State.selectedId]; + } State.chatPid = null; State.chatMessages = []; State.distantChatStatus = null; @@ -182,6 +185,7 @@ const ChatTab = () => { } }, }, [ + m('i.fas.fa-sign-out-alt'), 'Leave Chat', ]), diff --git a/webui-src/app/people/people_sidebar.js b/webui-src/app/people/people_sidebar.js index 75874cc..4b0109e 100644 --- a/webui-src/app/people/people_sidebar.js +++ b/webui-src/app/people/people_sidebar.js @@ -35,6 +35,17 @@ const PeopleSidebar = () => { // 1. Determine list based on mainTab ('people' vs 'chats') let displayItems = []; + // 0. Compute active chats count (conversations with real message history) + const allUserGroupIds = new Set((rs.userList.users || []).map((u) => u.mGroupId)); + Object.keys(State.chatHistoryMap || {}).forEach((id) => allUserGroupIds.add(id)); + let activeChatsCount = 0; + allUserGroupIds.forEach((gxsId) => { + const hist = State.chatHistoryMap && State.chatHistoryMap[gxsId]; + if (hist && hist.lastMsg && !isSystemMsg(hist.lastMsg)) { + activeChatsCount++; + } + }); + if (State.mainTab === 'people') { let baseList = []; if (State.activeFilter === 'own') { @@ -57,10 +68,7 @@ const PeopleSidebar = () => { }); } else { // Chats Tab: ONLY contacts and identities that have real chat history (ignoring system tunnel status logs) - const userGroupIds = new Set((rs.userList.users || []).map((u) => u.mGroupId)); - Object.keys(State.chatHistoryMap || {}).forEach((id) => userGroupIds.add(id)); - - displayItems = Array.from(userGroupIds) + displayItems = Array.from(allUserGroupIds) .map((gxsId) => { const entry = rs.userList.userMap[gxsId]; const name = entry && entry.name ? entry.name : (rs.userList.username(gxsId) || 'Unknown'); @@ -127,10 +135,15 @@ const PeopleSidebar = () => { m.redraw(); }, }, - [m('i.fas.fa-comments'), ' Chats'] + [ + m('i.fas.fa-comments'), + ' Chats', + activeChatsCount > 0 && m('span.segment-badge', activeChatsCount), + ] ), ]), + // 3. Sub-Filter Row (People Tab) State.mainTab === 'people' && m('.sub-filter-row', [ diff --git a/webui-src/app/people/people_state.js b/webui-src/app/people/people_state.js index 3867d1e..b4fbc3a 100644 --- a/webui-src/app/people/people_state.js +++ b/webui-src/app/people/people_state.js @@ -21,6 +21,7 @@ const State = { distantChatStatus: null, statusPollInterval: null, chatDisconnected: false, + activeDistantChats: {}, // gxsId -> { pid, status, messages, inputMsg, disconnected } activeMenu: null, showHistoryModal: false, historySearchQuery: '', @@ -28,6 +29,21 @@ const State = { isHistoryLoading: false, }; +function getDistantChatSession(gxsId) { + if (!gxsId) return null; + if (!State.activeDistantChats[gxsId]) { + State.activeDistantChats[gxsId] = { + pid: null, + status: null, + messages: [], + inputMsg: '', + disconnected: false, + }; + } + return State.activeDistantChats[gxsId]; +} + + function fetchIdDetails(gxsId) { if (!gxsId) return; if (State.gxsIdToDetailsMap[gxsId] === undefined) { @@ -175,11 +191,6 @@ function syncFilter(tab) { if (State.activeFilter !== newFilter) { State.activeFilter = newFilter; - State.selectedId = null; - State.chatPid = null; - State.chatMessages = []; - State.chatInputMsg = ''; - State.activeTab = 'details'; } } @@ -203,6 +214,8 @@ function getStatusTooltip(status) { function pollDistantChatStatus() { if (!State.chatPid) return; + const session = State.selectedId ? getDistantChatSession(State.selectedId) : null; + rs.rsJsonApiRequest( '/rsChats/getDistantChatStatus', { @@ -211,6 +224,7 @@ function pollDistantChatStatus() { (detail, success) => { if (success && detail.retval) { State.distantChatStatus = detail.info; + if (session) session.status = detail.info; if (detail.info.status === 2) { const text = 'Tunnel is secured. You can talk!'; @@ -258,7 +272,6 @@ function stopStatusPolling() { clearInterval(State.statusPollInterval); State.statusPollInterval = null; } - State.distantChatStatus = null; } function isSystemMsg(msgText) { @@ -273,11 +286,28 @@ function isSystemMsg(msgText) { ); } -function initializeDistantChat() { +function initializeDistantChat(force = false) { if (!State.selectedId || !State.selectedOwnGxsIdForChat) return; - State.chatPid = null; - State.chatMessages = [ + const session = getDistantChatSession(State.selectedId); + + // If chat session is already established/initiating for this peer and not forced/disconnected: + if (!force && session.pid && !session.disconnected) { + State.chatPid = session.pid; + State.chatMessages = session.messages; + State.distantChatStatus = session.status; + State.chatDisconnected = session.disconnected; + + loadChatMessages(); + pollDistantChatStatus(); + startStatusPolling(); + return; + } + + // Otherwise, start a new tunnel for this peer + session.pid = null; + session.status = null; + session.messages = [ { incoming: true, isSystem: true, @@ -285,6 +315,11 @@ function initializeDistantChat() { sendTime: Math.floor(Date.now() / 1000), } ]; + session.disconnected = false; + + State.chatPid = null; + State.chatMessages = session.messages; + State.distantChatStatus = null; State.chatDisconnected = false; m.redraw(); @@ -297,7 +332,9 @@ function initializeDistantChat() { }, (res) => { if (res && res.pid) { - State.chatPid = rs.idToHex(res.pid); + const hexPid = rs.idToHex(res.pid); + session.pid = hexPid; + State.chatPid = hexPid; State.distantChatStatus = null; loadChatMessages(); pollDistantChatStatus(); @@ -307,6 +344,7 @@ function initializeDistantChat() { ); } + function loadChatMessages() { if (!State.chatPid) return; @@ -564,6 +602,7 @@ function loadAllHistoryForSelectedPeer(callback) { module.exports = { State, + getDistantChatSession, isSystemMsg, preloadAllChatHistory, loadAllHistoryForSelectedPeer, @@ -586,3 +625,4 @@ module.exports = { loadChatMessages, sendDistantChatMessage, }; + diff --git a/webui-src/app/scss/pages/_people.scss b/webui-src/app/scss/pages/_people.scss index 8788b15..36f0810 100644 --- a/webui-src/app/scss/pages/_people.scss +++ b/webui-src/app/scss/pages/_people.scss @@ -58,3 +58,140 @@ img.avatar { color: green; cursor: pointer; } + +.people-sidebar-header { + display: flex; + flex-direction: column; + padding: 0.75rem 1rem 0.5rem 1rem; + gap: 0.75rem; + border-bottom: 1px solid #e2e8f0; + background-color: #ffffff; + + .searchbar-wrapper { + position: relative; + display: flex; + align-items: center; + + i.fa-search { + position: absolute; + left: 0.85rem; + color: #94a3b8; + font-size: 0.9rem; + } + + input.searchbar-input { + width: 100%; + padding: 0.5rem 0.75rem 0.5rem 2.25rem; + border: 1px solid #e2e8f0; + border-radius: 0.5rem; + font-size: 0.9rem; + background-color: #f8fafc; + color: #1e293b; + outline: none; + transition: all 0.2s ease; + + &:focus { + border-color: #3b82f6; + background-color: #ffffff; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); + } + } + } + + .segmented-control { + display: flex; + background-color: #f1f5f9; + padding: 3px; + border-radius: 0.5rem; + gap: 4px; + + button.segment-tab { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + font-weight: 600; + color: #64748b; + background: transparent; + border: none; + border-radius: 0.375rem; + cursor: pointer; + box-shadow: none; + transition: all 0.2s ease; + + &:hover { + color: #1e293b; + } + + &.active { + background-color: #ffffff; + color: #0f172a; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); + + .segment-badge { + background-color: #019dff; + color: #ffffff; + } + } + + .segment-badge { + display: inline-flex; + align-items: center; + justify-content: center; + background-color: #cbd5e1; + color: #334155; + font-size: 0.75rem; + font-weight: 700; + min-width: 1.25rem; + height: 1.25rem; + padding: 0 0.35rem; + border-radius: 9999px; + line-height: 1; + transition: all 0.2s ease; + } + } + } + + .sub-filter-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + min-height: 32px; + + select.filter-select { + padding: 0.35rem 0.6rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + font-size: 0.85rem; + font-weight: 600; + color: #475569; + background-color: #ffffff; + cursor: pointer; + outline: none; + } + + .btn-add-id { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 0.375rem; + background-color: #3b82f6; + color: #ffffff; + border: none; + cursor: pointer; + font-size: 0.9rem; + transition: background-color 0.2s; + + &:hover { + background-color: #2563eb; + } + } + } +} + diff --git a/webui-src/styles.css b/webui-src/styles.css index e3858ff..55041c2 100644 --- a/webui-src/styles.css +++ b/webui-src/styles.css @@ -689,6 +689,28 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); } +.people-sidebar-header .segmented-control button.segment-tab .segment-badge { + display: inline-flex; + align-items: center; + justify-content: center; + background-color: #cbd5e1; + color: #334155; + font-size: 0.75rem; + font-weight: 700; + min-width: 1.25rem; + height: 1.25rem; + padding: 0 0.35rem; + border-radius: 9999px; + line-height: 1; + transition: all 0.2s ease; +} + +.people-sidebar-header .segmented-control button.segment-tab.active .segment-badge { + background-color: #019dff; + color: #ffffff; +} + + /* Sub-header Filter Row */ .people-sidebar-header .sub-filter-row { display: flex; From dd66e7e1ff882c05765640b3843b4adb8dcc8eca Mon Sep 17 00:00:00 2001 From: defnax <9952056+defnax@users.noreply.github.com> Date: Sun, 26 Jul 2026 12:27:54 +0200 Subject: [PATCH 39/40] Added to Show chat room details Type & Security fields --- webui-src/app/chat/chat.js | 52 +++++++++++++++++++++++++++++ webui-src/app/scss/pages/_chat.scss | 2 +- webui-src/styles.css | 2 +- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index d25ed43..6f27b49 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -936,6 +936,46 @@ const ChatConversationView = () => { // ***************************** Page Layouts ****************************** +function getLobbyPrivacyInfo(room) { + if (!room) return { type: 'Public', security: 'Anonymous IDs accepted' }; + + const flags = + room.lobby_privacy_type !== undefined + ? room.lobby_privacy_type + : room.lobby_privacy_level !== undefined + ? room.lobby_privacy_level + : room.privacy_type !== undefined + ? room.privacy_type + : room.lobby_privacy !== undefined + ? room.lobby_privacy + : room.privacy_level !== undefined + ? room.privacy_level + : room.lobby_flags !== undefined + ? room.lobby_flags + : 0; + + let isPublic = + (flags & 4) !== 0 || + (flags & 1) !== 0 || + ChatHubState.selectedRoomType === 'public' || + room.is_public === true; + + if (flags === 1 || flags === 2) { + if ((flags & 1) === 1 && (flags & 4) === 0 && ChatHubState.selectedRoomType !== 'public') { + isPublic = false; + } + } + + const typeStr = isPublic ? 'Public' : 'Private'; + const isAuthOnly = (flags & 8) !== 0; + const securityStr = isAuthOnly ? 'No anonymous IDs' : 'Anonymous IDs accepted'; + + return { + type: typeStr, + security: securityStr, + }; +} + const ChatRoomDetailView = () => { return { view: () => { @@ -976,6 +1016,8 @@ const ChatRoomDetailView = () => { participantNames.sort((a, b) => a.localeCompare(b)); const lobbyHexId = rs.idToHex(room.lobby_id); + const privacy = getLobbyPrivacyInfo(room); + return m('.chat-room-detail-view', [ m('.detail-section', [ @@ -985,6 +1027,10 @@ const ChatRoomDetailView = () => { m('.info-value', room.lobby_name || ''), m('.info-label', 'Topic'), m('.info-value', room.lobby_topic || 'None'), + m('.info-label', 'Type'), + m('.info-value', privacy.type), + m('.info-label', 'Security'), + m('.info-value', privacy.security), m('.info-label', 'Participants'), m('.info-value', participantCount + ' users'), m('.info-label', 'Your Identity'), @@ -1020,6 +1066,7 @@ const ChatRoomJoinView = () => { const lobbyHexId = rs.idToHex(room.lobby_id); const participantCount = room.total_number_of_peers || 0; + const privacy = getLobbyPrivacyInfo(room); return m('.chat-room-detail-view', [ m('.detail-section', [ @@ -1029,11 +1076,16 @@ const ChatRoomJoinView = () => { m('.info-value', room.lobby_name || ''), m('.info-label', 'Topic'), m('.info-value', room.lobby_topic || 'None'), + m('.info-label', 'Type'), + m('.info-value', privacy.type), + m('.info-label', 'Security'), + m('.info-value', privacy.security), m('.info-label', 'Participants'), m('.info-value', participantCount + ' users'), ]), ]), + m('.detail-section', [ m('h3', 'Join Room'), m('p.join-description', 'Select an identity to join this chat room:'), diff --git a/webui-src/app/scss/pages/_chat.scss b/webui-src/app/scss/pages/_chat.scss index 3714e0e..5226f76 100644 --- a/webui-src/app/scss/pages/_chat.scss +++ b/webui-src/app/scss/pages/_chat.scss @@ -499,7 +499,7 @@ textarea.chatMsg { align-items: center; justify-content: center; color: #ffffff; - font-size: 0.85rem; + font-size: 1.35rem; } &.public-room .room-icon { diff --git a/webui-src/styles.css b/webui-src/styles.css index 55041c2..ef5e989 100644 --- a/webui-src/styles.css +++ b/webui-src/styles.css @@ -1010,7 +1010,7 @@ h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem align-items: center; justify-content: center; color: #ffffff; - font-size: 0.85rem; + font-size: 1.35rem; } .chat-room-list-item.public-room .room-icon { From 8ab15bfce17e74e96cf6c70b59e8d28b5e311958 Mon Sep 17 00:00:00 2001 From: Sumit Kumar Soni Date: Sun, 26 Jul 2026 17:29:25 +0530 Subject: [PATCH 40/40] chore: make sidebar layout better --- webui-src/app/main.js | 140 +- webui-src/app/scss/components/_navbar.scss | 4 +- webui-src/styles.css | 2247 +------------------- 3 files changed, 105 insertions(+), 2286 deletions(-) diff --git a/webui-src/app/main.js b/webui-src/app/main.js index 262160e..867acd3 100644 --- a/webui-src/app/main.js +++ b/webui-src/app/main.js @@ -45,7 +45,7 @@ const navbar = () => { display: 'flex', flexDirection: 'column', alignItems: 'center', - marginRight: '10px', + marginRight: isCollapsed ? 0 : '10px', }, }, [ @@ -53,28 +53,11 @@ const navbar = () => { src: 'images/retroshare.svg', alt: 'retroshare_icon', }), - m('i.fas.fa-circle', { - style: { - color: rs.connectionState.status ? '#2ecc71' : '#e74c3c', - fontSize: '0.6em', - marginTop: '5px', - transition: 'color 0.3s ease', - }, - title: rs.connectionState.status ? 'Connected to RetroShare Core' : 'Connection Lost', - }), - m('span.webui-version', { style: { fontSize: '0.7em', marginTop: '3px', color: '#888' } }, 'v131'), - m('i.fas.fa-sync-alt.refresh-icon', { - style: { fontSize: '0.8em', marginTop: '2px', cursor: 'pointer', color: '#888' }, - onclick: () => window.location.reload(true), - title: 'Force reload application', - }), ] ), - m('.nav-menu__logo-text', [ - m('h5', 'RetroShare'), - ]), + m('.nav-menu__logo-text', [m('h5', 'RetroShare')]), ]), - m('.nav-menu__box', [ + m('.nav-menu__box', { style: { flex: 1 } }, [ Object.keys(vnode.attrs.links).map((linkName) => { const active = m.route.get().split('/')[1] === linkName; return m( @@ -83,20 +66,9 @@ const navbar = () => { href: vnode.attrs.links[linkName], class: (active ? 'active-link' : '') + ' item', }, - [ - navIcon[linkName], - m('span', linkName.charAt(0).toUpperCase() + linkName.slice(1)), - ] + [navIcon[linkName], m('span', linkName.charAt(0).toUpperCase() + linkName.slice(1))] ); }), - m( - 'a.logout-link.item', - { - onclick: () => rs.logout(), - style: { marginTop: 'auto', cursor: 'pointer' }, - }, - [m('i.fas.fa-sign-out-alt'), m('span', 'Logout')] - ), m( 'button.toggle-nav', { @@ -105,6 +77,84 @@ const navbar = () => { m('i.fas.fa-angle-double-left') ), ]), + m( + '.nav-menu__footer', + { + style: { + marginTop: 'auto', + padding: '0.75rem 0 0', + color: '#888', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + gap: '0.75rem', + }, + }, + [ + m( + '.nav-menu__status', + { + style: { + display: 'flex', + flexDirection: isCollapsed ? 'column' : 'row', + alignItems: 'center', + justifyContent: 'center', + gap: isCollapsed ? '0.35rem' : '0.6rem', + }, + }, + [ + m('i.fas.fa-circle', { + style: { + color: rs.connectionState.status ? '#2ecc71' : '#e74c3c', + fontSize: '0.6em', + transition: 'color 0.3s ease', + }, + title: rs.connectionState.status + ? 'Connected to RetroShare Core' + : 'Connection Lost', + }), + m('span.webui-version', { style: { fontSize: '0.7em' } }, 'v131'), + m('i.fas.fa-sync-alt.refresh-icon', { + style: { cursor: 'pointer', fontSize: '0.8em' }, + onclick: () => window.location.reload(true), + title: 'Force reload application', + }), + ] + ), + m( + 'a.logout-link.item', + { + onclick: () => rs.logout(), + style: { + cursor: 'pointer', + margin: 0, + padding: isCollapsed ? '0.675rem 0' : '0.675rem 0.5rem', + width: isCollapsed ? '2.5rem' : '10rem', + display: 'flex', + alignItems: 'center', + justifyContent: isCollapsed ? 'center' : 'flex-start', + lineHeight: 1, + borderRadius: '0.5rem', + textDecoration: 'none', + color: '#ccc', + textTransform: 'capitalize', + }, + }, + [ + m('i.fas.fa-sign-out-alt.sidenav-icon', { + style: { + width: '2.5rem', + height: '1.4rem', + display: 'grid', + placeItems: 'center', + }, + }), + !isCollapsed && m('span', 'Logout'), + ] + ), + ] + ), ] ), }; @@ -128,10 +178,22 @@ const Layout = () => { config: '/config/network', }, }), - m('.main-container', { style: { display: 'flex', flexDirection: 'column', width: '100%', height: '100%', overflow: 'hidden' } }, [ - m('.tab-content', { style: { flex: '1', overflow: 'auto' } }, vnode.children), - m(statusbar) - ]), + m( + '.main-container', + { + style: { + display: 'flex', + flexDirection: 'column', + width: '100%', + height: '100%', + overflow: 'hidden', + }, + }, + [ + m('.tab-content', { style: { flex: '1', overflow: 'auto' } }, vnode.children), + m(statusbar), + ] + ), ]), }; }; @@ -208,8 +270,8 @@ m.route(document.getElementById('main'), '/', { if (rs.loginKey.isVerified && rs.loginKey.username && rs.loginKey.passwd) { rs.logon( { Authorization: `Basic ${btoa(`${rs.loginKey.username}:${rs.loginKey.passwd}`)}` }, - () => { }, // displayAuthError - () => { }, // displayErrorMessage - () => { } + () => {}, // displayAuthError + () => {}, // displayErrorMessage + () => {} ); } diff --git a/webui-src/app/scss/components/_navbar.scss b/webui-src/app/scss/components/_navbar.scss index 76a67db..ac38821 100644 --- a/webui-src/app/scss/components/_navbar.scss +++ b/webui-src/app/scss/components/_navbar.scss @@ -11,7 +11,7 @@ box-shadow: 0 5px 5px #222; @include flex(column, $align: center); height: 100%; - padding: 0.5rem 0.25rem; + padding: 0.25rem; margin-right: 0rem; &__logo { @@ -225,4 +225,4 @@ .sidebarquickview>h6 { display: none !important; } -} \ No newline at end of file +} diff --git a/webui-src/styles.css b/webui-src/styles.css index ef5e989..369212b 100644 --- a/webui-src/styles.css +++ b/webui-src/styles.css @@ -1,2250 +1,7 @@ -h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem}h5{font-size:1.25rem}h6{font-size:1.125rem}p{font-size:1rem}.small{font-size:.75rem}.bold{font-weight:bold}h1,h2,h3,h4,h5,h6,p{font-weight:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Bold.woff2") format("woff2"),url("./webfonts/Roboto-Bold.woff") format("woff"),url("./webfonts/Roboto-Bold.ttf") format("truetype");font-weight:700;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Bold.woff2") format("woff2"),url("./webfonts/Roboto-Bold.woff") format("woff"),url("./webfonts/Roboto-Bold.ttf") format("truetype");font-weight:bold;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-BoldItalic.woff2") format("woff2"),url("./webfonts/Roboto-BoldItalic.woff") format("woff"),url("./webfonts/Roboto-BoldItalic.ttf") format("truetype");font-weight:700;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-BoldItalic.woff2") format("woff2"),url("./webfonts/Roboto-BoldItalic.woff") format("woff"),url("./webfonts/Roboto-BoldItalic.ttf") format("truetype");font-weight:bold;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Medium.woff2") format("woff2"),url("./webfonts/Roboto-Medium.woff") format("woff"),url("./webfonts/Roboto-Medium.ttf") format("truetype");font-weight:500;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-MediumItalic.woff2") format("woff2"),url("./webfonts/Roboto-MediumItalic.woff") format("woff"),url("./webfonts/Roboto-MediumItalic.ttf") format("truetype");font-weight:500;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Regular.woff2") format("woff2"),url("./webfonts/Roboto-Regular.woff") format("woff"),url("./webfonts/Roboto-Regular.ttf") format("truetype");font-weight:400;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Regular.woff2") format("woff2"),url("./webfonts/Roboto-Regular.woff") format("woff"),url("./webfonts/Roboto-Regular.ttf") format("truetype");font-weight:normal;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Italic.woff2") format("woff2"),url("./webfonts/Roboto-Italic.woff") format("woff"),url("./webfonts/Roboto-Italic.ttf") format("truetype");font-weight:400;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Italic.woff2") format("woff2"),url("./webfonts/Roboto-Italic.woff") format("woff"),url("./webfonts/Roboto-Italic.ttf") format("truetype");font-weight:normal;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Light.woff2") format("woff2"),url("./webfonts/Roboto-Light.woff") format("woff"),url("./webfonts/Roboto-Light.ttf") format("truetype");font-weight:300;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-LightItalic.woff2") format("woff2"),url("./webfonts/Roboto-LightItalic.woff") format("woff"),url("./webfonts/Roboto-LightItalic.ttf") format("truetype");font-weight:300;font-style:italic}/*! +h1{font-size:3rem}h2{font-size:2.25rem}h3{font-size:1.875rem}h4{font-size:1.5rem}h5{font-size:1.25rem}h6{font-size:1.125rem}p{font-size:1rem}.small{font-size:.75rem}.bold{font-weight:bold}h1,h2,h3,h4,h5,h6,p{font-weight:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Bold.woff2") format("woff2"),url("./webfonts/Roboto-Bold.woff") format("woff"),url("./webfonts/Roboto-Bold.ttf") format("truetype");font-weight:700;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Bold.woff2") format("woff2"),url("./webfonts/Roboto-Bold.woff") format("woff"),url("./webfonts/Roboto-Bold.ttf") format("truetype");font-weight:bold;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-BoldItalic.woff2") format("woff2"),url("./webfonts/Roboto-BoldItalic.woff") format("woff"),url("./webfonts/Roboto-BoldItalic.ttf") format("truetype");font-weight:700;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-BoldItalic.woff2") format("woff2"),url("./webfonts/Roboto-BoldItalic.woff") format("woff"),url("./webfonts/Roboto-BoldItalic.ttf") format("truetype");font-weight:bold;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Medium.woff2") format("woff2"),url("./webfonts/Roboto-Medium.woff") format("woff"),url("./webfonts/Roboto-Medium.ttf") format("truetype");font-weight:500;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-MediumItalic.woff2") format("woff2"),url("./webfonts/Roboto-MediumItalic.woff") format("woff"),url("./webfonts/Roboto-MediumItalic.ttf") format("truetype");font-weight:500;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Regular.woff2") format("woff2"),url("./webfonts/Roboto-Regular.woff") format("woff"),url("./webfonts/Roboto-Regular.ttf") format("truetype");font-weight:400;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Regular.woff2") format("woff2"),url("./webfonts/Roboto-Regular.woff") format("woff"),url("./webfonts/Roboto-Regular.ttf") format("truetype");font-weight:normal;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Italic.woff2") format("woff2"),url("./webfonts/Roboto-Italic.woff") format("woff"),url("./webfonts/Roboto-Italic.ttf") format("truetype");font-weight:400;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Italic.woff2") format("woff2"),url("./webfonts/Roboto-Italic.woff") format("woff"),url("./webfonts/Roboto-Italic.ttf") format("truetype");font-weight:normal;font-style:italic}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-Light.woff2") format("woff2"),url("./webfonts/Roboto-Light.woff") format("woff"),url("./webfonts/Roboto-Light.ttf") format("truetype");font-weight:300;font-style:normal}@font-face{font-family:Roboto;src:url("./webfonts/Roboto-LightItalic.woff2") format("woff2"),url("./webfonts/Roboto-LightItalic.woff") format("woff"),url("./webfonts/Roboto-LightItalic.ttf") format("truetype");font-weight:300;font-style:italic}/*! * Font Awesome Free 5.9.0 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) */.fa,.fas,.far,.fal,.fab{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-0.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:solid .08em #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fas.fa-pull-left,.far.fa-pull-left,.fal.fa-pull-left,.fab.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fas.fa-pull-right,.far.fa-pull-right,.fal.fa-pull-right,.fab.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);transform:scale(1, -1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(-1, -1);transform:scale(-1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-flip-both{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:""}.fa-accessible-icon:before{content:""}.fa-accusoft:before{content:""}.fa-acquisitions-incorporated:before{content:""}.fa-ad:before{content:""}.fa-address-book:before{content:""}.fa-address-card:before{content:""}.fa-adjust:before{content:""}.fa-adn:before{content:""}.fa-adobe:before{content:""}.fa-adversal:before{content:""}.fa-affiliatetheme:before{content:""}.fa-air-freshener:before{content:""}.fa-airbnb:before{content:""}.fa-algolia:before{content:""}.fa-align-center:before{content:""}.fa-align-justify:before{content:""}.fa-align-left:before{content:""}.fa-align-right:before{content:""}.fa-alipay:before{content:""}.fa-allergies:before{content:""}.fa-amazon:before{content:""}.fa-amazon-pay:before{content:""}.fa-ambulance:before{content:""}.fa-american-sign-language-interpreting:before{content:""}.fa-amilia:before{content:""}.fa-anchor:before{content:""}.fa-android:before{content:""}.fa-angellist:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angry:before{content:""}.fa-angrycreative:before{content:""}.fa-angular:before{content:""}.fa-ankh:before{content:""}.fa-app-store:before{content:""}.fa-app-store-ios:before{content:""}.fa-apper:before{content:""}.fa-apple:before{content:""}.fa-apple-alt:before{content:""}.fa-apple-pay:before{content:""}.fa-archive:before{content:""}.fa-archway:before{content:""}.fa-arrow-alt-circle-down:before{content:""}.fa-arrow-alt-circle-left:before{content:""}.fa-arrow-alt-circle-right:before{content:""}.fa-arrow-alt-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-arrow-circle-left:before{content:""}.fa-arrow-circle-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-down:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrows-alt:before{content:""}.fa-arrows-alt-h:before{content:""}.fa-arrows-alt-v:before{content:""}.fa-artstation:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-asterisk:before{content:""}.fa-asymmetrik:before{content:""}.fa-at:before{content:""}.fa-atlas:before{content:""}.fa-atlassian:before{content:""}.fa-atom:before{content:""}.fa-audible:before{content:""}.fa-audio-description:before{content:""}.fa-autoprefixer:before{content:""}.fa-avianex:before{content:""}.fa-aviato:before{content:""}.fa-award:before{content:""}.fa-aws:before{content:""}.fa-baby:before{content:""}.fa-baby-carriage:before{content:""}.fa-backspace:before{content:""}.fa-backward:before{content:""}.fa-bacon:before{content:""}.fa-balance-scale:before{content:""}.fa-balance-scale-left:before{content:""}.fa-balance-scale-right:before{content:""}.fa-ban:before{content:""}.fa-band-aid:before{content:""}.fa-bandcamp:before{content:""}.fa-barcode:before{content:""}.fa-bars:before{content:""}.fa-baseball-ball:before{content:""}.fa-basketball-ball:before{content:""}.fa-bath:before{content:""}.fa-battery-empty:before{content:""}.fa-battery-full:before{content:""}.fa-battery-half:before{content:""}.fa-battery-quarter:before{content:""}.fa-battery-three-quarters:before{content:""}.fa-battle-net:before{content:""}.fa-bed:before{content:""}.fa-beer:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-bell:before{content:""}.fa-bell-slash:before{content:""}.fa-bezier-curve:before{content:""}.fa-bible:before{content:""}.fa-bicycle:before{content:""}.fa-biking:before{content:""}.fa-bimobject:before{content:""}.fa-binoculars:before{content:""}.fa-biohazard:before{content:""}.fa-birthday-cake:before{content:""}.fa-bitbucket:before{content:""}.fa-bitcoin:before{content:""}.fa-bity:before{content:""}.fa-black-tie:before{content:""}.fa-blackberry:before{content:""}.fa-blender:before{content:""}.fa-blender-phone:before{content:""}.fa-blind:before{content:""}.fa-blog:before{content:""}.fa-blogger:before{content:""}.fa-blogger-b:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-bold:before{content:""}.fa-bolt:before{content:""}.fa-bomb:before{content:""}.fa-bone:before{content:""}.fa-bong:before{content:""}.fa-book:before{content:""}.fa-book-dead:before{content:""}.fa-book-medical:before{content:""}.fa-book-open:before{content:""}.fa-book-reader:before{content:""}.fa-bookmark:before{content:""}.fa-bootstrap:before{content:""}.fa-border-all:before{content:""}.fa-border-none:before{content:""}.fa-border-style:before{content:""}.fa-bowling-ball:before{content:""}.fa-box:before{content:""}.fa-box-open:before{content:""}.fa-boxes:before{content:""}.fa-braille:before{content:""}.fa-brain:before{content:""}.fa-bread-slice:before{content:""}.fa-briefcase:before{content:""}.fa-briefcase-medical:before{content:""}.fa-broadcast-tower:before{content:""}.fa-broom:before{content:""}.fa-brush:before{content:""}.fa-btc:before{content:""}.fa-buffer:before{content:""}.fa-bug:before{content:""}.fa-building:before{content:""}.fa-bullhorn:before{content:""}.fa-bullseye:before{content:""}.fa-burn:before{content:""}.fa-buromobelexperte:before{content:""}.fa-bus:before{content:""}.fa-bus-alt:before{content:""}.fa-business-time:before{content:""}.fa-buysellads:before{content:""}.fa-calculator:before{content:""}.fa-calendar:before{content:""}.fa-calendar-alt:before{content:""}.fa-calendar-check:before{content:""}.fa-calendar-day:before{content:""}.fa-calendar-minus:before{content:""}.fa-calendar-plus:before{content:""}.fa-calendar-times:before{content:""}.fa-calendar-week:before{content:""}.fa-camera:before{content:""}.fa-camera-retro:before{content:""}.fa-campground:before{content:""}.fa-canadian-maple-leaf:before{content:""}.fa-candy-cane:before{content:""}.fa-cannabis:before{content:""}.fa-capsules:before{content:""}.fa-car:before{content:""}.fa-car-alt:before{content:""}.fa-car-battery:before{content:""}.fa-car-crash:before{content:""}.fa-car-side:before{content:""}.fa-caret-down:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-caret-square-down:before{content:""}.fa-caret-square-left:before{content:""}.fa-caret-square-right:before{content:""}.fa-caret-square-up:before{content:""}.fa-caret-up:before{content:""}.fa-carrot:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-cart-plus:before{content:""}.fa-cash-register:before{content:""}.fa-cat:before{content:""}.fa-cc-amazon-pay:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-apple-pay:before{content:""}.fa-cc-diners-club:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-cc-visa:before{content:""}.fa-centercode:before{content:""}.fa-centos:before{content:""}.fa-certificate:before{content:""}.fa-chair:before{content:""}.fa-chalkboard:before{content:""}.fa-chalkboard-teacher:before{content:""}.fa-charging-station:before{content:""}.fa-chart-area:before{content:""}.fa-chart-bar:before{content:""}.fa-chart-line:before{content:""}.fa-chart-pie:before{content:""}.fa-check:before{content:""}.fa-check-circle:before{content:""}.fa-check-double:before{content:""}.fa-check-square:before{content:""}.fa-cheese:before{content:""}.fa-chess:before{content:""}.fa-chess-bishop:before{content:""}.fa-chess-board:before{content:""}.fa-chess-king:before{content:""}.fa-chess-knight:before{content:""}.fa-chess-pawn:before{content:""}.fa-chess-queen:before{content:""}.fa-chess-rook:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-down:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-chevron-up:before{content:""}.fa-child:before{content:""}.fa-chrome:before{content:""}.fa-chromecast:before{content:""}.fa-church:before{content:""}.fa-circle:before{content:""}.fa-circle-notch:before{content:""}.fa-city:before{content:""}.fa-clinic-medical:before{content:""}.fa-clipboard:before{content:""}.fa-clipboard-check:before{content:""}.fa-clipboard-list:before{content:""}.fa-clock:before{content:""}.fa-clone:before{content:""}.fa-closed-captioning:before{content:""}.fa-cloud:before{content:""}.fa-cloud-download-alt:before{content:""}.fa-cloud-meatball:before{content:""}.fa-cloud-moon:before{content:""}.fa-cloud-moon-rain:before{content:""}.fa-cloud-rain:before{content:""}.fa-cloud-showers-heavy:before{content:""}.fa-cloud-sun:before{content:""}.fa-cloud-sun-rain:before{content:""}.fa-cloud-upload-alt:before{content:""}.fa-cloudscale:before{content:""}.fa-cloudsmith:before{content:""}.fa-cloudversify:before{content:""}.fa-cocktail:before{content:""}.fa-code:before{content:""}.fa-code-branch:before{content:""}.fa-codepen:before{content:""}.fa-codiepie:before{content:""}.fa-coffee:before{content:""}.fa-cog:before{content:""}.fa-cogs:before{content:""}.fa-coins:before{content:""}.fa-columns:before{content:""}.fa-comment:before{content:""}.fa-comment-alt:before{content:""}.fa-comment-dollar:before{content:""}.fa-comment-dots:before{content:""}.fa-comment-medical:before{content:""}.fa-comment-slash:before{content:""}.fa-comments:before{content:""}.fa-comments-dollar:before{content:""}.fa-compact-disc:before{content:""}.fa-compass:before{content:""}.fa-compress:before{content:""}.fa-compress-arrows-alt:before{content:""}.fa-concierge-bell:before{content:""}.fa-confluence:before{content:""}.fa-connectdevelop:before{content:""}.fa-contao:before{content:""}.fa-cookie:before{content:""}.fa-cookie-bite:before{content:""}.fa-copy:before{content:""}.fa-copyright:before{content:""}.fa-couch:before{content:""}.fa-cpanel:before{content:""}.fa-creative-commons:before{content:""}.fa-creative-commons-by:before{content:""}.fa-creative-commons-nc:before{content:""}.fa-creative-commons-nc-eu:before{content:""}.fa-creative-commons-nc-jp:before{content:""}.fa-creative-commons-nd:before{content:""}.fa-creative-commons-pd:before{content:""}.fa-creative-commons-pd-alt:before{content:""}.fa-creative-commons-remix:before{content:""}.fa-creative-commons-sa:before{content:""}.fa-creative-commons-sampling:before{content:""}.fa-creative-commons-sampling-plus:before{content:""}.fa-creative-commons-share:before{content:""}.fa-creative-commons-zero:before{content:""}.fa-credit-card:before{content:""}.fa-critical-role:before{content:""}.fa-crop:before{content:""}.fa-crop-alt:before{content:""}.fa-cross:before{content:""}.fa-crosshairs:before{content:""}.fa-crow:before{content:""}.fa-crown:before{content:""}.fa-crutch:before{content:""}.fa-css3:before{content:""}.fa-css3-alt:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-cut:before{content:""}.fa-cuttlefish:before{content:""}.fa-d-and-d:before{content:""}.fa-d-and-d-beyond:before{content:""}.fa-dashcube:before{content:""}.fa-database:before{content:""}.fa-deaf:before{content:""}.fa-delicious:before{content:""}.fa-democrat:before{content:""}.fa-deploydog:before{content:""}.fa-deskpro:before{content:""}.fa-desktop:before{content:""}.fa-dev:before{content:""}.fa-deviantart:before{content:""}.fa-dharmachakra:before{content:""}.fa-dhl:before{content:""}.fa-diagnoses:before{content:""}.fa-diaspora:before{content:""}.fa-dice:before{content:""}.fa-dice-d20:before{content:""}.fa-dice-d6:before{content:""}.fa-dice-five:before{content:""}.fa-dice-four:before{content:""}.fa-dice-one:before{content:""}.fa-dice-six:before{content:""}.fa-dice-three:before{content:""}.fa-dice-two:before{content:""}.fa-digg:before{content:""}.fa-digital-ocean:before{content:""}.fa-digital-tachograph:before{content:""}.fa-directions:before{content:""}.fa-discord:before{content:""}.fa-discourse:before{content:""}.fa-divide:before{content:""}.fa-dizzy:before{content:""}.fa-dna:before{content:""}.fa-dochub:before{content:""}.fa-docker:before{content:""}.fa-dog:before{content:""}.fa-dollar-sign:before{content:""}.fa-dolly:before{content:""}.fa-dolly-flatbed:before{content:""}.fa-donate:before{content:""}.fa-door-closed:before{content:""}.fa-door-open:before{content:""}.fa-dot-circle:before{content:""}.fa-dove:before{content:""}.fa-download:before{content:""}.fa-draft2digital:before{content:""}.fa-drafting-compass:before{content:""}.fa-dragon:before{content:""}.fa-draw-polygon:before{content:""}.fa-dribbble:before{content:""}.fa-dribbble-square:before{content:""}.fa-dropbox:before{content:""}.fa-drum:before{content:""}.fa-drum-steelpan:before{content:""}.fa-drumstick-bite:before{content:""}.fa-drupal:before{content:""}.fa-dumbbell:before{content:""}.fa-dumpster:before{content:""}.fa-dumpster-fire:before{content:""}.fa-dungeon:before{content:""}.fa-dyalog:before{content:""}.fa-earlybirds:before{content:""}.fa-ebay:before{content:""}.fa-edge:before{content:""}.fa-edit:before{content:""}.fa-egg:before{content:""}.fa-eject:before{content:""}.fa-elementor:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-ello:before{content:""}.fa-ember:before{content:""}.fa-empire:before{content:""}.fa-envelope:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-text:before{content:""}.fa-envelope-square:before{content:""}.fa-envira:before{content:""}.fa-equals:before{content:""}.fa-eraser:before{content:""}.fa-erlang:before{content:""}.fa-ethereum:before{content:""}.fa-ethernet:before{content:""}.fa-etsy:before{content:""}.fa-euro-sign:before{content:""}.fa-evernote:before{content:""}.fa-exchange-alt:before{content:""}.fa-exclamation:before{content:""}.fa-exclamation-circle:before{content:""}.fa-exclamation-triangle:before{content:""}.fa-expand:before{content:""}.fa-expand-arrows-alt:before{content:""}.fa-expeditedssl:before{content:""}.fa-external-link-alt:before{content:""}.fa-external-link-square-alt:before{content:""}.fa-eye:before{content:""}.fa-eye-dropper:before{content:""}.fa-eye-slash:before{content:""}.fa-facebook:before{content:""}.fa-facebook-f:before{content:""}.fa-facebook-messenger:before{content:""}.fa-facebook-square:before{content:""}.fa-fan:before{content:""}.fa-fantasy-flight-games:before{content:""}.fa-fast-backward:before{content:""}.fa-fast-forward:before{content:""}.fa-fax:before{content:""}.fa-feather:before{content:""}.fa-feather-alt:before{content:""}.fa-fedex:before{content:""}.fa-fedora:before{content:""}.fa-female:before{content:""}.fa-fighter-jet:before{content:""}.fa-figma:before{content:""}.fa-file:before{content:""}.fa-file-alt:before{content:""}.fa-file-archive:before{content:""}.fa-file-audio:before{content:""}.fa-file-code:before{content:""}.fa-file-contract:before{content:""}.fa-file-csv:before{content:""}.fa-file-download:before{content:""}.fa-file-excel:before{content:""}.fa-file-export:before{content:""}.fa-file-image:before{content:""}.fa-file-import:before{content:""}.fa-file-invoice:before{content:""}.fa-file-invoice-dollar:before{content:""}.fa-file-medical:before{content:""}.fa-file-medical-alt:before{content:""}.fa-file-pdf:before{content:""}.fa-file-powerpoint:before{content:""}.fa-file-prescription:before{content:""}.fa-file-signature:before{content:""}.fa-file-upload:before{content:""}.fa-file-video:before{content:""}.fa-file-word:before{content:""}.fa-fill:before{content:""}.fa-fill-drip:before{content:""}.fa-film:before{content:""}.fa-filter:before{content:""}.fa-fingerprint:before{content:""}.fa-fire:before{content:""}.fa-fire-alt:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-firefox:before{content:""}.fa-first-aid:before{content:""}.fa-first-order:before{content:""}.fa-first-order-alt:before{content:""}.fa-firstdraft:before{content:""}.fa-fish:before{content:""}.fa-fist-raised:before{content:""}.fa-flag:before{content:""}.fa-flag-checkered:before{content:""}.fa-flag-usa:before{content:""}.fa-flask:before{content:""}.fa-flickr:before{content:""}.fa-flipboard:before{content:""}.fa-flushed:before{content:""}.fa-fly:before{content:""}.fa-folder:before{content:""}.fa-folder-minus:before{content:""}.fa-folder-open:before{content:""}.fa-folder-plus:before{content:""}.fa-font:before{content:""}.fa-font-awesome:before{content:""}.fa-font-awesome-alt:before{content:""}.fa-font-awesome-flag:before{content:""}.fa-font-awesome-logo-full:before{content:""}.fa-fonticons:before{content:""}.fa-fonticons-fi:before{content:""}.fa-football-ball:before{content:""}.fa-fort-awesome:before{content:""}.fa-fort-awesome-alt:before{content:""}.fa-forumbee:before{content:""}.fa-forward:before{content:""}.fa-foursquare:before{content:""}.fa-free-code-camp:before{content:""}.fa-freebsd:before{content:""}.fa-frog:before{content:""}.fa-frown:before{content:""}.fa-frown-open:before{content:""}.fa-fulcrum:before{content:""}.fa-funnel-dollar:before{content:""}.fa-futbol:before{content:""}.fa-galactic-republic:before{content:""}.fa-galactic-senate:before{content:""}.fa-gamepad:before{content:""}.fa-gas-pump:before{content:""}.fa-gavel:before{content:""}.fa-gem:before{content:""}.fa-genderless:before{content:""}.fa-get-pocket:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-ghost:before{content:""}.fa-gift:before{content:""}.fa-gifts:before{content:""}.fa-git:before{content:""}.fa-git-alt:before{content:""}.fa-git-square:before{content:""}.fa-github:before{content:""}.fa-github-alt:before{content:""}.fa-github-square:before{content:""}.fa-gitkraken:before{content:""}.fa-gitlab:before{content:""}.fa-gitter:before{content:""}.fa-glass-cheers:before{content:""}.fa-glass-martini:before{content:""}.fa-glass-martini-alt:before{content:""}.fa-glass-whiskey:before{content:""}.fa-glasses:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-globe:before{content:""}.fa-globe-africa:before{content:""}.fa-globe-americas:before{content:""}.fa-globe-asia:before{content:""}.fa-globe-europe:before{content:""}.fa-gofore:before{content:""}.fa-golf-ball:before{content:""}.fa-goodreads:before{content:""}.fa-goodreads-g:before{content:""}.fa-google:before{content:""}.fa-google-drive:before{content:""}.fa-google-play:before{content:""}.fa-google-plus:before{content:""}.fa-google-plus-g:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-wallet:before{content:""}.fa-gopuram:before{content:""}.fa-graduation-cap:before{content:""}.fa-gratipay:before{content:""}.fa-grav:before{content:""}.fa-greater-than:before{content:""}.fa-greater-than-equal:before{content:""}.fa-grimace:before{content:""}.fa-grin:before{content:""}.fa-grin-alt:before{content:""}.fa-grin-beam:before{content:""}.fa-grin-beam-sweat:before{content:""}.fa-grin-hearts:before{content:""}.fa-grin-squint:before{content:""}.fa-grin-squint-tears:before{content:""}.fa-grin-stars:before{content:""}.fa-grin-tears:before{content:""}.fa-grin-tongue:before{content:""}.fa-grin-tongue-squint:before{content:""}.fa-grin-tongue-wink:before{content:""}.fa-grin-wink:before{content:""}.fa-grip-horizontal:before{content:""}.fa-grip-lines:before{content:""}.fa-grip-lines-vertical:before{content:""}.fa-grip-vertical:before{content:""}.fa-gripfire:before{content:""}.fa-grunt:before{content:""}.fa-guitar:before{content:""}.fa-gulp:before{content:""}.fa-h-square:before{content:""}.fa-hacker-news:before{content:""}.fa-hacker-news-square:before{content:""}.fa-hackerrank:before{content:""}.fa-hamburger:before{content:""}.fa-hammer:before{content:""}.fa-hamsa:before{content:""}.fa-hand-holding:before{content:""}.fa-hand-holding-heart:before{content:""}.fa-hand-holding-usd:before{content:""}.fa-hand-lizard:before{content:""}.fa-hand-middle-finger:before{content:""}.fa-hand-paper:before{content:""}.fa-hand-peace:before{content:""}.fa-hand-point-down:before{content:""}.fa-hand-point-left:before{content:""}.fa-hand-point-right:before{content:""}.fa-hand-point-up:before{content:""}.fa-hand-pointer:before{content:""}.fa-hand-rock:before{content:""}.fa-hand-scissors:before{content:""}.fa-hand-spock:before{content:""}.fa-hands:before{content:""}.fa-hands-helping:before{content:""}.fa-handshake:before{content:""}.fa-hanukiah:before{content:""}.fa-hard-hat:before{content:""}.fa-hashtag:before{content:""}.fa-hat-wizard:before{content:""}.fa-haykal:before{content:""}.fa-hdd:before{content:""}.fa-heading:before{content:""}.fa-headphones:before{content:""}.fa-headphones-alt:before{content:""}.fa-headset:before{content:""}.fa-heart:before{content:""}.fa-heart-broken:before{content:""}.fa-heartbeat:before{content:""}.fa-helicopter:before{content:""}.fa-highlighter:before{content:""}.fa-hiking:before{content:""}.fa-hippo:before{content:""}.fa-hips:before{content:""}.fa-hire-a-helper:before{content:""}.fa-history:before{content:""}.fa-hockey-puck:before{content:""}.fa-holly-berry:before{content:""}.fa-home:before{content:""}.fa-hooli:before{content:""}.fa-hornbill:before{content:""}.fa-horse:before{content:""}.fa-horse-head:before{content:""}.fa-hospital:before{content:""}.fa-hospital-alt:before{content:""}.fa-hospital-symbol:before{content:""}.fa-hot-tub:before{content:""}.fa-hotdog:before{content:""}.fa-hotel:before{content:""}.fa-hotjar:before{content:""}.fa-hourglass:before{content:""}.fa-hourglass-end:before{content:""}.fa-hourglass-half:before{content:""}.fa-hourglass-start:before{content:""}.fa-house-damage:before{content:""}.fa-houzz:before{content:""}.fa-hryvnia:before{content:""}.fa-html5:before{content:""}.fa-hubspot:before{content:""}.fa-i-cursor:before{content:""}.fa-ice-cream:before{content:""}.fa-icicles:before{content:""}.fa-icons:before{content:""}.fa-id-badge:before{content:""}.fa-id-card:before{content:""}.fa-id-card-alt:before{content:""}.fa-igloo:before{content:""}.fa-image:before{content:""}.fa-images:before{content:""}.fa-imdb:before{content:""}.fa-inbox:before{content:""}.fa-indent:before{content:""}.fa-industry:before{content:""}.fa-infinity:before{content:""}.fa-info:before{content:""}.fa-info-circle:before{content:""}.fa-instagram:before{content:""}.fa-intercom:before{content:""}.fa-internet-explorer:before{content:""}.fa-invision:before{content:""}.fa-ioxhost:before{content:""}.fa-italic:before{content:""}.fa-itch-io:before{content:""}.fa-itunes:before{content:""}.fa-itunes-note:before{content:""}.fa-java:before{content:""}.fa-jedi:before{content:""}.fa-jedi-order:before{content:""}.fa-jenkins:before{content:""}.fa-jira:before{content:""}.fa-joget:before{content:""}.fa-joint:before{content:""}.fa-joomla:before{content:""}.fa-journal-whills:before{content:""}.fa-js:before{content:""}.fa-js-square:before{content:""}.fa-jsfiddle:before{content:""}.fa-kaaba:before{content:""}.fa-kaggle:before{content:""}.fa-key:before{content:""}.fa-keybase:before{content:""}.fa-keyboard:before{content:""}.fa-keycdn:before{content:""}.fa-khanda:before{content:""}.fa-kickstarter:before{content:""}.fa-kickstarter-k:before{content:""}.fa-kiss:before{content:""}.fa-kiss-beam:before{content:""}.fa-kiss-wink-heart:before{content:""}.fa-kiwi-bird:before{content:""}.fa-korvue:before{content:""}.fa-landmark:before{content:""}.fa-language:before{content:""}.fa-laptop:before{content:""}.fa-laptop-code:before{content:""}.fa-laptop-medical:before{content:""}.fa-laravel:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-laugh:before{content:""}.fa-laugh-beam:before{content:""}.fa-laugh-squint:before{content:""}.fa-laugh-wink:before{content:""}.fa-layer-group:before{content:""}.fa-leaf:before{content:""}.fa-leanpub:before{content:""}.fa-lemon:before{content:""}.fa-less:before{content:""}.fa-less-than:before{content:""}.fa-less-than-equal:before{content:""}.fa-level-down-alt:before{content:""}.fa-level-up-alt:before{content:""}.fa-life-ring:before{content:""}.fa-lightbulb:before{content:""}.fa-line:before{content:""}.fa-link:before{content:""}.fa-linkedin:before{content:""}.fa-linkedin-in:before{content:""}.fa-linode:before{content:""}.fa-linux:before{content:""}.fa-lira-sign:before{content:""}.fa-list:before{content:""}.fa-list-alt:before{content:""}.fa-list-ol:before{content:""}.fa-list-ul:before{content:""}.fa-location-arrow:before{content:""}.fa-lock:before{content:""}.fa-lock-open:before{content:""}.fa-long-arrow-alt-down:before{content:""}.fa-long-arrow-alt-left:before{content:""}.fa-long-arrow-alt-right:before{content:""}.fa-long-arrow-alt-up:before{content:""}.fa-low-vision:before{content:""}.fa-luggage-cart:before{content:""}.fa-lyft:before{content:""}.fa-magento:before{content:""}.fa-magic:before{content:""}.fa-magnet:before{content:""}.fa-mail-bulk:before{content:""}.fa-mailchimp:before{content:""}.fa-male:before{content:""}.fa-mandalorian:before{content:""}.fa-map:before{content:""}.fa-map-marked:before{content:""}.fa-map-marked-alt:before{content:""}.fa-map-marker:before{content:""}.fa-map-marker-alt:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-markdown:before{content:""}.fa-marker:before{content:""}.fa-mars:before{content:""}.fa-mars-double:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mask:before{content:""}.fa-mastodon:before{content:""}.fa-maxcdn:before{content:""}.fa-medal:before{content:""}.fa-medapps:before{content:""}.fa-medium:before{content:""}.fa-medium-m:before{content:""}.fa-medkit:before{content:""}.fa-medrt:before{content:""}.fa-meetup:before{content:""}.fa-megaport:before{content:""}.fa-meh:before{content:""}.fa-meh-blank:before{content:""}.fa-meh-rolling-eyes:before{content:""}.fa-memory:before{content:""}.fa-mendeley:before{content:""}.fa-menorah:before{content:""}.fa-mercury:before{content:""}.fa-meteor:before{content:""}.fa-microchip:before{content:""}.fa-microphone:before{content:""}.fa-microphone-alt:before{content:""}.fa-microphone-alt-slash:before{content:""}.fa-microphone-slash:before{content:""}.fa-microscope:before{content:""}.fa-microsoft:before{content:""}.fa-minus:before{content:""}.fa-minus-circle:before{content:""}.fa-minus-square:before{content:""}.fa-mitten:before{content:""}.fa-mix:before{content:""}.fa-mixcloud:before{content:""}.fa-mizuni:before{content:""}.fa-mobile:before{content:""}.fa-mobile-alt:before{content:""}.fa-modx:before{content:""}.fa-monero:before{content:""}.fa-money-bill:before{content:""}.fa-money-bill-alt:before{content:""}.fa-money-bill-wave:before{content:""}.fa-money-bill-wave-alt:before{content:""}.fa-money-check:before{content:""}.fa-money-check-alt:before{content:""}.fa-monument:before{content:""}.fa-moon:before{content:""}.fa-mortar-pestle:before{content:""}.fa-mosque:before{content:""}.fa-motorcycle:before{content:""}.fa-mountain:before{content:""}.fa-mouse-pointer:before{content:""}.fa-mug-hot:before{content:""}.fa-music:before{content:""}.fa-napster:before{content:""}.fa-neos:before{content:""}.fa-network-wired:before{content:""}.fa-neuter:before{content:""}.fa-newspaper:before{content:""}.fa-nimblr:before{content:""}.fa-node:before{content:""}.fa-node-js:before{content:""}.fa-not-equal:before{content:""}.fa-notes-medical:before{content:""}.fa-npm:before{content:""}.fa-ns8:before{content:""}.fa-nutritionix:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-oil-can:before{content:""}.fa-old-republic:before{content:""}.fa-om:before{content:""}.fa-opencart:before{content:""}.fa-openid:before{content:""}.fa-opera:before{content:""}.fa-optin-monster:before{content:""}.fa-osi:before{content:""}.fa-otter:before{content:""}.fa-outdent:before{content:""}.fa-page4:before{content:""}.fa-pagelines:before{content:""}.fa-pager:before{content:""}.fa-paint-brush:before{content:""}.fa-paint-roller:before{content:""}.fa-palette:before{content:""}.fa-palfed:before{content:""}.fa-pallet:before{content:""}.fa-paper-plane:before{content:""}.fa-paperclip:before{content:""}.fa-parachute-box:before{content:""}.fa-paragraph:before{content:""}.fa-parking:before{content:""}.fa-passport:before{content:""}.fa-pastafarianism:before{content:""}.fa-paste:before{content:""}.fa-patreon:before{content:""}.fa-pause:before{content:""}.fa-pause-circle:before{content:""}.fa-paw:before{content:""}.fa-paypal:before{content:""}.fa-peace:before{content:""}.fa-pen:before{content:""}.fa-pen-alt:before{content:""}.fa-pen-fancy:before{content:""}.fa-pen-nib:before{content:""}.fa-pen-square:before{content:""}.fa-pencil-alt:before{content:""}.fa-pencil-ruler:before{content:""}.fa-penny-arcade:before{content:""}.fa-people-carry:before{content:""}.fa-pepper-hot:before{content:""}.fa-percent:before{content:""}.fa-percentage:before{content:""}.fa-periscope:before{content:""}.fa-person-booth:before{content:""}.fa-phabricator:before{content:""}.fa-phoenix-framework:before{content:""}.fa-phoenix-squadron:before{content:""}.fa-phone:before{content:""}.fa-phone-alt:before{content:""}.fa-phone-slash:before{content:""}.fa-phone-square:before{content:""}.fa-phone-square-alt:before{content:""}.fa-phone-volume:before{content:""}.fa-photo-video:before{content:""}.fa-php:before{content:""}.fa-pied-piper:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-pied-piper-hat:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-piggy-bank:before{content:""}.fa-pills:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-p:before{content:""}.fa-pinterest-square:before{content:""}.fa-pizza-slice:before{content:""}.fa-place-of-worship:before{content:""}.fa-plane:before{content:""}.fa-plane-arrival:before{content:""}.fa-plane-departure:before{content:""}.fa-play:before{content:""}.fa-play-circle:before{content:""}.fa-playstation:before{content:""}.fa-plug:before{content:""}.fa-plus:before{content:""}.fa-plus-circle:before{content:""}.fa-plus-square:before{content:""}.fa-podcast:before{content:""}.fa-poll:before{content:""}.fa-poll-h:before{content:""}.fa-poo:before{content:""}.fa-poo-storm:before{content:""}.fa-poop:before{content:""}.fa-portrait:before{content:""}.fa-pound-sign:before{content:""}.fa-power-off:before{content:""}.fa-pray:before{content:""}.fa-praying-hands:before{content:""}.fa-prescription:before{content:""}.fa-prescription-bottle:before{content:""}.fa-prescription-bottle-alt:before{content:""}.fa-print:before{content:""}.fa-procedures:before{content:""}.fa-product-hunt:before{content:""}.fa-project-diagram:before{content:""}.fa-pushed:before{content:""}.fa-puzzle-piece:before{content:""}.fa-python:before{content:""}.fa-qq:before{content:""}.fa-qrcode:before{content:""}.fa-question:before{content:""}.fa-question-circle:before{content:""}.fa-quidditch:before{content:""}.fa-quinscape:before{content:""}.fa-quora:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-quran:before{content:""}.fa-r-project:before{content:""}.fa-radiation:before{content:""}.fa-radiation-alt:before{content:""}.fa-rainbow:before{content:""}.fa-random:before{content:""}.fa-raspberry-pi:before{content:""}.fa-ravelry:before{content:""}.fa-react:before{content:""}.fa-reacteurope:before{content:""}.fa-readme:before{content:""}.fa-rebel:before{content:""}.fa-receipt:before{content:""}.fa-recycle:before{content:""}.fa-red-river:before{content:""}.fa-reddit:before{content:""}.fa-reddit-alien:before{content:""}.fa-reddit-square:before{content:""}.fa-redhat:before{content:""}.fa-redo:before{content:""}.fa-redo-alt:before{content:""}.fa-registered:before{content:""}.fa-remove-format:before{content:""}.fa-renren:before{content:""}.fa-reply:before{content:""}.fa-reply-all:before{content:""}.fa-replyd:before{content:""}.fa-republican:before{content:""}.fa-researchgate:before{content:""}.fa-resolving:before{content:""}.fa-restroom:before{content:""}.fa-retweet:before{content:""}.fa-rev:before{content:""}.fa-ribbon:before{content:""}.fa-ring:before{content:""}.fa-road:before{content:""}.fa-robot:before{content:""}.fa-rocket:before{content:""}.fa-rocketchat:before{content:""}.fa-rockrms:before{content:""}.fa-route:before{content:""}.fa-rss:before{content:""}.fa-rss-square:before{content:""}.fa-ruble-sign:before{content:""}.fa-ruler:before{content:""}.fa-ruler-combined:before{content:""}.fa-ruler-horizontal:before{content:""}.fa-ruler-vertical:before{content:""}.fa-running:before{content:""}.fa-rupee-sign:before{content:""}.fa-sad-cry:before{content:""}.fa-sad-tear:before{content:""}.fa-safari:before{content:""}.fa-salesforce:before{content:""}.fa-sass:before{content:""}.fa-satellite:before{content:""}.fa-satellite-dish:before{content:""}.fa-save:before{content:""}.fa-schlix:before{content:""}.fa-school:before{content:""}.fa-screwdriver:before{content:""}.fa-scribd:before{content:""}.fa-scroll:before{content:""}.fa-sd-card:before{content:""}.fa-search:before{content:""}.fa-search-dollar:before{content:""}.fa-search-location:before{content:""}.fa-search-minus:before{content:""}.fa-search-plus:before{content:""}.fa-searchengin:before{content:""}.fa-seedling:before{content:""}.fa-sellcast:before{content:""}.fa-sellsy:before{content:""}.fa-server:before{content:""}.fa-servicestack:before{content:""}.fa-shapes:before{content:""}.fa-share:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-share-square:before{content:""}.fa-shekel-sign:before{content:""}.fa-shield-alt:before{content:""}.fa-ship:before{content:""}.fa-shipping-fast:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-shoe-prints:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-shopping-cart:before{content:""}.fa-shopware:before{content:""}.fa-shower:before{content:""}.fa-shuttle-van:before{content:""}.fa-sign:before{content:""}.fa-sign-in-alt:before{content:""}.fa-sign-language:before{content:""}.fa-sign-out-alt:before{content:""}.fa-signal:before{content:""}.fa-signature:before{content:""}.fa-sim-card:before{content:""}.fa-simplybuilt:before{content:""}.fa-sistrix:before{content:""}.fa-sitemap:before{content:""}.fa-sith:before{content:""}.fa-skating:before{content:""}.fa-sketch:before{content:""}.fa-skiing:before{content:""}.fa-skiing-nordic:before{content:""}.fa-skull:before{content:""}.fa-skull-crossbones:before{content:""}.fa-skyatlas:before{content:""}.fa-skype:before{content:""}.fa-slack:before{content:""}.fa-slack-hash:before{content:""}.fa-slash:before{content:""}.fa-sleigh:before{content:""}.fa-sliders-h:before{content:""}.fa-slideshare:before{content:""}.fa-smile:before{content:""}.fa-smile-beam:before{content:""}.fa-smile-wink:before{content:""}.fa-smog:before{content:""}.fa-smoking:before{content:""}.fa-smoking-ban:before{content:""}.fa-sms:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-snowboarding:before{content:""}.fa-snowflake:before{content:""}.fa-snowman:before{content:""}.fa-snowplow:before{content:""}.fa-socks:before{content:""}.fa-solar-panel:before{content:""}.fa-sort:before{content:""}.fa-sort-alpha-down:before{content:""}.fa-sort-alpha-down-alt:before{content:""}.fa-sort-alpha-up:before{content:""}.fa-sort-alpha-up-alt:before{content:""}.fa-sort-amount-down:before{content:""}.fa-sort-amount-down-alt:before{content:""}.fa-sort-amount-up:before{content:""}.fa-sort-amount-up-alt:before{content:""}.fa-sort-down:before{content:""}.fa-sort-numeric-down:before{content:""}.fa-sort-numeric-down-alt:before{content:""}.fa-sort-numeric-up:before{content:""}.fa-sort-numeric-up-alt:before{content:""}.fa-sort-up:before{content:""}.fa-soundcloud:before{content:""}.fa-sourcetree:before{content:""}.fa-spa:before{content:""}.fa-space-shuttle:before{content:""}.fa-speakap:before{content:""}.fa-speaker-deck:before{content:""}.fa-spell-check:before{content:""}.fa-spider:before{content:""}.fa-spinner:before{content:""}.fa-splotch:before{content:""}.fa-spotify:before{content:""}.fa-spray-can:before{content:""}.fa-square:before{content:""}.fa-square-full:before{content:""}.fa-square-root-alt:before{content:""}.fa-squarespace:before{content:""}.fa-stack-exchange:before{content:""}.fa-stack-overflow:before{content:""}.fa-stackpath:before{content:""}.fa-stamp:before{content:""}.fa-star:before{content:""}.fa-star-and-crescent:before{content:""}.fa-star-half:before{content:""}.fa-star-half-alt:before{content:""}.fa-star-of-david:before{content:""}.fa-star-of-life:before{content:""}.fa-staylinked:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-steam-symbol:before{content:""}.fa-step-backward:before{content:""}.fa-step-forward:before{content:""}.fa-stethoscope:before{content:""}.fa-sticker-mule:before{content:""}.fa-sticky-note:before{content:""}.fa-stop:before{content:""}.fa-stop-circle:before{content:""}.fa-stopwatch:before{content:""}.fa-store:before{content:""}.fa-store-alt:before{content:""}.fa-strava:before{content:""}.fa-stream:before{content:""}.fa-street-view:before{content:""}.fa-strikethrough:before{content:""}.fa-stripe:before{content:""}.fa-stripe-s:before{content:""}.fa-stroopwafel:before{content:""}.fa-studiovinari:before{content:""}.fa-stumbleupon:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-subscript:before{content:""}.fa-subway:before{content:""}.fa-suitcase:before{content:""}.fa-suitcase-rolling:before{content:""}.fa-sun:before{content:""}.fa-superpowers:before{content:""}.fa-superscript:before{content:""}.fa-supple:before{content:""}.fa-surprise:before{content:""}.fa-suse:before{content:""}.fa-swatchbook:before{content:""}.fa-swimmer:before{content:""}.fa-swimming-pool:before{content:""}.fa-symfony:before{content:""}.fa-synagogue:before{content:""}.fa-sync:before{content:""}.fa-sync-alt:before{content:""}.fa-syringe:before{content:""}.fa-table:before{content:""}.fa-table-tennis:before{content:""}.fa-tablet:before{content:""}.fa-tablet-alt:before{content:""}.fa-tablets:before{content:""}.fa-tachometer-alt:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-tape:before{content:""}.fa-tasks:before{content:""}.fa-taxi:before{content:""}.fa-teamspeak:before{content:""}.fa-teeth:before{content:""}.fa-teeth-open:before{content:""}.fa-telegram:before{content:""}.fa-telegram-plane:before{content:""}.fa-temperature-high:before{content:""}.fa-temperature-low:before{content:""}.fa-tencent-weibo:before{content:""}.fa-tenge:before{content:""}.fa-terminal:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-th:before{content:""}.fa-th-large:before{content:""}.fa-th-list:before{content:""}.fa-the-red-yeti:before{content:""}.fa-theater-masks:before{content:""}.fa-themeco:before{content:""}.fa-themeisle:before{content:""}.fa-thermometer:before{content:""}.fa-thermometer-empty:before{content:""}.fa-thermometer-full:before{content:""}.fa-thermometer-half:before{content:""}.fa-thermometer-quarter:before{content:""}.fa-thermometer-three-quarters:before{content:""}.fa-think-peaks:before{content:""}.fa-thumbs-down:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbtack:before{content:""}.fa-ticket-alt:before{content:""}.fa-times:before{content:""}.fa-times-circle:before{content:""}.fa-tint:before{content:""}.fa-tint-slash:before{content:""}.fa-tired:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-toilet:before{content:""}.fa-toilet-paper:before{content:""}.fa-toolbox:before{content:""}.fa-tools:before{content:""}.fa-tooth:before{content:""}.fa-torah:before{content:""}.fa-torii-gate:before{content:""}.fa-tractor:before{content:""}.fa-trade-federation:before{content:""}.fa-trademark:before{content:""}.fa-traffic-light:before{content:""}.fa-train:before{content:""}.fa-tram:before{content:""}.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-trash:before{content:""}.fa-trash-alt:before{content:""}.fa-trash-restore:before{content:""}.fa-trash-restore-alt:before{content:""}.fa-tree:before{content:""}.fa-trello:before{content:""}.fa-tripadvisor:before{content:""}.fa-trophy:before{content:""}.fa-truck:before{content:""}.fa-truck-loading:before{content:""}.fa-truck-monster:before{content:""}.fa-truck-moving:before{content:""}.fa-truck-pickup:before{content:""}.fa-tshirt:before{content:""}.fa-tty:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-tv:before{content:""}.fa-twitch:before{content:""}.fa-twitter:before{content:""}.fa-twitter-square:before{content:""}.fa-typo3:before{content:""}.fa-uber:before{content:""}.fa-ubuntu:before{content:""}.fa-uikit:before{content:""}.fa-umbrella:before{content:""}.fa-umbrella-beach:before{content:""}.fa-underline:before{content:""}.fa-undo:before{content:""}.fa-undo-alt:before{content:""}.fa-uniregistry:before{content:""}.fa-universal-access:before{content:""}.fa-university:before{content:""}.fa-unlink:before{content:""}.fa-unlock:before{content:""}.fa-unlock-alt:before{content:""}.fa-untappd:before{content:""}.fa-upload:before{content:""}.fa-ups:before{content:""}.fa-usb:before{content:""}.fa-user:before{content:""}.fa-user-alt:before{content:""}.fa-user-alt-slash:before{content:""}.fa-user-astronaut:before{content:""}.fa-user-check:before{content:""}.fa-user-circle:before{content:""}.fa-user-clock:before{content:""}.fa-user-cog:before{content:""}.fa-user-edit:before{content:""}.fa-user-friends:before{content:""}.fa-user-graduate:before{content:""}.fa-user-injured:before{content:""}.fa-user-lock:before{content:""}.fa-user-md:before{content:""}.fa-user-minus:before{content:""}.fa-user-ninja:before{content:""}.fa-user-nurse:before{content:""}.fa-user-plus:before{content:""}.fa-user-secret:before{content:""}.fa-user-shield:before{content:""}.fa-user-slash:before{content:""}.fa-user-tag:before{content:""}.fa-user-tie:before{content:""}.fa-user-times:before{content:""}.fa-users:before{content:""}.fa-users-cog:before{content:""}.fa-usps:before{content:""}.fa-ussunnah:before{content:""}.fa-utensil-spoon:before{content:""}.fa-utensils:before{content:""}.fa-vaadin:before{content:""}.fa-vector-square:before{content:""}.fa-venus:before{content:""}.fa-venus-double:before{content:""}.fa-venus-mars:before{content:""}.fa-viacoin:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-vial:before{content:""}.fa-vials:before{content:""}.fa-viber:before{content:""}.fa-video:before{content:""}.fa-video-slash:before{content:""}.fa-vihara:before{content:""}.fa-vimeo:before{content:""}.fa-vimeo-square:before{content:""}.fa-vimeo-v:before{content:""}.fa-vine:before{content:""}.fa-vk:before{content:""}.fa-vnv:before{content:""}.fa-voicemail:before{content:""}.fa-volleyball-ball:before{content:""}.fa-volume-down:before{content:""}.fa-volume-mute:before{content:""}.fa-volume-off:before{content:""}.fa-volume-up:before{content:""}.fa-vote-yea:before{content:""}.fa-vr-cardboard:before{content:""}.fa-vuejs:before{content:""}.fa-walking:before{content:""}.fa-wallet:before{content:""}.fa-warehouse:before{content:""}.fa-water:before{content:""}.fa-wave-square:before{content:""}.fa-waze:before{content:""}.fa-weebly:before{content:""}.fa-weibo:before{content:""}.fa-weight:before{content:""}.fa-weight-hanging:before{content:""}.fa-weixin:before{content:""}.fa-whatsapp:before{content:""}.fa-whatsapp-square:before{content:""}.fa-wheelchair:before{content:""}.fa-whmcs:before{content:""}.fa-wifi:before{content:""}.fa-wikipedia-w:before{content:""}.fa-wind:before{content:""}.fa-window-close:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-windows:before{content:""}.fa-wine-bottle:before{content:""}.fa-wine-glass:before{content:""}.fa-wine-glass-alt:before{content:""}.fa-wix:before{content:""}.fa-wizards-of-the-coast:before{content:""}.fa-wolf-pack-battalion:before{content:""}.fa-won-sign:before{content:""}.fa-wordpress:before{content:""}.fa-wordpress-simple:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpexplorer:before{content:""}.fa-wpforms:before{content:""}.fa-wpressr:before{content:""}.fa-wrench:before{content:""}.fa-x-ray:before{content:""}.fa-xbox:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-y-combinator:before{content:""}.fa-yahoo:before{content:""}.fa-yammer:before{content:""}.fa-yandex:before{content:""}.fa-yandex-international:before{content:""}.fa-yarn:before{content:""}.fa-yelp:before{content:""}.fa-yen-sign:before{content:""}.fa-yin-yang:before{content:""}.fa-yoast:before{content:""}.fa-youtube:before{content:""}.fa-youtube-square:before{content:""}.fa-zhihu:before{content:""}.sr-only{border:0;clip:rect(0, 0, 0, 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}/*! * Font Awesome Free 5.9.0 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) - */@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:auto;src:url("./webfonts/fa-solid-900.eot");src:url("./webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"),url("./webfonts/fa-solid-900.woff2") format("woff2"),url("./webfonts/fa-solid-900.woff") format("woff"),url("./webfonts/fa-solid-900.ttf") format("truetype"),url("./webfonts/fa-solid-900.svg#fontawesome") format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900}html{font-size:87.5%;box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}body,h1,h2,h3,h4,h5,h6,p,figure,blockquote,dl,dd{margin:0;padding:0}ul[role=list],ol[role=list]{list-style:none}html:focus-within{scroll-behavior:smooth}body{text-rendering:optimizeSpeed;line-height:1.5;font-family:"Roboto",Arial,Helvetica,sans-serif !important;letter-spacing:-0.025ch}a:not([class]){text-decoration-skip-ink:auto}img,picture{max-width:100%;display:block}input,button,textarea,select{font:inherit}@media(prefers-reduced-motion: reduce){html:focus-within{scroll-behavior:auto}*,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important;scroll-behavior:auto !important}}#main{height:100vh}.content{display:flex;height:100%;overflow:hidden}.tab-content{display:flex;height:100%;width:100%;background-color:#eef3f6;animation:fadein .3s;overflow:auto}input[type=text],input[type=password],input[type=number],textarea{box-sizing:border-box;background:#fff;max-width:100%;font-size:1rem;font-weight:400;border:1px solid #ccc;border-radius:.25rem;padding:.25rem .5rem;outline:rgba(0,0,0,0)}input:focus{border:1px solid #3ba4d7;box-shadow:inset 0 0 5px #ccc}input.stretched{width:90%}input.small{max-width:70%;padding:.1rem}input.searchbar{width:40%}a{cursor:pointer}a[title=Back]{width:max-content;height:max-content;padding:.475rem .75rem;border-radius:50%;transition:100ms}a[title=Back]:hover{background:#eef3f6}table{padding:20px;table-layout:fixed;width:100%;border-collapse:collapse;text-align:center;color:#333;font-size:1.125rem}table th{font-size:1.125rem;color:#000;border-bottom:2px solid #eee}table tr{border-bottom:1px solid #eee}h3{color:#444}hr{margin-left:0;color:#aaa}.grid-2col{display:grid;grid-template-columns:auto auto;gap:1rem;justify-content:start}.grid-2col input[type=checkbox]{margin-top:20px}.error{color:red}.tooltip{color:#333;position:relative;display:inline-block;margin:0 .25rem}.tooltiptext{visibility:hidden;position:absolute;top:100%;left:50%;min-width:250px;margin-left:-120px;z-index:1;color:#ccc;background-color:#333;font-size:.875rem;text-align:center;padding:.25rem;border-radius:.5rem}.tooltip:hover .tooltiptext{visibility:visible;animation:fadein .5s}blockquote{color:#14141b;padding:.75rem 1rem .75rem 2rem;border-radius:.25rem}blockquote.info{position:relative;line-height:1.2;color:rgba(20,20,27,.8);border:1px solid rgba(17,143,204,.8)}blockquote.info::before{font-family:"Font Awesome 5 Free";position:absolute;top:.5rem;left:.5rem;content:"";color:#019dff}@keyframes fadein{from{opacity:0}to{opacity:1}}.fadein{animation:fadein .5s}@keyframes swipe-from-left{from{margin-left:100%}to{margin-left:0}}button{width:max-content;height:max-content;color:#fff;background:#019dff;font-size:1rem;padding:.4rem 1rem;border:0;border-radius:5px;cursor:pointer;box-shadow:inset -3px -3px 0 rgb(0,94.5826771654,154)}button:active{outline:none;box-shadow:inset 3px 3px 0 rgb(0,94.5826771654,154)}button.red{width:max-content;height:max-content;color:#fff;background:#ff3a4a;font-size:1rem;padding:.4rem 1rem;border:0;border-radius:5px;cursor:pointer;box-shadow:inset -3px -3px 0 rgb(211,0,17.1370558376)}button.red:active{outline:none;box-shadow:inset 3px 3px 0 rgb(211,0,17.1370558376)}.media-item{display:flex;margin-top:.5rem;padding:1rem;border:1px solid rgba(20,20,27,.1);border-radius:4px}.media-item__details{flex-basis:40%;display:flex;align-items:start;gap:.5rem}.media-item__details img{width:6rem;object-fit:contain}.media-item__desc{flex-basis:60%}.active-link{background:hsla(0,0%,100%,.1) !important}.nav-menu{background-color:#14141b;box-shadow:0 5px 5px #222;display:flex;flex-direction:column;align-items:center;height:100%;padding:.5rem .25rem;margin-right:0rem}.nav-menu__logo{padding:1.2rem 0;display:flex;align-items:center;gap:.3rem}.nav-menu__logo img{width:1.6rem}.nav-menu__logo h5{line-height:1;color:#fff}.nav-menu__box{padding:2rem .125rem;display:flex;flex-direction:column;gap:.5rem;position:relative}.nav-menu__box .item{margin:0;padding:.675rem .5rem;width:10rem;display:flex;align-items:center;line-height:1;border-radius:.5rem;text-decoration:none;color:#ccc;text-transform:capitalize;transition:0ms}.nav-menu__box .item:hover{background-color:rgba(238,243,246,.15)}.nav-menu__box .item i.sidenav-icon{width:2.5rem;height:1.4rem;display:grid;place-items:center}.nav-menu__box .item.item-selected{color:#9bdaff;background-color:rgba(155,218,255,.15);font-weight:medium}.nav-menu__box button.toggle-nav{display:none;position:absolute;padding:0;top:0;right:-1rem;background:rgb(77.5,186.5157480315,255);width:1.5rem;height:1.5rem;aspect-ratio:1;justify-content:center;align-items:center;border-radius:50%;box-shadow:none}.nav-menu.collapsed .nav-menu__logo .logo-container{display:flex;flex-direction:column;align-items:center;gap:.5rem}.nav-menu.collapsed .nav-menu__logo .logo-container>*:not(img){display:block}.nav-menu.collapsed .nav-menu__logo .nav-menu__logo-text{display:none !important}.nav-menu.collapsed .nav-menu__box .item{padding:.675rem 0;width:2.5rem;justify-content:center;transition:300ms}.nav-menu.collapsed .nav-menu__box .item span,.nav-menu.collapsed .nav-menu__box .item p{display:none !important}.nav-menu.collapsed button i{rotate:180deg}.nav-menu:hover button.toggle-nav{display:flex}.sidebar{width:13rem;background-color:#fff;display:flex;flex-direction:column}.sidebar a{text-decoration:none;text-transform:capitalize;padding:1rem;cursor:pointer;color:#999}.sidebar a:hover{color:#222}.sidebar .selected-sidebar-link{font-weight:bold;color:#222;border-left:5px solid #3ba4d7;animation:expand-left-border .1s}.sidebarquickview>h6{padding:.5rem}.sidebarquickview a{text-decoration:none;text-transform:capitalize;padding:.5rem 1rem;display:block;color:#999}.sidebarquickview a a:hover{color:#222}.sidebarquickview .selected-sidebarquickview-link{font-weight:bold;color:#222;border-left:5px solid #3ba4d7;animation:expand-left-border .1s}.node-panel{width:100%;padding:.5rem;animation:fadein .5s}@keyframes expand-left-border{from{border-left:0}to{border-left:5px solid #3ba4d7}}@media(max-width: 700px){.tab-content{flex-direction:column}.sidebar{width:100% !important;flex-direction:row !important;overflow-x:auto !important;overflow-y:hidden !important;white-space:nowrap !important;border-bottom:1px solid rgba(20,20,27,.1) !important;background:#fff !important;z-index:50 !important;flex-shrink:0 !important;height:auto !important;padding:0 !important}.sidebar a{display:inline-block !important;padding:.8rem 1.2rem !important;border-bottom:3px solid rgba(0,0,0,0) !important;border-left:none !important}.sidebar .selected-sidebar-link{border-left:none !important;border-bottom:3px solid #3ba4d7 !important;animation:none !important}.sidebarquickview>h4,.sidebarquickview>h6{display:none !important}}.posts{height:100%;margin-top:1rem;flex-direction:column;overflow:auto}.posts__heading{display:flex;flex-direction:column;justify-content:space-between}.posts-container{height:100%;padding:1rem;display:grid;grid-template-columns:repeat(auto-fill, minmax(150px, 1fr));gap:2rem;border:1px solid rgba(20,20,27,.1);border-radius:4px;overflow:auto}.posts-container-card{min-height:240px;flex-direction:column;border:1px solid rgba(20,20,27,.5);border-radius:4px;cursor:pointer;text-align:center}.posts-container-card img{flex-basis:90%;object-fit:cover}.posts-container-card p{padding:0 .125rem;flex-basis:10%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.progress-bar{width:100%;height:2rem;position:relative;text-align:center;background-color:#eef3f6;border-radius:20px;overflow:hidden}.progress-bar__status{position:absolute;top:0;left:0;height:100%;color:#14141b;background-color:#019dff}.progress-bar__percent{position:absolute;inset:0;margin:auto;width:fit-content;height:fit-content}.progress-bar-chunks{position:relative;margin-top:.5rem;width:100%;height:2rem;display:flex;border-radius:.25rem;overflow:hidden;background-color:#eef3f6}.progress-bar-chunks .chunk{width:100%}.progress-bar-chunks .chunk[data-chunkVal="0"]{background-color:rgba(155,218,255,.2)}.progress-bar-chunks .chunk[data-chunkVal="1"]{background-color:#ff3a4a}.progress-bar-chunks .chunk[data-chunkVal="2"]{background-color:#019dff}.progress-bar-chunks .chunk[data-chunkVal="3"]{background-color:#fcba03}.progress-bar-chunks__percent{position:absolute;inset:0;margin:auto;width:fit-content;height:fit-content}.widget{height:100%;padding:1rem;display:flex;flex-direction:column;gap:.5rem;background-color:#fff;border-radius:.5rem;overflow:auto}.widget .top-heading{display:flex;justify-content:space-between}.widget__heading{display:flex;justify-content:space-between;align-items:center;border-bottom:2px solid #999}.widget__body{height:100%;display:flex;flex-direction:column;overflow:auto}.widget__body-heading{display:flex;justify-content:space-between;align-items:center}.widget__body-heading .action{display:flex;gap:.5rem}.widget__body-content{height:100%;overflow:auto}.widget__body-box{display:flex;flex-direction:column;gap:.5rem}.widget-half{max-width:50%}#modal-container{display:none;position:fixed;z-index:1;height:100%;top:0;left:0;width:100%;background-color:rgba(0,0,0,.2)}.modal-content{position:absolute;color:#555;width:40%;min-height:10rem;height:max-content;padding:1.5rem;inset:0;margin:auto;background-color:#fff;border-radius:.5rem;animation:fadein .5s;display:flex;flex-direction:column}.modal-content button:last-child{margin-top:auto}.modal-content .close-btn{position:absolute;right:1.5rem}.modal-content .widget{padding:0}#notification-container{position:absolute;bottom:0;right:0}.login-page{background-image:linear-gradient(-45deg, rgba(1, 157, 255, 0.75), rgba(17, 143, 204, 0.75));height:100%;animation:fadein .5s}.login-page .login-container{background-color:#fff;box-shadow:3px 3px 5px rgba(20,20,27,.4);margin:auto;position:relative;top:100px;max-width:400px;max-height:500px;border-radius:5px;display:flex;flex-direction:column;align-items:center}.login-page .login-container input{padding:.375rem .75rem;border-radius:.275rem}.login-page .login-container *{margin-bottom:1rem}.login-page .login-container>img{margin:1rem 0 2rem}.login-page .login-container extra{margin:0}.login-page .login-container>a{text-decoration:underline;cursor:pointer}.login-page .extra>label,.login-page .extra>br,.login-page .extra>input{margin-bottom:0}.homepage{margin:2rem auto 0;display:flex;flex-direction:column;gap:4rem}.homepage .logo{display:flex;justify-content:center;align-items:center}.homepage .logo img{width:90px}.homepage .logo .retroshareText{display:flex;flex-direction:column;align-items:center}.homepage .logo .retroshareText .retrotext{font-size:36px;font-weight:600;line-height:1.125}.homepage .logo .retroshareText .retrotext>span{color:#118fcc}.homepage .logo .retroshareText>b{font-size:14px;line-height:1}.homepage .certificate{display:flex;flex-direction:column;gap:4rem}.homepage .certificate__heading{text-align:center}.homepage .certificate__heading>h1{margin-bottom:1rem}.homepage .certificate__content{display:flex;flex-direction:column;gap:2rem;padding:2rem;text-align:center;border:1.5px solid rgba(17,143,204,.2);border-radius:6px;box-shadow:0px 0px 8px 2px rgba(20,20,27,.05)}.homepage .certificate__content .rsId>p{margin-bottom:.5rem;color:#118fcc}.homepage .certificate__content .retroshareID{padding:.25rem;display:flex;align-items:center;justify-self:start;font-size:1.25rem;border-radius:4px;background:rgba(20,20,27,.05)}.homepage .certificate__content .retroshareID .textArea{padding:0;width:100%;min-height:75px;font-size:1rem;font-family:monospace;background:rgba(0,0,0,0);border:none;resize:none}.homepage .certificate__content .retroshareID i{color:#118fcc}.homepage .certificate__content .retroshareID>i{margin:0 .5rem;cursor:pointer}.homepage .certificate__content .webhelp{padding:.5rem;background:#f5f5f5;display:flex;justify-content:center;align-items:center;gap:.5rem;border-radius:4px;border:1px solid rgba(20,20,27,.5);width:fit-content;cursor:pointer}.homepage .certificate__content .webhelp-container{display:grid;place-items:center}.homepage .certificate__content .webhelp:hover{background:#eef3f6;border:1px solid #14141b}.homepage .certificate__content .webhelp>i{font-size:1.2rem;color:green}.homepage .certificate__content .add-friend>h6,.homepage .certificate__content .webhelp-container>h6{font-weight:normal;margin-bottom:.5rem}.friend{color:#444;font-size:1.2em;margin:1rem .5rem;padding:1.5rem;border:1px solid #aaa;border-radius:20px}.friend i{float:left;padding:0 10px;cursor:pointer}.friend h4{margin-bottom:5px}.friend button{font-size:.9em}.friend.hidden{display:none}.friend .brief-info.online{color:green}.friend .location{margin:5px;border-top:1px solid #bbb;display:grid;grid-template-columns:auto auto;justify-content:start}.friend .brief-info{display:flex;align-items:center;justify-self:start}.friend .fa-times-circle{color:#555}.friend .fa-check-circle{color:green}.identity{color:#444;font-size:1.1em;margin:20px;padding:10px;border:1px solid #aaa;border-radius:20px}.identity>h4{margin:5px;font-size:1.3em}.identity button{font-size:.9em}.identity .details{display:grid;grid-template-columns:140px auto;grid-row-gap:5px;justify-content:left}.defaultAvatar{width:3rem;height:3rem;aspect-ratio:1;background:#b0c4de;border-radius:50%;display:grid;place-items:center}.defaultAvatar p{font-weight:900;color:#666f7f;transform:translateY(1px)}img.avatar{display:block;width:3rem;height:max-content;aspect-ratio:1;margin-right:.3em;border-radius:50%}.counter{margin-left:.5em}.counter:before{content:"("}.counter:after{content:")"}.chatInit{margin-left:.5em;color:green;cursor:pointer}.lobby{margin:10px;border:1px solid #aaa;border-radius:20px}.lobby .mainname{margin:20px;font-weight:100;font-size:1.2em}.topic{color:#666}.lobby>.topic{font-size:.95em;margin-left:25px;margin-bottom:5px}.lefttitle{margin-top:15px;margin-bottom:0;font-weight:100;font-size:1.2em}.leftname{margin-top:5px;margin-bottom:5px;padding:5px;font-weight:100;font-size:1em}.leftlobby>.topic{font-size:.75em;margin-left:15px;margin-bottom:5px}.subscribed,.public{cursor:pointer}.leftlobby{border:1px solid #aaa;border-radius:10px;margin-top:5px;background-color:#fff}.leftlobby.selected-lobby,.selectedidentity{color:#fff;background-color:#3ba4d7}.rightbar{position:absolute;width:185px;background-color:#fff;overflow:auto;top:130px;bottom:15px;right:15px}.user{padding:5px}.lobbyName{padding:15px;margin-top:2rem}.lobbies{position:absolute;width:185px;left:165px;bottom:15px;top:130px;overflow:auto}.messages,.setup{position:absolute;background-color:#fff;top:130px;left:360px;right:215px;overflow:auto}.messages{bottom:115px}.messagetext{white-space:break-spaces;margin-right:5px}.message>*{margin-left:5px}.username{color:#006400;font-weight:bolder}.chatMessage{position:absolute;background-color:#fff;height:85px;bottom:15px;right:215px;left:360px}textarea.chatMsg{height:100%;width:100%}.chatatchar{margin-left:.2em;margin-right:.2em;color:silver}.setupicon{margin-left:1em;cursor:pointer}.leaveicon{margin-left:1em;cursor:pointer;color:#d40000}.selectidentity{margin:15px;font-size:1.2em}.setup>.identity{cursor:pointer}.setup{bottom:15px}.createDistantChat{margin-top:1em}.no-lobbies .messages,.no-lobbies .chatMessage,.no-lobbies .setup{left:165px}@media(min-width: 900px){.node-panel.chat-room{display:grid !important;grid-template-columns:250px 1fr 200px !important;grid-template-rows:auto 1fr auto !important;grid-template-areas:"lobbies header rightbar" "lobbies messages rightbar" "lobbies input rightbar" !important;padding:0 !important;height:100% !important}.node-panel.chat-room .lobbyName{grid-area:header;padding:10px;border-bottom:1px solid #eee;margin:0;z-index:10;background:#fff}.node-panel.chat-room .lobbies{grid-area:lobbies;position:static !important;width:auto !important;height:auto !important;border-right:1px solid #ccc;overflow-y:auto;display:block !important;top:auto !important;bottom:auto !important;left:auto !important}.node-panel.chat-room .messages{grid-area:messages;position:static !important;width:auto !important;height:auto !important;overflow-y:auto;padding:10px;left:auto !important;right:auto !important;top:auto !important;bottom:auto !important;margin:0 !important}.node-panel.chat-room .rightbar{grid-area:rightbar;position:static !important;width:auto !important;border-left:1px solid #ccc;overflow-y:auto;display:block !important}.node-panel.chat-room .chatMessage{grid-area:input;position:static !important;width:auto !important;height:auto !important;border-top:1px solid #eee;left:auto !important;right:auto !important;bottom:auto !important;flex:0 0 auto;padding:10px !important;background:#fff;z-index:10}}@media(max-width: 899px){.node-panel.chat-room{display:flex !important;flex-direction:column !important;height:100% !important;position:relative !important}.node-panel.chat-room .lobbyName{flex:0 0 auto}.node-panel.chat-room .messages{flex:1 !important;overflow-y:auto !important;position:relative !important;top:0 !important;bottom:0 !important;left:0 !important;right:0 !important;width:100% !important;height:auto !important;margin:0 !important}.node-panel.chat-room .chatMessage{flex:0 0 auto !important;position:relative !important;bottom:0 !important;left:0 !important;right:0 !important;width:100% !important;height:auto !important;z-index:100}.node-panel.chat-room .rightbar,.node-panel.chat-room .lobbies{display:none !important;position:fixed !important;top:60px !important;bottom:0 !important;width:80% !important;background:#fff !important;z-index:200 !important;box-shadow:2px 0 10px rgba(0,0,0,.2) !important}.node-panel.chat-room.show-lobbies .lobbies{display:block !important;left:0 !important}.node-panel.chat-room.show-users .rightbar{display:block !important;right:0 !important}.chat-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.4);z-index:150}.show-lobbies .chat-overlay,.show-users .chat-overlay{display:block}.mobile-menu-icons{display:flex;gap:15px;font-size:1.2rem}.mobile-menu-icons i{cursor:pointer;padding:5px}}@media(min-width: 900px){.mobile-menu-icons{display:none}}.side-bar{display:flex;flex-direction:column;background:#fff}.side-bar .mail-compose-btn{width:96%;margin:.25rem;padding:.75rem 0}.compose-mail__from{display:flex;justify-content:space-between;padding-bottom:.5rem;border-bottom:2px solid #eef3f6}.compose-mail__recipients{padding:.5rem 0;display:flex;flex-direction:column;gap:.5rem;border-bottom:2px solid #eef3f6}.compose-mail__recipients__container{display:flex;gap:.5rem}.compose-mail__recipients__container>label{text-transform:capitalize}.compose-mail__recipients__container .recipients{width:100%;display:flex;gap:.5rem;flex-wrap:wrap}.compose-mail__recipients__container .recipients__selected{padding:.125rem .5rem;display:flex;align-items:center;gap:.5rem;border:1px solid #eef3f6;border-radius:3px;cursor:default}.compose-mail__recipients__container .recipients__selected i{cursor:pointer;padding:.25rem}.compose-mail__recipients__container .recipients__input{display:flex;position:relative;flex-grow:1}.compose-mail__recipients__container .recipients__input-field{flex-grow:1;min-width:200px;padding:0;border:none;box-shadow:none}.compose-mail__recipients__container .recipients__input-field:focus+.recipients__input-list{display:flex}.compose-mail__recipients__container .recipients__input-list{z-index:1;position:absolute;top:1rem;padding:0;width:100%;max-height:15rem;flex-direction:column;overflow:auto;display:none;background:#fff;border-top:1px solid #eef3f6;border-bottom:1px solid #eef3f6}.compose-mail__recipients__container .recipients__input-list:hover{display:flex}.compose-mail__recipients__container .recipients__input-list li{list-style:none;padding:.25rem .5rem;cursor:pointer;background:#fff;border:1px solid #eef3f6;border-top:0px}.compose-mail__recipients__container .recipients__input-list li:hover{background:#eef3f6}.compose-mail__recipients__container .recipients__input-list li:last-child{border-bottom:0px}.compose-mail__recipients .remove-recipient{padding:.125rem .5rem}.compose-mail input[type=text].compose-mail__subject{padding:.5rem 0;border:none;box-shadow:none;border-bottom:2px solid #eef3f6;border-radius:0}.compose-mail__message{margin:.5rem 0;height:100%;display:flex;flex-direction:column;overflow:auto}.compose-mail__message-body{height:100%;outline:rgba(0,0,0,0)}.compose-mail__send-btn{display:flex;align-items:center;gap:.5rem}.compose-mail__send-btn i{transform:translateY(-1px)}.msg-view{height:100%;display:flex;flex-direction:column;gap:1rem;overflow:auto}.msg-view-nav{display:flex;justify-content:space-between;align-items:column}.msg-view-nav__action{display:flex;gap:.5rem}.msg-view__header{display:flex;flex-direction:column;gap:1rem}.msg-view__header>h3{line-height:1}.msg-view__header .msg-details{display:flex;gap:1rem}.msg-view__header .msg-details__avatar{height:max-content}.msg-view__header .msg-details__info{display:flex;flex-direction:column}.msg-view__header .msg-details__info-item{display:flex;gap:.5rem}.msg-view__body{height:100%;overflow:auto;font-size:14px !important}.msg-view__attachment{height:50%;overflow:auto;display:flex;flex-direction:column}.msg-view__attachment-items{height:100%;overflow:auto}.mail-tag{width:8rem;padding:.5rem}.msgHeader{display:flex}.msgHeaderDetails{display:flex;flex-direction:column}table.mails th:nth-child(1){width:5%;color:#fcba03}table.mails th:nth-child(2){width:5%;color:hsl(202.5,30.7692307692%,44.9019607843%)}table.mails th:nth-child(3){width:50%;text-align:start}table.mails th:nth-child(4),table.mails th:nth-child(5){width:20%;text-align:start}table.mails td:nth-child(3){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.mails td:nth-child(4),table.mails td:nth-child(5){text-align:start}table.mails tr:hover{background-color:#eef3f6;cursor:pointer}table.mails tr.unread{color:#000;background-color:#eef3f6}table.mails>tr:hover{cursor:auto;background-color:#fff}input.star-check{display:none}input.star-check+label.star-check{color:gray}input.star-check:checked+label.star-check{color:#fcba03}#truncate{height:6rem;overflow:auto}#truncate.truncated-view{height:1.75rem;overflow:hidden}.toggle-truncate{font-size:.75rem;padding:0 .25rem;background:#999;color:#14141b;box-shadow:none;border-radius:2px}table.attachment-container{padding:0}table.attachment-container>tr{border:0}table.attachment-container .attachment-header{width:100%;display:flex;justify-content:space-between}table.attachment-container .attachment-header th{text-align:start}table.attachment-container .attachment-header th:nth-child(1){flex-basis:45%}table.attachment-container .attachment-header th:nth-child(2){flex-basis:15%}table.attachment-container .attachment-header th:nth-child(3){flex-basis:10%}table.attachment-container .attachment-header th:nth-child(4){flex-basis:20%}table.attachment-container .attachment-header th:nth-child(5){text-align:center;flex-basis:10%}table.attachment-container .attachment{width:100%;display:flex;justify-content:space-between;text-align:start}table.attachment-container .attachment__name{flex-basis:45%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}table.attachment-container .attachment__name span{margin-left:8px}table.attachment-container .attachment__from{flex-basis:15%}table.attachment-container .attachment__size{flex-basis:10%}table.attachment-container .attachment__date{flex-basis:20%}table.attachment-container .attachment td:nth-child(5){display:flex;justify-content:center;align-items:center;flex-basis:10%}table.attachment-container .attachment td:nth-child(5) button{font-size:.875rem}.view-toggle{height:max-content;border:1px solid #019dff;border-radius:4px;display:flex}.view-toggle *{padding:4px 12px;border-radius:4px}.composePopupOverlay{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1;background-color:rgba(0,0,0,.2)}.composePopupOverlay .composePopup{position:absolute;inset:0;margin:auto;width:80%;height:90%}.composePopupOverlay .composePopup>.widget{padding:2rem}.composePopupOverlay .composePopup .close-btn{position:absolute;top:1.5rem;right:1.5rem}.file-view{width:100%;padding:1rem;margin-top:1.5rem;border-radius:8px;border:1px solid #ccc;animation:fadein .5s}.file-view__heading{display:flex;justify-content:space-between;margin-bottom:.5rem}.file-view__heading-chunk{display:flex;gap:1rem}.file-view__body{display:flex;flex-direction:column;gap:1rem}.file-view__body-details{display:flex;align-items:center}.file-view__body-details-stat{width:100%;display:grid;grid-template-columns:repeat(5, 1fr)}.file-view__body-details-stat span>i{margin-right:.5rem}.file-view__body-details-action{display:flex;gap:1rem;height:100%}.file-view__body-details-action button,.file-view__body-details-action button.red{padding:.25rem .75rem}table.myfiles td{word-wrap:break-word}table.myfiles th:nth-child(1){width:2%}table.myfiles th:nth-child(2){width:50%}table.myfiles td:nth-child(2){text-align:start}table.friendsfiles td{word-wrap:break-word}table.friendsfiles th:nth-child(1){width:2%}table.friendsfiles th:nth-child(2){width:50%}table.friendsfiles th:nth-child(4){width:40%}table.friendsfiles td:nth-child(2){text-align:start}.file-search-container{margin-top:1rem;padding:8px;display:flex;gap:8px;border:1px solid rgba(20,20,27,.2);border-radius:6px;height:100%;overflow:auto}.file-search-container__keywords{flex-basis:15%;padding-right:.25rem;border-right:1px solid rgba(20,20,27,.1)}.file-search-container__keywords .keywords-container{display:flex;flex-direction:column;border-top:2.5px solid rgba(20,20,27,.08);margin-top:.125rem;padding-top:.25rem}.file-search-container__keywords .keywords-container a{font-size:1.2rem;text-decoration:none;color:#14141b}.file-search-container__keywords .keywords-container a.selected{color:#019dff}.file-search-container__results{flex-basis:85%;height:100%;overflow:auto}.file-search-container__results .results-container .results-header tr{display:flex}.file-search-container__results .results-container .results-header tr th{font-size:1.25rem;font-weight:bold;text-align:left}.file-search-container__results .results-container .results-header tr th:nth-child(1){flex-basis:40%}.file-search-container__results .results-container .results-header tr th:nth-child(2){flex-basis:10%;text-align:center}.file-search-container__results .results-container .results-header tr th:nth-child(3){flex-basis:40%}.file-search-container__results .results-container .results-header tr th:nth-child(4){flex-basis:10%}.file-search-container__results .results-container .results{height:100%;overflow:auto}.file-search-container__results .results-container .results tr{display:flex}.file-search-container__results .results-container .results tr .results__hash,.file-search-container__results .results-container .results tr .results__name{text-align:left;flex-basis:40%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.file-search-container__results .results-container .results tr .results__hash span,.file-search-container__results .results-container .results tr .results__name span{margin-left:8px}.file-search-container__results .results-container .results tr .results__size{flex-basis:10%}.file-search-container__results .results-container .results tr .results__download{flex-basis:10%;display:flex;justify-content:start;align-items:center}.search-form{display:flex;width:40%}.search-form input{width:100%}.search-form button{margin-left:.5rem}.shareManagerPopupOverlay{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1;background-color:rgba(0,0,0,.2)}.shareManagerPopupOverlay .shareManagerPopup{position:absolute;inset:0;margin:auto;width:80%;height:90%}.shareManagerPopupOverlay .shareManagerPopup>.widget{padding:1.5rem}.shareManagerPopupOverlay .shareManagerPopup .close-btn{position:absolute;top:1.5rem;right:1.5rem}.share-manager{display:flex;flex-direction:column;justify-content:space-between}.share-manager__table{margin:1rem 0 auto}.share-manager__table thead{font-weight:bold;text-align:left}.share-manager__table thead td:nth-child(1),.share-manager__table thead td:nth-child(2){padding-left:.5rem}.share-manager__table thead td:nth-child(3) .tooltip,.share-manager__table thead td:nth-child(4) .tooltip{font-weight:normal;font-size:1rem}.share-manager__table tbody{text-align:left}.share-manager__table tbody td:nth-child(4){font-size:1rem}.share-manager__table td input{border:0 !important}.share-manager__table td input[type=text]{width:100%}.share-manager__table td:nth-child(1){width:45%}.share-manager__table td:nth-child(2){width:20%}.share-manager__table td:nth-child(3){width:10%}.share-manager__table td:nth-child(4){width:25%}.share-manager__actions{display:flex;justify-content:space-between}.share-manager__form{display:flex;flex-direction:column;gap:.5rem}.share-manager__form_input{display:flex;flex-direction:column;gap:.5rem}.share-manager__form_input input{flex-grow:1}.share-manager .share-flags input.share-flags-check{display:none}.share-manager .share-flags input.share-flags-check+label.share-flags-label{color:gray;margin-right:.25rem;padding:.25rem .25rem .125rem;border:1px solid #6d6d6d;border-radius:.5rem}.share-manager .share-flags input.share-flags-check:checked+label.share-flags-label{color:#118fcc}.share-manager label span{display:inline-block;width:1.125rem}.manage-visibility label{width:100%;cursor:pointer}.manage-visibility{display:flex;justify-content:space-between}@media(max-width: 700px){.file-view__body-details{flex-direction:column;align-items:flex-start;gap:1rem}.file-view__body-details-stat{grid-template-columns:1fr;gap:.5rem}.file-view__body-details-stat span{display:flex;align-items:center}.share-manager__table,.share-manager__table thead,.share-manager__table tbody,.share-manager__table tr,.share-manager__table td{display:block;width:100% !important}.share-manager__table thead{display:none}.share-manager__table tr{border:1px solid #ccc;border-radius:8px;margin-bottom:1rem;padding:.5rem;background:#fff}.share-manager__table td{margin-bottom:.5rem;border:none !important;padding-left:0 !important}table.myfiles,table.myfiles tr,table.myfiles td,table.friendsfiles,table.friendsfiles tr,table.friendsfiles td{display:block;width:100% !important}table.myfiles th,table.friendsfiles th{display:none}table.myfiles tr,table.friendsfiles tr{border:1px solid #ccc;border-radius:8px;margin-bottom:1rem;padding:.5rem;background:#fff}.file-search-container{flex-direction:column}.file-search-container__keywords{flex-basis:auto;width:100%;border-right:none;border-bottom:1px solid rgba(20,20,27,.1);padding-bottom:1rem;margin-bottom:1rem}.results-container,.results-container thead,.results-container tbody,.results-container tr,.results-container td{display:block;width:100% !important}.results-container thead{display:none}.results-container tr{border-bottom:1px solid #eee;padding:1rem 0}.results-container td{margin-bottom:.5rem;word-break:break-all}}.file-section{margin-top:2rem;display:flex;flex-direction:column}.comments-section{margin-top:2rem;display:flex;justify-content:space-between}.comments-section__menu{display:flex;gap:1rem}.comments-section__menu-id{display:flex;align-items:center;gap:.25rem}#toggleunsub{position:relative;background:gray}table.channels th:nth-child(1){width:50%;text-align:start}table.channels td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.channels tr:hover{background-color:#eef3f6;cursor:pointer}table.channels tr.hidden{display:none}table{padding:.5rem}table.comments{border:1px solid #eee}table.comments th{height:40px}table.comments th:nth-child(1){width:2%}table.comments th:nth-child(2){width:40%}table.comments td{word-wrap:break-word}table.comments td:nth-child(2){text-align:start}table.files th:first-child{text-align:start;width:60%}table.files tr td:first-child{text-align:start}table.files td{word-wrap:break-word}#mtags{width:160px;text-align:center;font-size:medium;margin-left:10px;height:40px}.forums-node-panel{position:relative;bottom:200px;margin-left:200px;animation:fadein .5s}table.forums th:nth-child(1){width:50%;text-align:start}table.forums td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.forums tr:hover{background-color:#eef3f6;cursor:pointer}table.forums tr.hidden{display:none}#searchforum{position:relative;margin-left:250px}#forumdetails{position:relative;padding:10px}.p{margin:0}#toggleunsub{position:relative;background:gray}table.threads tr:hover{background-color:#eef3f6;cursor:pointer}table.threads td{word-wrap:break-word}table.threadreply th:nth-child(2){width:50%}table.threadreply th:nth-child(1){width:2%}table.threadreply td:nth-child(2){width:50%;text-align:start}table.threadreply td{word-wrap:break-word}table.threadreply tr:hover{background-color:#eef3f6;cursor:pointer}table.boards th:nth-child(1){width:50%;text-align:start}table.boards td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.boards tr:hover{background-color:#eef3f6;cursor:pointer}table.boards tr.hidden{display:none}#toggleunsub{position:relative;background:gray}#options{width:100px;text-align:center;font-size:medium;margin-left:20px;height:40px}#composepopup{height:80%;width:70%}#mtags{width:160px;text-align:center;font-size:medium;margin-left:10px;height:40px}.mail .permission-flag{margin-bottom:1rem;display:flex;gap:1rem}.mail-tags{padding:.5rem;border:1px solid rgba(20,20,27,.2);border-radius:6px}.mail-tags__container{display:flex;flex-direction:column}.mail-tags__container .tag-item{display:flex;align-items:center;gap:4px;border-bottom:1px solid rgba(20,20,27,.1);padding:2px 0}.mail-tags__container .tag-item:last-child{border:none}.mail-tags__container .tag-item__color{width:1.25rem;height:1.25rem;aspect-ratio:1}.mail-tags__container .tag-item__name{font-size:1.125rem}.mail-tags__container .tag-item__modify{margin-left:auto;font-size:.75rem;display:flex;gap:4px}.mail-tags__container .tag-item:hover{background-color:#eef3f6}.mail-tags__container .tag-item button,.mail-tags__container .tag-item button.red{padding:.25rem .6rem}.mail-tags-form .input-field{margin-bottom:.5rem}.mail-tags-form .input-field label{margin-right:.5rem}.external-address{margin:0;padding-left:1rem;height:100px;overflow:hidden auto}.external-address::-webkit-scrollbar{display:none}.proxy-server{display:flex;flex-direction:column;gap:4px}.proxy-server__tor>h4,.proxy-server__i2p>h4{margin-bottom:.25rem}.proxy-server__tor>input,.proxy-server__i2p>input{margin-right:.5rem}.proxy-server__tor .proxy-outgoing,.proxy-server__i2p .proxy-outgoing{display:inline-flex;align-items:center;gap:.5rem}.proxy-server__tor .proxy-outgoing__status,.proxy-server__i2p .proxy-outgoing__status{width:1rem;height:1rem;aspect-ratio:1;border:1px solid #000;border-radius:50%}.config-files{display:flex;flex-direction:column;gap:1rem} - -/* Custom improvements for Network Page */ - -.network-container { - display: flex; - height: 100%; - width: 100%; - overflow: hidden; - background-color: #f1f5f9; -} - -.network-left-pane { - width: 320px; - min-width: 300px; - max-width: 350px; - border-right: 1px solid #cbd5e1; - display: flex; - flex-direction: column; - background: #ffffff; - box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05); -} - -.own-profile-card { - padding: 1.25rem; - border-bottom: 1px solid #e2e8f0; - background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.own-profile-card .profile-header { - display: flex; - align-items: center; - gap: 1rem; -} - -.own-profile-card .profile-info { - display: flex; - flex-direction: column; - flex: 1; - overflow: hidden; -} - -.own-profile-card .profile-info .profile-name { - font-weight: 700; - color: #1e293b; - font-size: 1.1rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.own-profile-card .profile-info .profile-status { - font-size: 0.85rem; - color: #10b981; - font-weight: 500; - display: flex; - align-items: center; - gap: 0.35rem; -} - -.own-profile-card .profile-info .profile-status::before { - content: ''; - display: inline-block; - width: 8px; - height: 8px; - background-color: #10b981; - border-radius: 50%; -} - -.own-profile-card .own-identity-select-container { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.own-profile-card .own-identity-select-container label { - font-size: 0.75rem; - color: #64748b; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.own-profile-card .own-identity-select-container select.own-identity-select { - width: 100%; - padding: 0.375rem 0.5rem; - font-size: 0.85rem; - border: 1px solid #cbd5e1; - border-radius: 0.375rem; - background-color: #ffffff; - color: #334155; - outline: none; - cursor: pointer; - transition: border-color 0.2s; -} - -.own-profile-card .own-identity-select-container select.own-identity-select:focus { - border-color: #3ba4d7; -} - -.friends-list-container { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - position: relative; -} - -.friends-list-container .people-context-menu { - position: absolute; - left: 2rem; - width: 210px; - background-color: #ffffff; - border: 1px solid #e2e8f0; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); - border-radius: 0.375rem; - z-index: 1010; - padding: 0.25rem 0; - display: flex; - flex-direction: column; -} - -.friends-list-container .people-context-menu .menu-item { - padding: 0.5rem 1rem; - font-size: 0.85rem; - color: #334155; - cursor: pointer; - display: flex; - align-items: center; - transition: background-color 0.2s; -} - -.friends-list-container .people-context-menu .menu-item:hover { - background-color: #f1f5f9; - color: #0f172a; -} - -.friends-list-container .searchbar-container { - padding: 0.75rem 1rem; - border-bottom: 1px solid #e2e8f0; -} - -.friends-list-container .searchbar-container input.searchbar { - width: 100%; - padding: 0.5rem 0.75rem; - font-size: 0.9rem; - border: 1px solid #cbd5e1; - border-radius: 0.375rem; - background-color: #f8fafc; - outline: none; - transition: all 0.2s; -} - -.friends-list-container .searchbar-container input.searchbar:focus { - background-color: #ffffff; - border-color: #3ba4d7; - box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); -} - -.friends-list-container .friends-scroll { - flex: 1; - overflow-y: auto; - padding: 0.5rem 0; -} - -.friend-list-item { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem 1rem; - margin: 0.125rem 0.5rem; - border-radius: 0.5rem; - cursor: pointer; - transition: all 0.2s; -} - -.friend-list-item:hover { - background-color: #f1f5f9; -} - -.friend-list-item.selected { - background-color: #e0f2fe; -} - -.friend-list-item.selected .friend-meta .friend-name { - color: #0369a1; - font-weight: 600; -} - -.friend-list-item .friend-avatar { - flex-shrink: 0; -} - -.friend-list-item .friend-meta { - flex: 1; - min-width: 0; -} - -.friend-list-item .friend-meta .friend-name { - font-size: 0.95rem; - color: #334155; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - transition: color 0.2s; -} - -.friend-list-item .friend-meta .friend-status { - font-size: 0.8rem; - color: #94a3b8; -} - -.friend-list-item .friend-meta .friend-status.online { - color: #10b981; - font-weight: 500; -} - -.network-right-pane { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - background-color: #f8fafc; -} - -.network-pane-placeholder { - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - color: #94a3b8; - gap: 1rem; - padding: 2rem; - text-align: center; -} - -.network-pane-placeholder i { - font-size: 4rem; - color: #cbd5e1; -} - -.network-pane-placeholder p { - font-size: 1.1rem; - max-width: 400px; -} - -.network-tabs { - display: flex; - background-color: #ffffff; - border-bottom: 1px solid #cbd5e1; - padding: 0.5rem 1rem 0; - gap: 0.5rem; -} - -.network-tabs .tab-btn { - padding: 0.625rem 1.25rem; - font-size: 0.95rem; - font-weight: 600; - color: #64748b; - background: transparent; - border: none; - border-radius: 0.375rem 0.375rem 0 0; - border-bottom: 3px solid transparent; - cursor: pointer; - box-shadow: none; - transition: all 0.2s; -} - -.network-tabs .tab-btn:hover { - color: #334155; - background-color: #f1f5f9; -} - -.network-tabs .tab-btn.active { - color: #3ba4d7; - border-bottom-color: #3ba4d7; - background-color: transparent; -} - -.network-tab-content { - flex: 1; - overflow-y: auto; - padding: 1.5rem; -} - -.network-detail-view { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.network-detail-view .detail-header { - display: flex; - align-items: flex-start; - gap: 1.5rem; - padding-bottom: 1.5rem; - border-bottom: 1px solid #e2e8f0; -} - -.network-detail-view .detail-header .detail-title { - flex: 1; - display: flex; - flex-direction: column; - align-items: flex-start; -} - -.network-detail-view .detail-header .detail-title h2 { - font-size: 1.75rem; - font-weight: 800; - color: #1e293b; - margin-bottom: 0.25rem; -} - -.network-detail-view .detail-header .detail-title .detail-subtitle { - font-size: 0.9rem; - color: #64748b; - display: flex; - align-items: center; - gap: 0.5rem; - margin-bottom: 1.25rem; -} - -.network-detail-view .detail-header .detail-actions { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; -} - -.network-detail-view .detail-header .detail-actions button { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - font-size: 0.9rem; -} - -.network-detail-view .detail-section { - background-color: #ffffff; - border-radius: 0.5rem; - border: 1px solid #e2e8f0; - padding: 1.25rem; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); -} - -.network-detail-view .detail-section h3 { - font-size: 1.1rem; - font-weight: 700; - color: #334155; - margin-bottom: 1rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid #f1f5f9; -} - -.network-detail-view .detail-section .info-grid { - display: grid; - grid-template-columns: 120px 1fr; - row-gap: 0.75rem; - font-size: 0.9rem; -} - -.network-detail-view .detail-section .info-grid .info-label { - font-weight: 600; - color: #64748b; -} - -.network-detail-view .detail-section .info-grid .info-value { - color: #1e293b; - word-break: break-all; -} - -.network-detail-view .locations-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 1rem; -} - -.location-card { - background-color: #ffffff; - border: 1px solid #e2e8f0; - border-radius: 0.5rem; - padding: 1rem; - display: flex; - flex-direction: column; - gap: 0.5rem; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); -} - -.location-card .loc-header { - display: flex; - justify-content: space-between; - align-items: center; - border-bottom: 1px solid #f1f5f9; - padding-bottom: 0.5rem; - margin-bottom: 0.25rem; -} - -.location-card .loc-header .loc-name { - font-weight: 700; - color: #334155; - font-size: 0.95rem; -} - -.location-card .loc-header .loc-status { - font-size: 0.75rem; - font-weight: 600; - padding: 0.125rem 0.5rem; - border-radius: 0.25rem; -} - -.location-card .loc-header .loc-status.online { - background-color: #d1fae5; - color: #065f46; -} - -.location-card .loc-header .loc-status.offline { - background-color: #f1f5f9; - color: #475569; -} - -.location-card .loc-body { - font-size: 0.85rem; - display: grid; - grid-template-columns: 80px 1fr; - row-gap: 0.25rem; -} - -.location-card .loc-body .loc-label { - color: #64748b; -} - -.location-card .loc-body .loc-val { - color: #334155; - word-break: break-all; -} - -.location-card .loc-footer { - margin-top: 0.5rem; - display: flex; - justify-content: flex-end; -} - -.location-card .loc-footer button { - font-size: 0.8rem; - padding: 0.25rem 0.75rem; -} - -.network-chat-view { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; - background-color: #f8fafc; -} - -.network-chat-view .chat-messages { - flex: 1; - overflow-y: auto; - padding: 1.25rem; - display: flex; - flex-direction: column; - gap: 1rem; -} - -.chat-bubble-container { - display: flex; - flex-direction: column; - max-width: 70%; -} - -.chat-bubble-container.outgoing { - align-self: flex-end; - align-items: flex-end; -} - -.chat-bubble-container.outgoing .chat-bubble { - background-color: #3ba4d7; - color: #ffffff; - border-bottom-right-radius: 0.125rem; -} - -.chat-bubble-container.incoming { - align-self: flex-start; - align-items: flex-start; -} - -.chat-bubble-container.incoming .chat-bubble { - background-color: #ffffff; - color: #1e293b; - border: 1px solid #e2e8f0; - border-bottom-left-radius: 0.125rem; -} - -.chat-bubble-container .chat-sender { - font-size: 0.75rem; - color: #64748b; - margin-bottom: 0.25rem; - padding: 0 0.25rem; -} - -.chat-bubble-container .chat-bubble { - padding: 0.625rem 0.875rem; - border-radius: 0.75rem; - font-size: 0.925rem; - line-height: 1.4; - white-space: break-spaces; - word-break: break-word; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.chat-bubble-container .chat-time { - font-size: 0.7rem; - color: #94a3b8; - margin-top: 0.25rem; - padding: 0 0.25rem; -} - -.network-chat-view .chat-input-area { - padding: 1rem; - background-color: #ffffff; - border-top: 1px solid #cbd5e1; - display: flex; - gap: 0.75rem; - align-items: center; -} - -.network-chat-view .chat-input-area textarea.chat-textarea { - flex: 1; - resize: none; - height: 40px; - padding: 0.5rem 0.75rem; - border: 1px solid #cbd5e1; - border-radius: 0.375rem; - font-size: 0.9rem; - outline: none; - transition: all 0.2s; -} - -.network-chat-view .chat-input-area textarea.chat-textarea:focus { - border-color: #3ba4d7; - box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); -} - -.network-chat-view .chat-input-area button.send-btn { - padding: 0.5rem 1.25rem; - font-size: 0.9rem; - height: 40px; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.network-chat-view .chat-warning { - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - color: #64748b; - text-align: center; - padding: 2rem; - gap: 1rem; -} - -.network-chat-view .chat-warning i { - font-size: 3rem; - color: #cbd5e1; -} - -.network-chat-view .chat-warning h4 { - font-weight: 700; - color: #334155; -} - -.network-chat-view .chat-warning p { - max-width: 350px; - font-size: 0.9rem; -} - -/* People Page Modern Split-Pane Layout */ -.people-container { - display: flex; - height: calc(100vh - 55px); - width: 100%; - overflow: hidden; -} - -.people-left-pane { - width: 320px; - border-right: 1px solid #cbd5e1; - display: flex; - flex-direction: column; - background-color: #ffffff; - overflow: hidden; -} - -.people-right-pane { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - background-color: #f8fafc; -} - -/* Header Search Bar at top of People sidebar */ -.people-sidebar-header { - display: flex; - flex-direction: column; - padding: 0.75rem 1rem 0.5rem 1rem; - gap: 0.75rem; - border-bottom: 1px solid #e2e8f0; - background-color: #ffffff; -} - -.people-sidebar-header .searchbar-wrapper { - position: relative; - display: flex; - align-items: center; -} - -.people-sidebar-header .searchbar-wrapper i.fa-search { - position: absolute; - left: 0.85rem; - color: #94a3b8; - font-size: 0.9rem; -} - -.people-sidebar-header .searchbar-wrapper input.searchbar-input { - width: 100%; - padding: 0.5rem 0.75rem 0.5rem 2.25rem; - border: 1px solid #e2e8f0; - border-radius: 0.5rem; - font-size: 0.9rem; - background-color: #f8fafc; - color: #1e293b; - outline: none; - transition: all 0.2s ease; -} - -.people-sidebar-header .searchbar-wrapper input.searchbar-input:focus { - border-color: #3b82f6; - background-color: #ffffff; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); -} - -/* Dual Segmented Tab Control: People | Chats */ -.people-sidebar-header .segmented-control { - display: flex; - background-color: #f1f5f9; - padding: 3px; - border-radius: 0.5rem; - gap: 4px; -} - -.people-sidebar-header .segmented-control button.segment-tab { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - gap: 0.5rem; - padding: 0.5rem 0.75rem; - font-size: 0.9rem; - font-weight: 600; - color: #64748b; - background: transparent; - border: none; - border-radius: 0.375rem; - cursor: pointer; - box-shadow: none; - transition: all 0.2s ease; -} - -.people-sidebar-header .segmented-control button.segment-tab:hover { - color: #1e293b; -} - -.people-sidebar-header .segmented-control button.segment-tab.active { - background-color: #ffffff; - color: #0f172a; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06); -} - -.people-sidebar-header .segmented-control button.segment-tab .segment-badge { - display: inline-flex; - align-items: center; - justify-content: center; - background-color: #cbd5e1; - color: #334155; - font-size: 0.75rem; - font-weight: 700; - min-width: 1.25rem; - height: 1.25rem; - padding: 0 0.35rem; - border-radius: 9999px; - line-height: 1; - transition: all 0.2s ease; -} - -.people-sidebar-header .segmented-control button.segment-tab.active .segment-badge { - background-color: #019dff; - color: #ffffff; -} - - -/* Sub-header Filter Row */ -.people-sidebar-header .sub-filter-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.5rem; - min-height: 32px; -} - -.people-sidebar-header .sub-filter-row select.filter-select { - padding: 0.35rem 0.6rem; - border: 1px solid #cbd5e1; - border-radius: 0.375rem; - font-size: 0.85rem; - font-weight: 600; - color: #475569; - background-color: #ffffff; - cursor: pointer; - outline: none; -} - -.people-sidebar-header .sub-filter-row .btn-add-id { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border-radius: 0.375rem; - background-color: #3b82f6; - color: #ffffff; - border: none; - cursor: pointer; - font-size: 0.9rem; - transition: background-color 0.2s; -} - -.people-sidebar-header .sub-filter-row .btn-add-id:hover { - background-color: #2563eb; -} - -/* Recent Chats List Item */ -.people-left-pane .chat-item { - display: flex; - align-items: center; - padding: 0.75rem 1rem; - gap: 0.75rem; - border-bottom: 1px solid #f1f5f9; - cursor: pointer; - transition: background-color 0.15s ease; - position: relative; -} - -.people-left-pane .chat-item:hover { - background-color: #f8fafc; -} - -.people-left-pane .chat-item.selected { - background-color: #eff6ff; - border-left: 3px solid #3b82f6; -} - -.people-left-pane .chat-item .chat-avatar-wrapper { - position: relative; - flex-shrink: 0; -} - -.people-left-pane .chat-item .chat-avatar-wrapper .status-dot { - position: absolute; - bottom: 0; - right: 0; - width: 10px; - height: 10px; - border-radius: 50%; - border: 2px solid #ffffff; -} - -.people-left-pane .chat-item .chat-info { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 0.15rem; -} - -.people-left-pane .chat-item .chat-info .chat-name { - font-size: 0.9rem; - font-weight: 700; - color: #1e293b; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.people-left-pane .chat-item .chat-info .chat-last-msg { - font-size: 0.8rem; - color: #64748b; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.people-left-pane .chat-item .chat-meta { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 0.25rem; - flex-shrink: 0; -} - -.people-left-pane .chat-item .chat-meta .chat-time { - font-size: 0.75rem; - color: #94a3b8; - white-space: nowrap; -} - -/* ===================================================== - CHAT HUB - Two-Pane Layout - ===================================================== */ - -.chat-hub-container { - display: flex; - height: 100%; - width: 100%; - overflow: hidden; - background-color: #f1f5f9; -} - -.chat-hub-left-pane { - width: 320px; - min-width: 300px; - max-width: 350px; - border-right: 1px solid #cbd5e1; - display: flex; - flex-direction: column; - background: #ffffff; - box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05); -} - -.chat-own-profile-card { - padding: 1.25rem; - border-bottom: 1px solid #e2e8f0; - background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); - position: relative; -} - -.chat-create-lobby-btn { - position: absolute; - bottom: 0.5rem; - right: 1.25rem; - background-color: #0084ff; - color: #ffffff; - border: none; - border-radius: 0.375rem; - padding: 0.35rem 0.75rem; - font-size: 0.85rem; - font-weight: 600; - cursor: pointer; - box-shadow: 0 4px 6px -1px rgba(0, 132, 255, 0.2), 0 2px 4px -1px rgba(0, 132, 255, 0.1); - transition: background-color 0.2s, transform 0.2s; - display: flex; - align-items: center; - gap: 0.25rem; -} - -.chat-create-lobby-btn:hover { - background-color: #0073e6; - transform: translateY(-1px); -} - -.chat-create-lobby-btn:active { - transform: translateY(0); -} - -.chat-own-profile-card .profile-header { - display: flex; - align-items: center; - gap: 1rem; -} - -.chat-own-profile-card .profile-info { - display: flex; - flex-direction: column; - flex: 1; - overflow: hidden; -} - -.chat-own-profile-card .profile-info .profile-name { - font-weight: 700; - color: #1e293b; - font-size: 1.1rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.chat-own-profile-card .profile-info .profile-status { - font-size: 0.85rem; - color: #10b981; - font-weight: 500; - display: flex; - align-items: center; - gap: 0.35rem; -} - -.chat-own-profile-card .profile-info .profile-status::before { - content: ''; - display: inline-block; - width: 8px; - height: 8px; - background-color: #10b981; - border-radius: 50%; -} - -.chat-rooms-list-container { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.chat-rooms-list-container .searchbar-container { - padding: 0.75rem 1rem; - border-bottom: 1px solid #e2e8f0; -} - -.chat-rooms-list-container .searchbar-container input.searchbar { - width: 100%; - padding: 0.5rem 0.75rem; - font-size: 0.9rem; - border: 1px solid #cbd5e1; - border-radius: 0.375rem; - background-color: #f8fafc; - outline: none; - transition: all 0.2s; -} - -.chat-rooms-list-container .searchbar-container input.searchbar:focus { - background-color: #ffffff; - border-color: #3ba4d7; - box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); -} - -.chat-rooms-list-container .rooms-scroll { - flex: 1; - overflow-y: auto; - padding: 0.5rem 0; -} - -.rooms-section-title { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1rem 0.375rem; - font-size: 0.75rem; - font-weight: 700; - color: #64748b; - text-transform: uppercase; - letter-spacing: 0.05em; -} - -.rooms-section-title i { - font-size: 0.7rem; - color: #94a3b8; -} - -.chat-room-list-item { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem 1rem; - margin: 0.125rem 0.5rem; - border-radius: 0.5rem; - cursor: pointer; - transition: all 0.2s; -} - -.chat-room-list-item:hover { - background-color: #f1f5f9; -} - -.chat-room-list-item.selected { - background-color: #e0f2fe; -} - -.chat-room-list-item.selected .room-meta .room-name { - color: #0369a1; - font-weight: 600; -} - -.chat-room-list-item .room-icon { - flex-shrink: 0; - width: 36px; - height: 36px; - border-radius: 0.5rem; - background: linear-gradient(135deg, #3ba4d7, #0ea5e9); - display: flex; - align-items: center; - justify-content: center; - color: #ffffff; - font-size: 1.35rem; -} - -.chat-room-list-item.public-room .room-icon { - background: linear-gradient(135deg, #10b981, #059669); -} - -.chat-room-list-item .room-meta { - flex: 1; - min-width: 0; -} - -.chat-room-list-item .room-meta .room-name { - font-size: 0.95rem; - color: #334155; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - transition: color 0.2s; -} - -.chat-room-list-item .room-meta .room-topic { - font-size: 0.8rem; - color: #94a3b8; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.chat-room-list-item .room-badge { - flex-shrink: 0; - min-width: 24px; - height: 24px; - border-radius: 12px; - background-color: #e2e8f0; - color: #475569; - font-size: 0.75rem; - font-weight: 700; - display: flex; - align-items: center; - justify-content: center; - padding: 0 0.375rem; -} - -.chat-hub-right-pane { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - background-color: #f8fafc; -} - -.chat-pane-placeholder { - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - color: #94a3b8; - gap: 1rem; - padding: 2rem; - text-align: center; -} - -.chat-pane-placeholder i { - font-size: 4rem; - color: #cbd5e1; -} - -.chat-pane-placeholder p { - font-size: 1.1rem; - max-width: 400px; -} - -.chat-hub-tab-content { - flex: 1; - overflow-y: auto; - padding: 1.5rem; -} - -.chat-room-detail-view { - display: flex; - flex-direction: column; - gap: 1.5rem; -} - -.chat-room-detail-view .detail-header { - display: flex; - align-items: flex-start; - gap: 1.5rem; - padding-bottom: 1.5rem; - border-bottom: 1px solid #e2e8f0; - flex-wrap: wrap; -} - -.chat-room-detail-view .detail-header .detail-title { - flex: 1; - min-width: 200px; -} - -.chat-room-detail-view .detail-header .detail-title h2 { - font-size: 1.75rem; - font-weight: 800; - color: #1e293b; - margin-bottom: 0.25rem; -} - -.chat-room-detail-view .detail-header .detail-title .detail-subtitle { - font-size: 0.9rem; - color: #64748b; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.chat-room-detail-view .detail-header .detail-actions { - display: flex; - gap: 0.75rem; - flex-wrap: wrap; -} - -.chat-room-detail-view .detail-header .detail-actions button { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - font-size: 0.9rem; -} - -.chat-room-detail-view .detail-section { - background-color: #ffffff; - border-radius: 0.5rem; - border: 1px solid #e2e8f0; - padding: 1.25rem; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); -} - -.chat-room-detail-view .detail-section h3 { - font-size: 1.1rem; - font-weight: 700; - color: #334155; - margin-bottom: 1rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid #f1f5f9; -} - -.chat-room-detail-view .detail-section .info-grid { - display: grid; - grid-template-columns: 130px 1fr; - row-gap: 0.75rem; - font-size: 0.9rem; -} - -.chat-room-detail-view .detail-section .info-grid .info-label { - font-weight: 600; - color: #64748b; -} - -.chat-room-detail-view .detail-section .info-grid .info-value { - color: #1e293b; - word-break: break-all; -} - -.participants-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); - gap: 0.5rem; -} - -.participant-card { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 0.75rem; - background-color: #f8fafc; - border: 1px solid #e2e8f0; - border-radius: 0.375rem; -} - -.participant-card .participant-name { - font-size: 0.875rem; - color: #334155; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.no-participants { - color: #94a3b8; - font-size: 0.9rem; - font-style: italic; -} - -.detail-actions-footer { - display: flex; - gap: 0.75rem; -} - -.detail-actions-footer button { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - font-size: 0.9rem; -} - -.join-description { - color: #64748b; - font-size: 0.9rem; - margin-bottom: 1rem; -} - -.identities-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 0.75rem; -} - -.identity-card { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.75rem 1rem; - background-color: #f8fafc; - border: 1px solid #e2e8f0; - border-radius: 0.5rem; - cursor: pointer; - transition: all 0.2s; -} - -.identity-card:hover { - background-color: #e0f2fe; - border-color: #3ba4d7; -} - -.identity-card .identity-name { - font-size: 0.95rem; - font-weight: 600; - color: #334155; -} - -.identity-card i { - color: #3ba4d7; - font-size: 0.9rem; -} - -.no-rooms { - padding: 1rem; - color: #94a3b8; - text-align: center; - font-style: italic; -} - -/* Chat Hub Responsive - Mobile */ -@media (max-width: 899px) { - .chat-hub-container { - flex-direction: column; - } - - .chat-hub-left-pane { - width: 100%; - min-width: 0; - max-width: none; - max-height: 45%; - border-right: none; - border-bottom: 1px solid #cbd5e1; - } - - .chat-hub-right-pane { - flex: 1; - min-height: 0; - } -} - -/* ===================================================== - CHAT HUB - Right Pane Conversation & Tabs Styling - ===================================================== */ - -.chat-hub-header-bar { - padding: 0.75rem 1.5rem; - background-color: #ffffff; - border-bottom: 1px solid #e2e8f0; - display: flex; - align-items: center; - justify-content: space-between; - height: 65px; - flex-shrink: 0; -} - -.chat-hub-header-bar .chat-header-info { - display: flex; - flex-direction: column; - overflow: hidden; -} - -.chat-hub-header-bar .chat-header-info .chat-header-name { - font-size: 1.15rem; - font-weight: 800; - color: #1e293b; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.chat-hub-header-bar .chat-header-info .chat-header-topic { - font-size: 0.85rem; - color: #64748b; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin-top: 0.125rem; -} - -.chat-hub-header-bar .chat-header-actions { - display: flex; - gap: 0.5rem; -} - -.chat-hub-header-bar .chat-header-actions button { - display: flex; - align-items: center; - gap: 0.35rem; - padding: 0.375rem 0.75rem; - font-size: 0.85rem; -} - -.chat-hub-tabs-container { - background-color: #ffffff; - border-bottom: 1px solid #cbd5e1; - padding: 0.5rem 1.5rem 0; -} - -.chat-hub-tabs { - display: flex; - gap: 0.5rem; -} - -.chat-hub-tabs .tab-btn { - padding: 0.625rem 1.25rem; - font-size: 0.95rem; - font-weight: 600; - color: #64748b; - background: transparent; - border: none; - border-radius: 0.375rem 0.375rem 0 0; - border-bottom: 3px solid transparent; - cursor: pointer; - box-shadow: none; - transition: all 0.2s; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.chat-hub-tabs .tab-btn:hover { - color: #334155; - background-color: #f1f5f9; -} - -.chat-hub-tabs .tab-btn.active { - color: #3ba4d7; - border-bottom-color: #3ba4d7; - background-color: transparent; -} - -.chat-hub-tab-content { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - background-color: #f8fafc; -} - -.chat-hub-conversation-layout { - display: flex; - flex-direction: row; - height: 100%; - width: 100%; - overflow: hidden; -} - -.chat-hub-conversation-main { - display: flex; - flex-direction: column; - flex: 1; - height: 100%; - overflow: hidden; - min-height: 0; - min-width: 0; -} - -.chat-hub-rightbar { - width: 200px; - border-left: 1px solid #cbd5e1; - background-color: #ffffff; - display: flex; - flex-direction: column; - flex-shrink: 0; - position: relative; -} - -.chat-hub-rightbar .rightbar-title { - padding: 0.75rem 1rem; - font-size: 0.85rem; - font-weight: 700; - color: #64748b; - text-transform: uppercase; - letter-spacing: 0.05em; - border-bottom: 1px solid #e2e8f0; -} - -.chat-hub-rightbar .rightbar-users-list { - flex: 1; - overflow-y: auto; - padding: 0.5rem; -} - -.chat-hub-rightbar .user { - padding: 0.5rem 0.75rem; - font-size: 0.9rem; - color: #334155; - border-radius: 0.375rem; - transition: all 0.2s; - display: flex; - align-items: center; - gap: 0.5rem; - position: relative; -} - -.chat-hub-rightbar .user .user-name { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1; -} - -.chat-hub-rightbar .user:hover { - background-color: #f1f5f9; - color: #0f172a; -} - -.user-tooltip { - position: absolute; - width: 260px; - background-color: #ffffe1; - border: 1px solid #7f7f7f; - box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.25); - padding: 0.5rem; - border-radius: 0.25rem; - z-index: 10000; - white-space: normal; - display: flex; - gap: 0.5rem; - align-items: flex-start; -} - -.chat-hub-rightbar .user-tooltip { - left: -275px; - transform: translateY(-50%); - z-index: 1000; -} - -.user-tooltip .tooltip-avatar { - flex-shrink: 0; -} - -.user-tooltip .tooltip-details { - display: flex; - flex-direction: column; - gap: 0.25rem; - font-size: 0.8rem; - color: #000000; - text-align: left; -} - -.user-tooltip .tooltip-row { - line-height: 1.2; -} - -.user-tooltip .tooltip-label { - font-weight: bold; -} - -.user-tooltip .tooltip-value { - font-weight: normal; - word-break: break-all; -} - -.user-tooltip .tooltip-value.tooltip-id { - font-family: monospace; -} - -.chat-hub-rightbar .rightbar-context-menu { - position: absolute; - right: 1rem; - width: 210px; - background-color: #ffffff; - border: 1px solid #e2e8f0; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); - border-radius: 0.375rem; - z-index: 1010; - padding: 0.25rem 0; - display: flex; - flex-direction: column; -} - -.chat-hub-rightbar .rightbar-context-menu .menu-item { - padding: 0.5rem 1rem; - font-size: 0.85rem; - color: #334155; - cursor: pointer; - display: flex; - align-items: center; - transition: background-color 0.2s; -} - -.chat-hub-rightbar .rightbar-context-menu .menu-item:hover { - background-color: #f1f5f9; - color: #0f172a; -} - -.chat-hub-rightbar .user .defaultAvatar { - width: 2rem; - height: 2rem; - font-size: 0.9rem; - flex-shrink: 0; -} - -.chat-hub-rightbar .user img.avatar { - width: 2rem; - height: 2rem; - flex-shrink: 0; -} - -@media (max-width: 899px) { - .chat-hub-rightbar { - display: none; - } -} - -.chat-hub-messages { - flex: 1; - overflow-y: auto; - padding: 1.25rem 1.5rem; - display: flex; - flex-direction: column; - gap: 1rem; -} - -/* Chat bubble overrides for two-pane layout */ -.chat-hub-messages .message { - display: flex; - flex-direction: column; - max-width: 70%; - padding: 0.625rem 0.875rem; - border-radius: 0.75rem; - font-size: 1rem; - line-height: 1.4; - word-break: break-word; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.chat-hub-messages .message.incoming { - align-self: flex-start; - align-items: flex-start; - background-color: #ffffff; - color: #1e293b; - border: 1px solid #e2e8f0; - border-bottom-left-radius: 0.125rem; -} - -.chat-hub-messages .message.outgoing { - align-self: flex-end; - align-items: flex-end; - background-color: #3ba4d7; - color: #ffffff; - border-bottom-right-radius: 0.125rem; -} - -.chat-hub-messages .message .username { - font-size: 0.75rem; - margin-bottom: 0.25rem; - padding: 0 0.125rem; - font-weight: 700; -} - -.chat-hub-messages .message.incoming .username { - color: #0369a1; -} - -.chat-hub-messages .message.outgoing .username { - color: #e0f2fe; -} - -.chat-hub-messages .message .messagetext { - white-space: break-spaces; - margin: 0; -} - -.chat-hub-messages .message .datetime { - font-size: 0.7rem; - margin-top: 0.25rem; - padding: 0 0.125rem; - opacity: 0.8; -} - -.chat-hub-messages .message.incoming .datetime { - color: #64748b; -} - -.chat-hub-messages .message.outgoing .datetime { - color: #f1f5f9; -} - -.chat-hub-input-area { - padding: 0.75rem 1.5rem; - background-color: #ffffff; - border-top: 1px solid #cbd5e1; - display: flex; - gap: 0.75rem; - align-items: flex-end; - flex-shrink: 0; -} - -.chat-hub-input-area textarea.chat-hub-textarea { - flex: 1; - resize: vertical; - min-height: 40px; - max-height: 250px; - height: 40px; - padding: 0.5rem 0.75rem; - border: 1px solid #cbd5e1; - border-radius: 0.375rem; - font-size: 0.9rem; - outline: none; - transition: border-color 0.2s, box-shadow 0.2s; - background-color: #f8fafc; -} - -.chat-hub-input-area textarea.chat-hub-textarea:focus { - background-color: #ffffff; - border-color: #3ba4d7; - box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); -} - -.chat-hub-input-area button.chat-hub-send-btn { - padding: 0.5rem 1.25rem; - font-size: 0.9rem; - height: 40px; - display: flex; - align-items: center; - gap: 0.5rem; - border-radius: 0.375rem; -} - -/* Compact Room Chat Style (No bubbles, IRC-style single line per message) */ -.chat-hub-messages.compact-container, -.messages.compact-container { - gap: 0 !important; - padding: 0.5rem 0.75rem !important; - background-color: #ffffff !important; - display: flex !important; - flex-direction: column !important; - flex: 1 !important; - overflow-y: auto !important; - min-height: 0 !important; -} - -.chat-hub-messages.compact-container .message.compact, -.messages.compact-container .message.compact { - display: block !important; - max-width: 100% !important; - padding: 1px 4px !important; - border-radius: 2px !important; - background-color: transparent !important; - border: none !important; - box-shadow: none !important; - align-self: stretch !important; - font-size: 0.9rem !important; - line-height: 1.35 !important; - margin: 0 !important; - white-space: normal !important; - word-break: break-word !important; - overflow: visible !important; - width: 100% !important; -} - -.chat-hub-messages.compact-container .message.compact:hover, -.messages.compact-container .message.compact:hover { - background-color: #f8fafc !important; -} - -.chat-hub-messages.compact-container .message.compact .datetime, -.messages.compact-container .message.compact .datetime { - color: #a0a0a0 !important; - margin-right: 0.4rem !important; - font-size: 0.78rem !important; - font-family: monospace !important; - opacity: 1 !important; - display: inline !important; - margin-top: 0 !important; - margin-bottom: 0 !important; - padding: 0 !important; -} - -.chat-hub-messages.compact-container .message.compact .username, -.messages.compact-container .message.compact .username { - font-weight: bold !important; - margin-right: 0.2rem !important; - margin-bottom: 0 !important; - font-size: 0.875rem !important; - display: inline !important; - padding: 0 !important; -} - -.chat-hub-messages.compact-container .message.compact .messagetext, -.messages.compact-container .message.compact .messagetext { - color: #1e293b !important; - white-space: normal !important; - word-break: break-word !important; - display: inline !important; - margin: 0 !important; - font-size: 1rem !important; -} - -/* Make emoji characters render larger than surrounding text in chat */ -.chat-hub-messages .message .messagetext, -.chat-hub-messages.compact-container .message.compact .messagetext, -.messages.compact-container .message.compact .messagetext { - font-family: 'Segoe UI Emoji', 'Apple Color Emoji', 'Noto Color Emoji', 'Roboto', Arial, sans-serif; -} - -.chat-emoji { - font-size: 1.45em; - line-height: 1; - vertical-align: -0.15em; - display: inline-block; -} - -/* Fix RetroShare ID textarea - auto-size to content, no scrollbar */ -.homepage .certificate__content .retroshareID .textArea { - min-height: unset !important; - height: auto !important; - overflow: hidden !important; - field-sizing: content !important; -} - -/* Attach file, emoji, image action buttons in chat */ -.chat-hub-attach-btn, -.chat-hub-action-btn { - background-color: transparent !important; - border: none !important; - font-size: 1.15rem !important; - color: #64748b !important; - cursor: pointer !important; - padding: 0.4rem 0.5rem !important; - border-radius: 0.375rem !important; - flex-shrink: 0 !important; - display: inline-flex !important; - align-items: center !important; - justify-content: center !important; - transition: all 0.2s !important; - box-shadow: none !important; - margin: 0 !important; - line-height: 1 !important; - height: 36px !important; - width: 36px !important; -} - -.chat-hub-attach-btn:hover, -.chat-hub-action-btn:hover { - background-color: #f1f5f9 !important; - color: #3b82f6 !important; - transform: none !important; -} - -.attach-modal-overlay { - 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; -} - -.attach-modal { - background-color: #ffffff; - border-radius: 0.5rem; - width: 450px; - max-width: 90%; - padding: 1.5rem; - box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); - display: flex; - flex-direction: column; - gap: 1rem; -} - -.attach-modal .attach-modal-header { - display: flex; - align-items: center; - gap: 0.6rem; - margin-bottom: 0.25rem; -} - -.attach-modal .attach-modal-icon { - font-size: 1.2rem; - color: #3b82f6; -} - -.attach-modal h4 { - margin: 0; - font-size: 1.2rem; - color: #0f172a; -} - -.attach-modal p { - margin: 0; - font-size: 0.9rem; - color: #475569; -} - -.attach-modal .attach-path-row { - display: flex; - gap: 0.5rem; - align-items: center; -} - -.attach-modal .attach-path-row input[type="text"] { - flex: 1; - padding: 0.75rem; - border: 1px solid #cbd5e1; - border-radius: 0.375rem; - font-size: 0.9rem; - outline: none; - transition: border-color 0.2s; - min-width: 0; -} - -.attach-modal .attach-path-row input[type="text"]:focus { - border-color: #3b82f6; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); -} - -.attach-browse-btn { - flex-shrink: 0; - display: flex; - align-items: center; - gap: 0.35rem; - padding: 0.625rem 0.9rem; - font-size: 0.875rem; - background-color: #f1f5f9; - color: #334155; - border: 1px solid #cbd5e1; - border-radius: 0.375rem; - cursor: pointer; - box-shadow: none; - transition: background-color 0.2s, border-color 0.2s; - white-space: nowrap; -} - -.attach-browse-btn:hover { - background-color: #e2e8f0; - border-color: #94a3b8; -} - -.attach-path-hint { - display: flex; - align-items: flex-start; - gap: 0.5rem; - padding: 0.6rem 0.75rem; - background-color: #fffbeb; - border: 1px solid #fcd34d; - border-left: 3px solid #f59e0b; - border-radius: 0.375rem; - font-size: 0.825rem; - color: #92400e; - line-height: 1.45; -} - -.attach-path-hint i { - color: #f59e0b; - margin-top: 0.1rem; - flex-shrink: 0; -} - -.attach-path-hint code { - font-family: monospace; - background-color: rgba(245, 158, 11, 0.15); - padding: 0.05rem 0.25rem; - border-radius: 0.2rem; -} - -.attach-modal .hashing-spinner { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.9rem; - color: #3b82f6; -} - -.attach-modal .error-text { - color: #ef4444; - font-size: 0.85rem; - margin: 0; -} - -.attach-modal .modal-buttons { - display: flex; - justify-content: flex-end; - gap: 0.75rem; - margin-top: 0.5rem; -} - -.attach-modal .modal-buttons button { - padding: 0.5rem 1rem; - font-size: 0.9rem; - border-radius: 0.25rem; - border: none; - cursor: pointer; - transition: opacity 0.2s; -} - -.attach-modal .modal-buttons button:hover { - opacity: 0.9; -} - -/* ========================= Emoji Picker ========================= */ -.chat-hub-emoji-btn { - background-color: transparent; - border: none; - font-size: 1.3rem; - cursor: pointer; - padding: 0.35rem 0.4rem; - margin-right: 0.25rem; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - border-radius: 0.375rem; - line-height: 1; - transition: background-color 0.15s, transform 0.15s; - box-shadow: none; -} - -.chat-hub-emoji-btn:hover { - background-color: #f1f5f9; - transform: scale(1.1); -} - -.emoji-picker-wrapper { - position: relative; - flex-shrink: 0; - display: flex; - align-items: center; -} - -.emoji-picker { - position: absolute; - bottom: calc(100% + 0.5rem); - left: 0; - width: 320px; - background-color: #ffffff; - border: 1px solid #e2e8f0; - border-radius: 0.625rem; - box-shadow: 0 8px 30px -4px rgba(0, 0, 0, 0.18), 0 4px 12px -2px rgba(0, 0, 0, 0.1); - z-index: 3000; - display: flex; - flex-direction: column; - overflow: hidden; - animation: emoji-pop 0.15s ease-out; -} - -@keyframes emoji-pop { - from { opacity: 0; transform: scale(0.92) translateY(6px); } - to { opacity: 1; transform: scale(1) translateY(0); } -} - -.emoji-search-row { - display: flex; - align-items: center; - gap: 0.4rem; - padding: 0.6rem 0.75rem 0.4rem; - border-bottom: 1px solid #f1f5f9; -} - -.emoji-search-icon { - color: #94a3b8; - font-size: 0.8rem; - flex-shrink: 0; -} - -.emoji-search-input { - flex: 1; - border: 1px solid #e2e8f0; - border-radius: 0.375rem; - padding: 0.3rem 0.5rem; - font-size: 0.85rem; - outline: none; - background-color: #f8fafc; - transition: border-color 0.15s; -} - -.emoji-search-input:focus { - border-color: #3ba4d7; - background-color: #fff; -} - -.emoji-search-clear { - background: none; - border: none; - cursor: pointer; - color: #94a3b8; - padding: 0.2rem; - font-size: 0.8rem; - box-shadow: none; - display: flex; - align-items: center; -} - -.emoji-search-clear:hover { - color: #475569; -} - -.emoji-categories { - display: flex; - gap: 0.1rem; - padding: 0.35rem 0.5rem; - border-bottom: 1px solid #f1f5f9; - overflow-x: auto; - scrollbar-width: none; -} - -.emoji-categories::-webkit-scrollbar { - display: none; -} - -.emoji-cat-btn { - background: none; - border: none; - cursor: pointer; - font-size: 1.2rem; - padding: 0.3rem 0.35rem; - border-radius: 0.375rem; - line-height: 1; - box-shadow: none; - transition: background-color 0.1s; - flex-shrink: 0; -} - -.emoji-cat-btn:hover { - background-color: #f1f5f9; -} - -.emoji-cat-btn.active { - background-color: #e0f2fe; - box-shadow: inset 0 -2px 0 #3ba4d7; -} - -.emoji-grid { - display: grid; - grid-template-columns: repeat(7, 1fr); - gap: 0; - padding: 0.4rem 0.35rem; - max-height: 220px; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: #cbd5e1 transparent; -} - -.emoji-grid::-webkit-scrollbar { - width: 4px; -} - -.emoji-grid::-webkit-scrollbar-track { - background: transparent; -} - -.emoji-grid::-webkit-scrollbar-thumb { - background-color: #cbd5e1; - border-radius: 4px; -} - -.emoji-btn { - background: none; - border: none; - cursor: pointer; - font-size: 1.7rem; - padding: 0.25rem; - border-radius: 0.3rem; - line-height: 1; - box-shadow: none; - text-align: center; - transition: background-color 0.1s, transform 0.1s; - display: flex; - align-items: center; - justify-content: center; - aspect-ratio: 1; -} - -.emoji-btn:hover { - background-color: #f1f5f9; - transform: scale(1.2); -} - -table.mails th.sortable-th { - cursor: pointer; - user-select: none; - transition: background-color 0.2s, color 0.2s; -} - -table.mails th.sortable-th:hover { - background-color: #eef3f6; - color: #000; -} - -.compose-mail__from { - display: flex; - justify-content: flex-start; - align-items: center; - gap: 0.5rem; -} - -/* Status Bar Styles */ -.statusbar { - display: flex; - justify-content: space-between; - align-items: center; - height: 28px; - background-color: #14141b; - border-top: 1px solid #2e2e38; - padding: 0 1rem; - font-size: 0.8rem; - color: #94a3b8; - z-index: 100; - box-sizing: border-box; - user-select: none; - flex-shrink: 0; -} - -.statusbar-left { - display: flex; - align-items: center; -} - -.statusbar-right { - display: flex; - align-items: center; - gap: 1.5rem; -} - -.statusbar-item { - display: flex; - align-items: center; -} - -.statusbar-divider { - width: 1px; - height: 14px; - background-color: #2e2e38; -} - -.status-bullet { - width: 8px; - height: 8px; - border-radius: 50%; - display: inline-block; - box-shadow: 0 0 4px rgba(0, 0, 0, 0.5); -} - -/* Hidden Service Configuration layout overrides */ -.proxy-server-container { - width: 100%; - display: flex; - flex-direction: column; - gap: 1rem; -} - -.proxy-description { - color: #334155; - font-size: 0.95rem; - margin-bottom: 0.5rem; -} - -.proxy-rows-container { - display: flex; - flex-direction: column; - gap: 0.75rem; - width: 100%; -} - -.proxy-row { - display: grid; - grid-template-columns: 160px 220px 220px auto; - gap: 0.75rem; - align-items: center; - width: 100%; -} - -.proxy-label { - font-size: 0.95rem; - font-weight: 500; - color: #1e293b; -} - -.proxy-addr-input { - width: 100% !important; - max-width: none !important; -} - -.proxy-port-input { - width: 100% !important; - max-width: none !important; -} - -.proxy-status-container { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.proxy-status-bullet { - width: 14px; - height: 14px; - border-radius: 50%; - display: inline-block; - border: 1px solid #475569; -} - -.proxy-status-text { - font-size: 0.95rem; - color: #1e293b; -} - + */@font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:auto;src:url("./webfonts/fa-solid-900.eot");src:url("./webfonts/fa-solid-900.eot?#iefix") format("embedded-opentype"),url("./webfonts/fa-solid-900.woff2") format("woff2"),url("./webfonts/fa-solid-900.woff") format("woff"),url("./webfonts/fa-solid-900.ttf") format("truetype"),url("./webfonts/fa-solid-900.svg#fontawesome") format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900}html{font-size:87.5%;box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}body,h1,h2,h3,h4,h5,h6,p,figure,blockquote,dl,dd{margin:0;padding:0}ul[role=list],ol[role=list]{list-style:none}html:focus-within{scroll-behavior:smooth}body{text-rendering:optimizeSpeed;line-height:1.5;font-family:"Roboto",Arial,Helvetica,sans-serif !important;letter-spacing:-0.025ch}a:not([class]){text-decoration-skip-ink:auto}img,picture{max-width:100%;display:block}input,button,textarea,select{font:inherit}@media(prefers-reduced-motion: reduce){html:focus-within{scroll-behavior:auto}*,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important;scroll-behavior:auto !important}}#main{height:100vh}.content{display:flex;height:100%;overflow:hidden}.tab-content{display:flex;height:100%;width:100%;background-color:#eef3f6;animation:fadein .3s;overflow:auto}input[type=text],input[type=password],input[type=number],textarea{box-sizing:border-box;background:#fff;max-width:100%;font-size:1rem;font-weight:400;border:1px solid #ccc;border-radius:.25rem;padding:.25rem .5rem;outline:rgba(0,0,0,0)}input:focus{border:1px solid #3ba4d7;box-shadow:inset 0 0 5px #ccc}input.stretched{width:90%}input.small{max-width:70%;padding:.1rem}input.searchbar{width:40%}a{cursor:pointer}a[title=Back]{width:max-content;height:max-content;padding:.475rem .75rem;border-radius:50%;transition:100ms}a[title=Back]:hover{background:#eef3f6}table{padding:20px;table-layout:fixed;width:100%;border-collapse:collapse;text-align:center;color:#333;font-size:1.125rem}table th{font-size:1.125rem;color:#000;border-bottom:2px solid #eee}table tr{border-bottom:1px solid #eee}h3{color:#444}hr{margin-left:0;color:#aaa}.grid-2col{display:grid;grid-template-columns:auto auto;gap:1rem;justify-content:start}.grid-2col input[type=checkbox]{margin-top:20px}.error{color:red}.tooltip{color:#333;position:relative;display:inline-block;margin:0 .25rem}.tooltiptext{visibility:hidden;position:absolute;top:100%;left:50%;min-width:250px;margin-left:-120px;z-index:1;color:#ccc;background-color:#333;font-size:.875rem;text-align:center;padding:.25rem;border-radius:.5rem}.tooltip:hover .tooltiptext{visibility:visible;animation:fadein .5s}blockquote{color:#14141b;padding:.75rem 1rem .75rem 2rem;border-radius:.25rem}blockquote.info{position:relative;line-height:1.2;color:rgba(20,20,27,.8);border:1px solid rgba(17,143,204,.8)}blockquote.info::before{font-family:"Font Awesome 5 Free";position:absolute;top:.5rem;left:.5rem;content:"";color:#019dff}@keyframes fadein{from{opacity:0}to{opacity:1}}.fadein{animation:fadein .5s}@keyframes swipe-from-left{from{margin-left:100%}to{margin-left:0}}button{width:max-content;height:max-content;color:#fff;background:#019dff;font-size:1rem;padding:.4rem 1rem;border:0;border-radius:5px;cursor:pointer;box-shadow:inset -3px -3px 0 rgb(0,94.5826771654,154)}button:active{outline:none;box-shadow:inset 3px 3px 0 rgb(0,94.5826771654,154)}button.red{width:max-content;height:max-content;color:#fff;background:#ff3a4a;font-size:1rem;padding:.4rem 1rem;border:0;border-radius:5px;cursor:pointer;box-shadow:inset -3px -3px 0 rgb(211,0,17.1370558376)}button.red:active{outline:none;box-shadow:inset 3px 3px 0 rgb(211,0,17.1370558376)}.media-item{display:flex;margin-top:.5rem;padding:1rem;border:1px solid rgba(20,20,27,.1);border-radius:4px}.media-item__details{flex-basis:40%;display:flex;align-items:start;gap:.5rem}.media-item__details img{width:6rem;object-fit:contain}.media-item__desc{flex-basis:60%}.active-link{background:hsla(0,0%,100%,.1) !important}.nav-menu{background-color:#14141b;box-shadow:0 5px 5px #222;display:flex;flex-direction:column;align-items:center;height:100%;padding:.25rem;margin-right:0rem}.nav-menu__logo{padding:1.2rem 0;display:flex;align-items:center;gap:.3rem}.nav-menu__logo img{width:1.6rem}.nav-menu__logo h5{line-height:1;color:#fff}.nav-menu__box{padding:2rem .125rem;display:flex;flex-direction:column;gap:.5rem;position:relative}.nav-menu__box .item{margin:0;padding:.675rem .5rem;width:10rem;display:flex;align-items:center;line-height:1;border-radius:.5rem;text-decoration:none;color:#ccc;text-transform:capitalize;transition:0ms}.nav-menu__box .item:hover{background-color:rgba(238,243,246,.15)}.nav-menu__box .item i.sidenav-icon{width:2.5rem;height:1.4rem;display:grid;place-items:center}.nav-menu__box .item.item-selected{color:#9bdaff;background-color:rgba(155,218,255,.15);font-weight:medium}.nav-menu__box button.toggle-nav{display:none;position:absolute;padding:0;top:0;right:-1rem;background:rgb(77.5,186.5157480315,255);width:1.5rem;height:1.5rem;aspect-ratio:1;justify-content:center;align-items:center;border-radius:50%;box-shadow:none}.nav-menu.collapsed .nav-menu__logo .logo-container{display:flex;flex-direction:column;align-items:center;gap:.5rem}.nav-menu.collapsed .nav-menu__logo .logo-container>*:not(img){display:block}.nav-menu.collapsed .nav-menu__logo .nav-menu__logo-text{display:none !important}.nav-menu.collapsed .nav-menu__box .item{padding:.675rem 0;width:2.5rem;justify-content:center;transition:300ms}.nav-menu.collapsed .nav-menu__box .item span,.nav-menu.collapsed .nav-menu__box .item p{display:none !important}.nav-menu.collapsed button i{rotate:180deg}.nav-menu:hover button.toggle-nav{display:flex}.sidebar{width:13rem;background-color:#fff;display:flex;flex-direction:column}.sidebar a{text-decoration:none;text-transform:capitalize;padding:1rem;cursor:pointer;color:#999}.sidebar a:hover{color:#222}.sidebar .selected-sidebar-link{font-weight:bold;color:#222;border-left:5px solid #3ba4d7;animation:expand-left-border .1s}.sidebarquickview>h6{padding:.5rem}.sidebarquickview a{text-decoration:none;text-transform:capitalize;padding:.5rem 1rem;display:block;color:#999}.sidebarquickview a a:hover{color:#222}.sidebarquickview .selected-sidebarquickview-link{font-weight:bold;color:#222;border-left:5px solid #3ba4d7;animation:expand-left-border .1s}.node-panel{width:100%;padding:.5rem;animation:fadein .5s}@keyframes expand-left-border{from{border-left:0}to{border-left:5px solid #3ba4d7}}@media(max-width: 700px){.tab-content{flex-direction:column}.sidebar{width:100% !important;flex-direction:row !important;overflow-x:auto !important;overflow-y:hidden !important;white-space:nowrap !important;border-bottom:1px solid rgba(20,20,27,.1) !important;background:#fff !important;z-index:50 !important;flex-shrink:0 !important;height:auto !important;padding:0 !important}.sidebar a{display:inline-block !important;padding:.8rem 1.2rem !important;border-bottom:3px solid rgba(0,0,0,0) !important;border-left:none !important}.sidebar .selected-sidebar-link{border-left:none !important;border-bottom:3px solid #3ba4d7 !important;animation:none !important}.sidebarquickview>h4,.sidebarquickview>h6{display:none !important}}.posts{height:100%;margin-top:1rem;flex-direction:column;overflow:auto}.posts__heading{display:flex;flex-direction:column;justify-content:space-between}.posts-container{height:100%;padding:1rem;display:grid;grid-template-columns:repeat(auto-fill, minmax(150px, 1fr));gap:2rem;border:1px solid rgba(20,20,27,.1);border-radius:4px;overflow:auto}.posts-container-card{min-height:240px;flex-direction:column;border:1px solid rgba(20,20,27,.5);border-radius:4px;cursor:pointer;text-align:center}.posts-container-card img{flex-basis:90%;object-fit:cover}.posts-container-card p{padding:0 .125rem;flex-basis:10%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.progress-bar{width:100%;height:2rem;position:relative;text-align:center;background-color:#eef3f6;border-radius:20px;overflow:hidden}.progress-bar__status{position:absolute;top:0;left:0;height:100%;color:#14141b;background-color:#019dff}.progress-bar__percent{position:absolute;inset:0;margin:auto;width:fit-content;height:fit-content}.progress-bar-chunks{position:relative;margin-top:.5rem;width:100%;height:2rem;display:flex;border-radius:.25rem;overflow:hidden;background-color:#eef3f6}.progress-bar-chunks .chunk{width:100%}.progress-bar-chunks .chunk[data-chunkVal="0"]{background-color:rgba(155,218,255,.2)}.progress-bar-chunks .chunk[data-chunkVal="1"]{background-color:#ff3a4a}.progress-bar-chunks .chunk[data-chunkVal="2"]{background-color:#019dff}.progress-bar-chunks .chunk[data-chunkVal="3"]{background-color:#fcba03}.progress-bar-chunks__percent{position:absolute;inset:0;margin:auto;width:fit-content;height:fit-content}.statusbar{display:flex;justify-content:space-between;align-items:center;height:28px;background-color:#14141b;border-top:1px solid #2e2e38;padding:0 1rem;font-size:.8rem;color:#94a3b8;z-index:100;box-sizing:border-box;user-select:none;flex-shrink:0}.statusbar-left{display:flex;align-items:center}.statusbar-right{display:flex;align-items:center;gap:1.5rem}.statusbar-item{display:flex;align-items:center}.statusbar-divider{width:1px;height:14px;background-color:#2e2e38}.status-bullet{width:8px;height:8px;border-radius:50%;display:inline-block;box-shadow:0 0 4px rgba(0,0,0,.5)}.widget{height:100%;padding:1rem;display:flex;flex-direction:column;gap:.5rem;background-color:#fff;border-radius:.5rem;overflow:auto}.widget .top-heading{display:flex;justify-content:space-between}.widget__heading{display:flex;justify-content:space-between;align-items:center;border-bottom:2px solid #999}.widget__body{height:100%;display:flex;flex-direction:column;overflow:auto}.widget__body-heading{display:flex;justify-content:space-between;align-items:center}.widget__body-heading .action{display:flex;gap:.5rem}.widget__body-content{height:100%;overflow:auto}.widget__body-box{display:flex;flex-direction:column;gap:.5rem}.widget-half{max-width:50%}#modal-container{display:none;position:fixed;z-index:1;height:100%;top:0;left:0;width:100%;background-color:rgba(0,0,0,.2)}.modal-content{position:absolute;color:#555;width:40%;min-height:10rem;height:max-content;padding:1.5rem;inset:0;margin:auto;background-color:#fff;border-radius:.5rem;animation:fadein .5s;display:flex;flex-direction:column}.modal-content button:last-child{margin-top:auto}.modal-content .close-btn{position:absolute;right:1.5rem}.modal-content .widget{padding:0}#notification-container{position:absolute;bottom:0;right:0}.login-page{background-image:linear-gradient(-45deg, rgba(1, 157, 255, 0.75), rgba(17, 143, 204, 0.75));height:100%;animation:fadein .5s}.login-page .login-container{background-color:#fff;box-shadow:3px 3px 5px rgba(20,20,27,.4);margin:auto;position:relative;top:100px;max-width:400px;max-height:500px;border-radius:5px;display:flex;flex-direction:column;align-items:center}.login-page .login-container input{padding:.375rem .75rem;border-radius:.275rem}.login-page .login-container *{margin-bottom:1rem}.login-page .login-container>img{margin:1rem 0 2rem}.login-page .login-container extra{margin:0}.login-page .login-container>a{text-decoration:underline;cursor:pointer}.login-page .extra>label,.login-page .extra>br,.login-page .extra>input{margin-bottom:0}.homepage{margin:2rem auto 0;display:flex;flex-direction:column;gap:4rem}.homepage .logo{display:flex;justify-content:center;align-items:center}.homepage .logo img{width:90px}.homepage .logo .retroshareText{display:flex;flex-direction:column;align-items:center}.homepage .logo .retroshareText .retrotext{font-size:36px;font-weight:600;line-height:1.125}.homepage .logo .retroshareText .retrotext>span{color:#118fcc}.homepage .logo .retroshareText>b{font-size:14px;line-height:1}.homepage .certificate{display:flex;flex-direction:column;gap:4rem}.homepage .certificate__heading{text-align:center}.homepage .certificate__heading>h1{margin-bottom:1rem}.homepage .certificate__content{display:flex;flex-direction:column;gap:2rem;padding:2rem;text-align:center;border:1.5px solid rgba(17,143,204,.2);border-radius:6px;box-shadow:0px 0px 8px 2px rgba(20,20,27,.05)}.homepage .certificate__content .rsId>p{margin-bottom:.5rem;color:#118fcc}.homepage .certificate__content .retroshareID{padding:.25rem;display:flex;align-items:center;justify-self:start;font-size:1.25rem;border-radius:4px;background:rgba(20,20,27,.05)}.homepage .certificate__content .retroshareID .textArea{padding:0;width:100%;height:auto;font-size:1rem;font-family:monospace;background:rgba(0,0,0,0);border:none;resize:none;overflow:hidden;field-sizing:content}.homepage .certificate__content .retroshareID i{color:#118fcc}.homepage .certificate__content .retroshareID>i{margin:0 .5rem;cursor:pointer}.homepage .certificate__content .webhelp{padding:.5rem;background:#f5f5f5;display:flex;justify-content:center;align-items:center;gap:.5rem;border-radius:4px;border:1px solid rgba(20,20,27,.5);width:fit-content;cursor:pointer}.homepage .certificate__content .webhelp-container{display:grid;place-items:center}.homepage .certificate__content .webhelp:hover{background:#eef3f6;border:1px solid #14141b}.homepage .certificate__content .webhelp>i{font-size:1.2rem;color:green}.homepage .certificate__content .add-friend>h6,.homepage .certificate__content .webhelp-container>h6{font-weight:normal;margin-bottom:.5rem}.network-container{display:flex;height:100%;width:100%;overflow:hidden;background-color:#f1f5f9}.network-left-pane{width:320px;min-width:300px;max-width:350px;border-right:1px solid #cbd5e1;display:flex;flex-direction:column;background:#fff;box-shadow:2px 0 5px rgba(0,0,0,.05)}.own-profile-card{padding:1.25rem;border-bottom:1px solid #e2e8f0;background:linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);display:flex;flex-direction:column;gap:.75rem}.own-profile-card .profile-header{display:flex;align-items:center;gap:1rem}.own-profile-card .profile-info{display:flex;flex-direction:column;flex:1;overflow:hidden}.own-profile-card .profile-info .profile-name{font-weight:700;color:#1e293b;font-size:1.1rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.own-profile-card .profile-info .profile-status{font-size:.85rem;color:#10b981;font-weight:500;display:flex;align-items:center;gap:.35rem}.own-profile-card .profile-info .profile-status::before{content:"";display:inline-block;width:8px;height:8px;background-color:#10b981;border-radius:50%}.own-profile-card .own-identity-select-container{display:flex;flex-direction:column;gap:.25rem}.own-profile-card .own-identity-select-container label{font-size:.75rem;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:.05em}.own-profile-card .own-identity-select-container select.own-identity-select{width:100%;padding:.375rem .5rem;font-size:.85rem;border:1px solid #cbd5e1;border-radius:.375rem;background-color:#fff;color:#334155;outline:none;cursor:pointer;transition:border-color .2s}.own-profile-card .own-identity-select-container select.own-identity-select:focus{border-color:#3ba4d7}.friends-list-container{flex:1;display:flex;flex-direction:column;overflow:hidden}.friends-list-container .searchbar-container{padding:.75rem 1rem;border-bottom:1px solid #e2e8f0}.friends-list-container .searchbar-container input.searchbar{width:100%;padding:.5rem .75rem;font-size:.9rem;border:1px solid #cbd5e1;border-radius:.375rem;background-color:#f8fafc;outline:none;transition:all .2s}.friends-list-container .searchbar-container input.searchbar:focus{background-color:#fff;border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.friends-list-container .friends-scroll{flex:1;overflow-y:auto;padding:.5rem 0}.friend-list-item{display:flex;align-items:center;gap:.75rem;padding:.75rem 1rem;margin:.125rem .5rem;border-radius:.5rem;cursor:pointer;transition:all .2s}.friend-list-item:hover{background-color:#f1f5f9}.friend-list-item.selected{background-color:#e0f2fe}.friend-list-item.selected .friend-name{color:#0369a1;font-weight:600}.friend-list-item .friend-avatar{flex-shrink:0}.friend-list-item .friend-meta{flex:1;min-width:0}.friend-list-item .friend-meta .friend-name{font-size:.95rem;color:#334155;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .2s}.friend-list-item .friend-meta .friend-status{font-size:.8rem;color:#94a3b8}.friend-list-item .friend-meta .friend-status.online{color:#10b981;font-weight:500}.network-right-pane{flex:1;display:flex;flex-direction:column;overflow:hidden;background-color:#f8fafc}.network-pane-placeholder{flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;color:#94a3b8;gap:1rem;padding:2rem;text-align:center}.network-pane-placeholder i{font-size:4rem;color:#cbd5e1}.network-pane-placeholder p{font-size:1.1rem;max-width:400px}.network-tabs{display:flex;background-color:#fff;border-bottom:1px solid #cbd5e1;padding:.5rem 1rem 0;gap:.5rem}.network-tabs .tab-btn{padding:.625rem 1.25rem;font-size:.95rem;font-weight:600;color:#64748b;background:rgba(0,0,0,0);border:none;border-radius:.375rem .375rem 0 0;border-bottom:3px solid rgba(0,0,0,0);cursor:pointer;box-shadow:none;transition:all .2s}.network-tabs .tab-btn:hover{color:#334155;background-color:#f1f5f9}.network-tabs .tab-btn.active{color:#3ba4d7;border-bottom-color:#3ba4d7;background-color:rgba(0,0,0,0)}.network-tab-content{flex:1;overflow-y:auto;padding:1.5rem}.network-detail-view{display:flex;flex-direction:column;gap:1.5rem}.network-detail-view .detail-header{display:flex;align-items:center;gap:1.5rem;padding-bottom:1.5rem;border-bottom:1px solid #e2e8f0}.network-detail-view .detail-header .detail-title{flex:1}.network-detail-view .detail-header .detail-title h2{font-size:1.75rem;font-weight:800;color:#1e293b;margin-bottom:.25rem}.network-detail-view .detail-header .detail-title .detail-subtitle{font-size:.9rem;color:#64748b;display:flex;align-items:center;gap:.5rem}.network-detail-view .detail-header .detail-actions{display:flex;gap:.75rem}.network-detail-view .detail-header .detail-actions button{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.9rem}.network-detail-view .detail-section{background-color:#fff;border-radius:.5rem;border:1px solid #e2e8f0;padding:1.25rem;box-shadow:0 1px 3px rgba(0,0,0,.02)}.network-detail-view .detail-section h3{font-size:1.1rem;font-weight:700;color:#334155;margin-bottom:1rem;padding-bottom:.5rem;border-bottom:1px solid #f1f5f9}.network-detail-view .detail-section .info-grid{display:grid;grid-template-columns:120px 1fr;row-gap:.75rem;font-size:.9rem}.network-detail-view .detail-section .info-grid .info-label{font-weight:600;color:#64748b}.network-detail-view .detail-section .info-grid .info-value{color:#1e293b;word-break:break-all}.network-detail-view .locations-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));gap:1rem}.network-detail-view .location-card{background-color:#fff;border:1px solid #e2e8f0;border-radius:.5rem;padding:1rem;display:flex;flex-direction:column;gap:.5rem;box-shadow:0 1px 3px rgba(0,0,0,.02)}.network-detail-view .location-card .loc-header{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #f1f5f9;padding-bottom:.5rem;margin-bottom:.25rem}.network-detail-view .location-card .loc-header .loc-name{font-weight:700;color:#334155;font-size:.95rem}.network-detail-view .location-card .loc-header .loc-status{font-size:.75rem;font-weight:600;padding:.125rem .5rem;border-radius:.25rem}.network-detail-view .location-card .loc-header .loc-status.online{background-color:#d1fae5;color:#065f46}.network-detail-view .location-card .loc-header .loc-status.offline{background-color:#f1f5f9;color:#475569}.network-detail-view .location-card .loc-body{font-size:.85rem;display:grid;grid-template-columns:80px 1fr;row-gap:.25rem}.network-detail-view .location-card .loc-body .loc-label{color:#64748b}.network-detail-view .location-card .loc-body .loc-val{color:#334155;word-break:break-all}.network-detail-view .location-card .loc-footer{margin-top:.5rem;display:flex;justify-content:flex-end}.network-detail-view .location-card .loc-footer button{font-size:.8rem;padding:.25rem .75rem}.network-chat-view{display:flex;flex-direction:column;height:100%;overflow:hidden;background-color:#f8fafc}.network-chat-view .chat-messages{flex:1;overflow-y:auto;padding:1.25rem;display:flex;flex-direction:column;gap:1rem}.network-chat-view .chat-bubble-container{display:flex;flex-direction:column;max-width:70%}.network-chat-view .chat-bubble-container.outgoing{align-self:flex-end;align-items:flex-end}.network-chat-view .chat-bubble-container.outgoing .chat-bubble{background-color:#3ba4d7;color:#fff;border-bottom-right-radius:.125rem}.network-chat-view .chat-bubble-container.incoming{align-self:flex-start;align-items:flex-start}.network-chat-view .chat-bubble-container.incoming .chat-bubble{background-color:#fff;color:#1e293b;border:1px solid #e2e8f0;border-bottom-left-radius:.125rem}.network-chat-view .chat-bubble-container .chat-sender{font-size:.75rem;color:#64748b;margin-bottom:.25rem;padding:0 .25rem}.network-chat-view .chat-bubble-container .chat-bubble{padding:.625rem .875rem;border-radius:.75rem;font-size:.925rem;line-height:1.4;white-space:break-spaces;word-break:break-word;box-shadow:0 1px 2px rgba(0,0,0,.05)}.network-chat-view .chat-bubble-container .chat-time{font-size:.7rem;color:#94a3b8;margin-top:.25rem;padding:0 .25rem}.network-chat-view .chat-input-area{padding:1rem;background-color:#fff;border-top:1px solid #cbd5e1;display:flex;gap:.75rem;align-items:center}.network-chat-view .chat-input-area textarea.chat-textarea{flex:1;resize:none;height:40px;padding:.5rem .75rem;border:1px solid #cbd5e1;border-radius:.375rem;font-size:.9rem;outline:none;transition:all .2s}.network-chat-view .chat-input-area textarea.chat-textarea:focus{border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.network-chat-view .chat-input-area button.send-btn{padding:.5rem 1.25rem;font-size:.9rem;height:40px;display:flex;align-items:center;gap:.5rem}.network-chat-view .chat-warning{flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;color:#64748b;text-align:center;padding:2rem;gap:1rem}.network-chat-view .chat-warning i{font-size:3rem;color:#cbd5e1}.network-chat-view .chat-warning h4{font-weight:700;color:#334155}.network-chat-view .chat-warning p{max-width:350px;font-size:.9rem}.identity{color:#444;font-size:1.1em;margin:20px;padding:10px;border:1px solid #aaa;border-radius:20px}.identity>h4{margin:5px;font-size:1.3em}.identity button{font-size:.9em}.identity .details{display:grid;grid-template-columns:140px auto;grid-row-gap:5px;justify-content:left}.defaultAvatar{width:3rem;height:3rem;aspect-ratio:1;background:#b0c4de;border-radius:50%;display:grid;place-items:center}.defaultAvatar p{font-weight:900;color:#666f7f;transform:translateY(1px)}img.avatar{display:block;width:3rem;height:max-content;aspect-ratio:1;margin-right:.3em;border-radius:50%}.counter{margin-left:.5em}.counter:before{content:"("}.counter:after{content:")"}.chatInit{margin-left:.5em;color:green;cursor:pointer}.people-sidebar-header{display:flex;flex-direction:column;padding:.75rem 1rem .5rem 1rem;gap:.75rem;border-bottom:1px solid #e2e8f0;background-color:#fff}.people-sidebar-header .searchbar-wrapper{position:relative;display:flex;align-items:center}.people-sidebar-header .searchbar-wrapper i.fa-search{position:absolute;left:.85rem;color:#94a3b8;font-size:.9rem}.people-sidebar-header .searchbar-wrapper input.searchbar-input{width:100%;padding:.5rem .75rem .5rem 2.25rem;border:1px solid #e2e8f0;border-radius:.5rem;font-size:.9rem;background-color:#f8fafc;color:#1e293b;outline:none;transition:all .2s ease}.people-sidebar-header .searchbar-wrapper input.searchbar-input:focus{border-color:#3b82f6;background-color:#fff;box-shadow:0 0 0 3px rgba(59,130,246,.1)}.people-sidebar-header .segmented-control{display:flex;background-color:#f1f5f9;padding:3px;border-radius:.5rem;gap:4px}.people-sidebar-header .segmented-control button.segment-tab{flex:1;display:flex;align-items:center;justify-content:center;gap:.5rem;padding:.5rem .75rem;font-size:.9rem;font-weight:600;color:#64748b;background:rgba(0,0,0,0);border:none;border-radius:.375rem;cursor:pointer;box-shadow:none;transition:all .2s ease}.people-sidebar-header .segmented-control button.segment-tab:hover{color:#1e293b}.people-sidebar-header .segmented-control button.segment-tab.active{background-color:#fff;color:#0f172a;box-shadow:0 1px 3px rgba(0,0,0,.1),0 1px 2px rgba(0,0,0,.06)}.people-sidebar-header .segmented-control button.segment-tab.active .segment-badge{background-color:#019dff;color:#fff}.people-sidebar-header .segmented-control button.segment-tab .segment-badge{display:inline-flex;align-items:center;justify-content:center;background-color:#cbd5e1;color:#334155;font-size:.75rem;font-weight:700;min-width:1.25rem;height:1.25rem;padding:0 .35rem;border-radius:9999px;line-height:1;transition:all .2s ease}.people-sidebar-header .sub-filter-row{display:flex;align-items:center;justify-content:space-between;gap:.5rem;min-height:32px}.people-sidebar-header .sub-filter-row select.filter-select{padding:.35rem .6rem;border:1px solid #cbd5e1;border-radius:.375rem;font-size:.85rem;font-weight:600;color:#475569;background-color:#fff;cursor:pointer;outline:none}.people-sidebar-header .sub-filter-row .btn-add-id{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:.375rem;background-color:#3b82f6;color:#fff;border:none;cursor:pointer;font-size:.9rem;transition:background-color .2s}.people-sidebar-header .sub-filter-row .btn-add-id:hover{background-color:#2563eb}.lobby{margin:10px;border:1px solid #aaa;border-radius:20px}.lobby .mainname{margin:20px;font-weight:100;font-size:1.2em}.topic{color:#666}.lobby>.topic{font-size:.95em;margin-left:25px;margin-bottom:5px}.lefttitle{margin-top:15px;margin-bottom:0;font-weight:100;font-size:1.2em}.leftname{margin-top:5px;margin-bottom:5px;padding:5px;font-weight:100;font-size:1em}.leftlobby>.topic{font-size:.75em;margin-left:15px;margin-bottom:5px}.subscribed,.public{cursor:pointer}.leftlobby{border:1px solid #aaa;border-radius:10px;margin-top:5px;background-color:#fff}.leftlobby.selected-lobby,.selectedidentity{color:#fff;background-color:#3ba4d7}.rightbar{position:absolute;width:185px;background-color:#fff;overflow:auto;top:130px;bottom:15px;right:15px}.user{padding:5px}.lobbyName{padding:15px;margin-top:2rem}.lobbies{position:absolute;width:185px;left:165px;bottom:15px;top:130px;overflow:auto}.messages,.setup{position:absolute;background-color:#fff;top:130px;left:360px;right:215px;overflow:auto}.messages{bottom:115px}.messagetext{white-space:break-spaces;margin-right:5px}.message>*{margin-left:5px}.username{color:#006400;font-weight:bolder}.chatMessage{position:absolute;background-color:#fff;height:85px;bottom:15px;right:215px;left:360px}textarea.chatMsg{height:100%;width:100%}.chatatchar{margin-left:.2em;margin-right:.2em;color:silver}.setupicon{margin-left:1em;cursor:pointer}.leaveicon{margin-left:1em;cursor:pointer;color:#d40000}.selectidentity{margin:15px;font-size:1.2em}.setup>.identity{cursor:pointer}.setup{bottom:15px}.createDistantChat{margin-top:1em}.no-lobbies .messages,.no-lobbies .chatMessage,.no-lobbies .setup{left:165px}@media(min-width: 900px){.node-panel.chat-room{display:grid !important;grid-template-columns:250px 1fr 200px !important;grid-template-rows:auto 1fr auto !important;grid-template-areas:"lobbies header rightbar" "lobbies messages rightbar" "lobbies input rightbar" !important;padding:0 !important;height:100% !important}.node-panel.chat-room .lobbyName{grid-area:header;padding:10px;border-bottom:1px solid #eee;margin:0;z-index:10;background:#fff}.node-panel.chat-room .lobbies{grid-area:lobbies;position:static !important;width:auto !important;height:auto !important;border-right:1px solid #ccc;overflow-y:auto;display:block !important;top:auto !important;bottom:auto !important;left:auto !important}.node-panel.chat-room .messages{grid-area:messages;position:static !important;width:auto !important;height:auto !important;overflow-y:auto;padding:10px;left:auto !important;right:auto !important;top:auto !important;bottom:auto !important;margin:0 !important}.node-panel.chat-room .rightbar{grid-area:rightbar;position:static !important;width:auto !important;border-left:1px solid #ccc;overflow-y:auto;display:block !important}.node-panel.chat-room .chatMessage{grid-area:input;position:static !important;width:auto !important;height:auto !important;border-top:1px solid #eee;left:auto !important;right:auto !important;bottom:auto !important;flex:0 0 auto;padding:10px !important;background:#fff;z-index:10}}@media(max-width: 899px){.node-panel.chat-room{display:flex !important;flex-direction:column !important;height:100% !important;position:relative !important}.node-panel.chat-room .lobbyName{flex:0 0 auto}.node-panel.chat-room .messages{flex:1 !important;overflow-y:auto !important;position:relative !important;top:0 !important;bottom:0 !important;left:0 !important;right:0 !important;width:100% !important;height:auto !important;margin:0 !important}.node-panel.chat-room .chatMessage{flex:0 0 auto !important;position:relative !important;bottom:0 !important;left:0 !important;right:0 !important;width:100% !important;height:auto !important;z-index:100}.node-panel.chat-room .rightbar,.node-panel.chat-room .lobbies{display:none !important;position:fixed !important;top:60px !important;bottom:0 !important;width:80% !important;background:#fff !important;z-index:200 !important;box-shadow:2px 0 10px rgba(0,0,0,.2) !important}.node-panel.chat-room.show-lobbies .lobbies{display:block !important;left:0 !important}.node-panel.chat-room.show-users .rightbar{display:block !important;right:0 !important}.chat-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.4);z-index:150}.show-lobbies .chat-overlay,.show-users .chat-overlay{display:block}.mobile-menu-icons{display:flex;gap:15px;font-size:1.2rem}.mobile-menu-icons i{cursor:pointer;padding:5px}}@media(min-width: 900px){.mobile-menu-icons{display:none}}.chat-hub-container{display:flex;height:100%;width:100%;overflow:hidden;background-color:#f1f5f9}.chat-hub-left-pane{width:320px;min-width:300px;max-width:350px;border-right:1px solid #cbd5e1;display:flex;flex-direction:column;background:#fff;box-shadow:2px 0 5px rgba(0,0,0,.05)}.chat-own-profile-card{padding:1.25rem;border-bottom:1px solid #e2e8f0;background:linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)}.chat-own-profile-card .profile-header{display:flex;align-items:center;gap:1rem}.chat-own-profile-card .profile-info{display:flex;flex-direction:column;flex:1;overflow:hidden}.chat-own-profile-card .profile-info .profile-name{font-weight:700;color:#1e293b;font-size:1.1rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-own-profile-card .profile-info .profile-status{font-size:.85rem;color:#10b981;font-weight:500;display:flex;align-items:center;gap:.35rem}.chat-own-profile-card .profile-info .profile-status::before{content:"";display:inline-block;width:8px;height:8px;background-color:#10b981;border-radius:50%}.chat-rooms-list-container{flex:1;display:flex;flex-direction:column;overflow:hidden}.chat-rooms-list-container .searchbar-container{padding:.75rem 1rem;border-bottom:1px solid #e2e8f0}.chat-rooms-list-container .searchbar-container input.searchbar{width:100%;padding:.5rem .75rem;font-size:.9rem;border:1px solid #cbd5e1;border-radius:.375rem;background-color:#f8fafc;outline:none;transition:all .2s}.chat-rooms-list-container .searchbar-container input.searchbar:focus{background-color:#fff;border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.chat-rooms-list-container .rooms-scroll{flex:1;overflow-y:auto;padding:.5rem 0}.rooms-section-title{display:flex;align-items:center;gap:.5rem;padding:.75rem 1rem .375rem;font-size:.75rem;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.05em}.rooms-section-title i{font-size:.7rem;color:#94a3b8}.chat-room-list-item{display:flex;align-items:center;gap:.75rem;padding:.75rem 1rem;margin:.125rem .5rem;border-radius:.5rem;cursor:pointer;transition:all .2s}.chat-room-list-item:hover{background-color:#f1f5f9}.chat-room-list-item.selected{background-color:#e0f2fe}.chat-room-list-item.selected .room-name{color:#0369a1;font-weight:600}.chat-room-list-item .room-icon{flex-shrink:0;width:36px;height:36px;border-radius:.5rem;background:linear-gradient(135deg, #3ba4d7, #0ea5e9);display:flex;align-items:center;justify-content:center;color:#fff;font-size:1.35rem}.chat-room-list-item.public-room .room-icon{background:linear-gradient(135deg, #10b981, #059669)}.chat-room-list-item .room-meta{flex:1;min-width:0}.chat-room-list-item .room-meta .room-name{font-size:.95rem;color:#334155;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .2s}.chat-room-list-item .room-meta .room-topic{font-size:.8rem;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-room-list-item .room-badge{flex-shrink:0;min-width:24px;height:24px;border-radius:12px;background-color:#e2e8f0;color:#475569;font-size:.75rem;font-weight:700;display:flex;align-items:center;justify-content:center;padding:0 .375rem}.chat-hub-right-pane{flex:1;display:flex;flex-direction:column;overflow:hidden;background-color:#f8fafc}.chat-pane-placeholder{flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;color:#94a3b8;gap:1rem;padding:2rem;text-align:center}.chat-pane-placeholder i{font-size:4rem;color:#cbd5e1}.chat-pane-placeholder p{font-size:1.1rem;max-width:400px}.chat-hub-tab-content{flex:1;overflow-y:auto;padding:1.5rem}.chat-room-detail-view{display:flex;flex-direction:column;gap:1.5rem}.chat-room-detail-view .detail-header{display:flex;align-items:flex-start;gap:1.5rem;padding-bottom:1.5rem;border-bottom:1px solid #e2e8f0;flex-wrap:wrap}.chat-room-detail-view .detail-header .detail-title{flex:1;min-width:200px}.chat-room-detail-view .detail-header .detail-title h2{font-size:1.75rem;font-weight:800;color:#1e293b;margin-bottom:.25rem}.chat-room-detail-view .detail-header .detail-title .detail-subtitle{font-size:.9rem;color:#64748b;display:flex;align-items:center;gap:.5rem}.chat-room-detail-view .detail-header .detail-actions{display:flex;gap:.75rem;flex-wrap:wrap}.chat-room-detail-view .detail-header .detail-actions button{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.9rem}.chat-room-detail-view .detail-section{background-color:#fff;border-radius:.5rem;border:1px solid #e2e8f0;padding:1.25rem;box-shadow:0 1px 3px rgba(0,0,0,.02)}.chat-room-detail-view .detail-section h3{font-size:1.1rem;font-weight:700;color:#334155;margin-bottom:1rem;padding-bottom:.5rem;border-bottom:1px solid #f1f5f9}.chat-room-detail-view .detail-section .info-grid{display:grid;grid-template-columns:130px 1fr;row-gap:.75rem;font-size:.9rem}.chat-room-detail-view .detail-section .info-grid .info-label{font-weight:600;color:#64748b}.chat-room-detail-view .detail-section .info-grid .info-value{color:#1e293b;word-break:break-all}.participants-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(180px, 1fr));gap:.5rem}.participant-card{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;background-color:#f8fafc;border:1px solid #e2e8f0;border-radius:.375rem}.participant-card .participant-name{font-size:.875rem;color:#334155;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.no-participants{color:#94a3b8;font-size:.9rem;font-style:italic}.detail-actions-footer{display:flex;gap:.75rem}.detail-actions-footer button{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.9rem}.join-description{color:#64748b;font-size:.9rem;margin-bottom:1rem}.identities-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(200px, 1fr));gap:.75rem}.identity-card{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1rem;background-color:#f8fafc;border:1px solid #e2e8f0;border-radius:.5rem;cursor:pointer;transition:all .2s}.identity-card:hover{background-color:#e0f2fe;border-color:#3ba4d7}.identity-card .identity-name{font-size:.95rem;font-weight:600;color:#334155}.identity-card i{color:#3ba4d7;font-size:.9rem}.no-rooms{padding:1rem;color:#94a3b8;text-align:center;font-style:italic}@media(max-width: 899px){.chat-hub-container{flex-direction:column}.chat-hub-left-pane{width:100%;min-width:0;max-width:none;max-height:45%;border-right:none;border-bottom:1px solid #cbd5e1}.chat-hub-right-pane{flex:1;min-height:0}}.chat-hub-header-bar{padding:.75rem 1.5rem;background-color:#fff;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;justify-content:space-between;height:65px;flex-shrink:0}.chat-hub-header-bar .chat-header-info{display:flex;flex-direction:column;overflow:hidden}.chat-hub-header-bar .chat-header-info .chat-header-name{font-size:1.15rem;font-weight:800;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-hub-header-bar .chat-header-info .chat-header-topic{font-size:.85rem;color:#64748b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:.125rem}.chat-hub-header-bar .chat-header-actions{display:flex;gap:.5rem}.chat-hub-header-bar .chat-header-actions button{display:flex;align-items:center;gap:.35rem;padding:.375rem .75rem;font-size:.85rem}.chat-hub-tabs-container{background-color:#fff;border-bottom:1px solid #cbd5e1;padding:.5rem 1.5rem 0}.chat-hub-tabs{display:flex;gap:.5rem}.chat-hub-tabs .tab-btn{padding:.625rem 1.25rem;font-size:.95rem;font-weight:600;color:#64748b;background:rgba(0,0,0,0);border:none;border-radius:.375rem .375rem 0 0;border-bottom:3px solid rgba(0,0,0,0);cursor:pointer;box-shadow:none;transition:all .2s;display:flex;align-items:center;gap:.5rem}.chat-hub-tabs .tab-btn:hover{color:#334155;background-color:#f1f5f9}.chat-hub-tabs .tab-btn.active{color:#3ba4d7;border-bottom-color:#3ba4d7;background-color:rgba(0,0,0,0)}.chat-hub-tab-content{flex:1;display:flex;flex-direction:column;overflow:hidden;background-color:#f8fafc}.chat-hub-conversation-layout{display:flex;flex-direction:row;height:100%;width:100%;overflow:hidden}.chat-hub-conversation-main{display:flex;flex-direction:column;flex:1;height:100%;overflow:hidden}.chat-hub-rightbar{width:200px;border-left:1px solid #cbd5e1;background-color:#fff;display:flex;flex-direction:column;flex-shrink:0}.chat-hub-rightbar .rightbar-title{padding:.75rem 1rem;font-size:.85rem;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid #e2e8f0}.chat-hub-rightbar .rightbar-users-list{flex:1;overflow-y:auto;padding:.5rem}.chat-hub-rightbar .user{padding:.5rem .75rem;font-size:.9rem;color:#334155;border-radius:.375rem;transition:all .2s;display:flex;align-items:center;gap:.5rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-hub-rightbar .user:hover{background-color:#f1f5f9;color:#0f172a}.chat-hub-rightbar .user .defaultAvatar{width:2rem;height:2rem;font-size:.9rem;flex-shrink:0}.chat-hub-rightbar .user img.avatar{width:2rem;height:2rem;flex-shrink:0}@media(max-width: 899px){.chat-hub-rightbar{display:none}}.chat-hub-messages{flex:1;overflow-y:auto;padding:1.25rem 1.5rem;display:flex;flex-direction:column;gap:1rem}.chat-hub-messages .message{display:flex;flex-direction:column;max-width:70%;padding:.625rem .875rem;border-radius:.75rem;font-size:.925rem;line-height:1.4;word-break:break-word;box-shadow:0 1px 2px rgba(0,0,0,.05)}.chat-hub-messages .message.incoming{align-self:flex-start;align-items:flex-start;background-color:#fff;color:#1e293b;border:1px solid #e2e8f0;border-bottom-left-radius:.125rem}.chat-hub-messages .message.outgoing{align-self:flex-end;align-items:flex-end;background-color:#3ba4d7;color:#fff;border-bottom-right-radius:.125rem}.chat-hub-messages .message .username{font-size:.75rem;margin-bottom:.25rem;padding:0 .125rem;font-weight:700}.chat-hub-messages .message.incoming .username{color:#0369a1}.chat-hub-messages .message.outgoing .username{color:#e0f2fe}.chat-hub-messages .message .messagetext{white-space:break-spaces;margin:0}.chat-hub-messages .message .datetime{font-size:.7rem;margin-top:.25rem;padding:0 .125rem;opacity:.8}.chat-hub-messages .message.incoming .datetime{color:#64748b}.chat-hub-messages .message.outgoing .datetime{color:#f1f5f9}.chat-hub-input-area{padding:.75rem 1.5rem;background-color:#fff;border-top:1px solid #cbd5e1;display:flex;gap:.75rem;align-items:flex-end;flex-shrink:0}.chat-hub-input-area textarea.chat-hub-textarea{flex:1;resize:vertical;min-height:40px;max-height:250px;height:40px;padding:.5rem .75rem;border:1px solid #cbd5e1;border-radius:.375rem;font-size:.9rem;outline:none;transition:border-color .2s,box-shadow .2s;background-color:#f8fafc}.chat-hub-input-area textarea.chat-hub-textarea:focus{background-color:#fff;border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.chat-hub-input-area button.chat-hub-send-btn{padding:.5rem 1.25rem;font-size:.9rem;height:40px;display:flex;align-items:center;gap:.5rem;border-radius:.375rem}.chat-hub-messages.compact-container,.messages.compact-container{gap:0 !important;padding:.75rem 1rem !important;background-color:#fff !important;display:flex !important;flex-direction:column !important}.chat-hub-messages.compact-container .message.compact,.messages.compact-container .message.compact{display:block !important;max-width:100% !important;padding:.1rem 0 !important;border-radius:0 !important;background-color:rgba(0,0,0,0) !important;border:none !important;box-shadow:none !important;align-self:flex-start !important;font-size:.875rem !important;line-height:1.45 !important;margin:0 !important;white-space:nowrap !important}.chat-hub-messages.compact-container .message.compact:hover,.messages.compact-container .message.compact:hover{background-color:#f8fafc !important;overflow:visible !important;white-space:normal !important}.chat-hub-messages.compact-container .message.compact .datetime,.messages.compact-container .message.compact .datetime{color:#a0a0a0 !important;margin-right:.4rem !important;font-size:.78rem !important;font-family:monospace !important;opacity:1 !important;display:inline !important}.chat-hub-messages.compact-container .message.compact .username,.messages.compact-container .message.compact .username{font-weight:bold !important;margin-right:.2rem !important;font-size:.875rem !important;display:inline !important}.chat-hub-messages.compact-container .message.compact .messagetext,.messages.compact-container .message.compact .messagetext{color:#1e293b !important;white-space:normal !important;word-break:break-word !important;display:inline !important;margin:0 !important}.side-bar{display:flex;flex-direction:column;background:#fff}.side-bar .mail-compose-btn{width:96%;margin:.25rem;padding:.75rem 0}.compose-mail__from{display:flex;justify-content:flex-start;align-items:center;gap:.5rem;padding-bottom:.5rem;border-bottom:2px solid #eef3f6}.compose-mail__recipients{padding:.5rem 0;display:flex;flex-direction:column;gap:.5rem;border-bottom:2px solid #eef3f6}.compose-mail__recipients__container{display:flex;gap:.5rem}.compose-mail__recipients__container>label{text-transform:capitalize}.compose-mail__recipients__container .recipients{width:100%;display:flex;gap:.5rem;flex-wrap:wrap}.compose-mail__recipients__container .recipients__selected{padding:.125rem .5rem;display:flex;align-items:center;gap:.5rem;border:1px solid #eef3f6;border-radius:3px;cursor:default}.compose-mail__recipients__container .recipients__selected i{cursor:pointer;padding:.25rem}.compose-mail__recipients__container .recipients__input{display:flex;position:relative;flex-grow:1}.compose-mail__recipients__container .recipients__input-field{flex-grow:1;min-width:200px;padding:0;border:none;box-shadow:none}.compose-mail__recipients__container .recipients__input-field:focus+.recipients__input-list{display:flex}.compose-mail__recipients__container .recipients__input-list{z-index:1;position:absolute;top:1rem;padding:0;width:100%;max-height:15rem;flex-direction:column;overflow:auto;display:none;background:#fff;border-top:1px solid #eef3f6;border-bottom:1px solid #eef3f6}.compose-mail__recipients__container .recipients__input-list:hover{display:flex}.compose-mail__recipients__container .recipients__input-list li{list-style:none;padding:.25rem .5rem;cursor:pointer;background:#fff;border:1px solid #eef3f6;border-top:0px}.compose-mail__recipients__container .recipients__input-list li:hover{background:#eef3f6}.compose-mail__recipients__container .recipients__input-list li:last-child{border-bottom:0px}.compose-mail__recipients .remove-recipient{padding:.125rem .5rem}.compose-mail input[type=text].compose-mail__subject{padding:.5rem 0;border:none;box-shadow:none;border-bottom:2px solid #eef3f6;border-radius:0}.compose-mail__message{margin:.5rem 0;height:100%;display:flex;flex-direction:column;overflow:auto}.compose-mail__message-body{height:100%;outline:rgba(0,0,0,0)}.compose-mail__send-btn{display:flex;align-items:center;gap:.5rem}.compose-mail__send-btn i{transform:translateY(-1px)}.msg-view{height:100%;display:flex;flex-direction:column;gap:1rem;overflow:auto}.msg-view-nav{display:flex;justify-content:space-between;align-items:column}.msg-view-nav__action{display:flex;gap:.5rem}.msg-view__header{display:flex;flex-direction:column;gap:1rem}.msg-view__header>h3{line-height:1}.msg-view__header .msg-details{display:flex;gap:1rem}.msg-view__header .msg-details__avatar{height:max-content}.msg-view__header .msg-details__info{display:flex;flex-direction:column}.msg-view__header .msg-details__info-item{display:flex;gap:.5rem}.msg-view__body{height:100%;overflow:auto;font-size:14px !important}.msg-view__attachment{height:50%;overflow:auto;display:flex;flex-direction:column}.msg-view__attachment-items{height:100%;overflow:auto}.mail-tag{width:8rem;padding:.5rem}.msgHeader{display:flex}.msgHeaderDetails{display:flex;flex-direction:column}table.mails th:nth-child(1){width:5%;color:#fcba03}table.mails th:nth-child(2){width:5%;color:hsl(202.5,30.7692307692%,44.9019607843%)}table.mails th:nth-child(3){width:50%;text-align:start}table.mails th:nth-child(4),table.mails th:nth-child(5){width:20%;text-align:start}table.mails td:nth-child(3){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.mails td:nth-child(4),table.mails td:nth-child(5){text-align:start}table.mails tr:hover{background-color:#eef3f6;cursor:pointer}table.mails tr.unread{color:#000;background-color:#eef3f6}table.mails>tr:hover{cursor:auto;background-color:#fff}table.mails th.sortable-th{cursor:pointer;user-select:none;transition:background-color .2s,color .2s}table.mails th.sortable-th:hover{background-color:#eef3f6;color:hsl(202.5,30.7692307692%,14.9019607843%)}input.star-check{display:none}input.star-check+label.star-check{color:gray}input.star-check:checked+label.star-check{color:#fcba03}#truncate{height:6rem;overflow:auto}#truncate.truncated-view{height:1.75rem;overflow:hidden}.toggle-truncate{font-size:.75rem;padding:0 .25rem;background:#999;color:#14141b;box-shadow:none;border-radius:2px}table.attachment-container{padding:0}table.attachment-container>tr{border:0}table.attachment-container .attachment-header{width:100%;display:flex;justify-content:space-between}table.attachment-container .attachment-header th{text-align:start}table.attachment-container .attachment-header th:nth-child(1){flex-basis:45%}table.attachment-container .attachment-header th:nth-child(2){flex-basis:15%}table.attachment-container .attachment-header th:nth-child(3){flex-basis:10%}table.attachment-container .attachment-header th:nth-child(4){flex-basis:20%}table.attachment-container .attachment-header th:nth-child(5){text-align:center;flex-basis:10%}table.attachment-container .attachment{width:100%;display:flex;justify-content:space-between;text-align:start}table.attachment-container .attachment__name{flex-basis:45%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}table.attachment-container .attachment__name span{margin-left:8px}table.attachment-container .attachment__from{flex-basis:15%}table.attachment-container .attachment__size{flex-basis:10%}table.attachment-container .attachment__date{flex-basis:20%}table.attachment-container .attachment td:nth-child(5){display:flex;justify-content:center;align-items:center;flex-basis:10%}table.attachment-container .attachment td:nth-child(5) button{font-size:.875rem}.view-toggle{height:max-content;border:1px solid #019dff;border-radius:4px;display:flex}.view-toggle *{padding:4px 12px;border-radius:4px}.composePopupOverlay{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1;background-color:rgba(0,0,0,.2)}.composePopupOverlay .composePopup{position:absolute;inset:0;margin:auto;width:80%;height:90%}.composePopupOverlay .composePopup>.widget{padding:2rem}.composePopupOverlay .composePopup .close-btn{position:absolute;top:1.5rem;right:1.5rem}.file-view{width:100%;padding:1rem;margin-top:1.5rem;border-radius:8px;border:1px solid #ccc;animation:fadein .5s}.file-view__heading{display:flex;justify-content:space-between;margin-bottom:.5rem}.file-view__heading-chunk{display:flex;gap:1rem}.file-view__body{display:flex;flex-direction:column;gap:1rem}.file-view__body-details{display:flex;align-items:center}.file-view__body-details-stat{width:100%;display:grid;grid-template-columns:repeat(5, 1fr)}.file-view__body-details-stat span>i{margin-right:.5rem}.file-view__body-details-action{display:flex;gap:1rem;height:100%}.file-view__body-details-action button,.file-view__body-details-action button.red{padding:.25rem .75rem}table.myfiles td{word-wrap:break-word}table.myfiles th:nth-child(1){width:2%}table.myfiles th:nth-child(2){width:50%}table.myfiles td:nth-child(2){text-align:start}table.friendsfiles td{word-wrap:break-word}table.friendsfiles th:nth-child(1){width:2%}table.friendsfiles th:nth-child(2){width:50%}table.friendsfiles th:nth-child(4){width:40%}table.friendsfiles td:nth-child(2){text-align:start}.file-search-container{margin-top:1rem;padding:8px;display:flex;gap:8px;border:1px solid rgba(20,20,27,.2);border-radius:6px;height:100%;overflow:auto}.file-search-container__keywords{flex-basis:15%;padding-right:.25rem;border-right:1px solid rgba(20,20,27,.1)}.file-search-container__keywords .keywords-container{display:flex;flex-direction:column;border-top:2.5px solid rgba(20,20,27,.08);margin-top:.125rem;padding-top:.25rem}.file-search-container__keywords .keywords-container a{font-size:1.2rem;text-decoration:none;color:#14141b}.file-search-container__keywords .keywords-container a.selected{color:#019dff}.file-search-container__results{flex-basis:85%;height:100%;overflow:auto}.file-search-container__results .results-container .results-header tr{display:flex}.file-search-container__results .results-container .results-header tr th{font-size:1.25rem;font-weight:bold;text-align:left}.file-search-container__results .results-container .results-header tr th:nth-child(1){flex-basis:40%}.file-search-container__results .results-container .results-header tr th:nth-child(2){flex-basis:10%;text-align:center}.file-search-container__results .results-container .results-header tr th:nth-child(3){flex-basis:40%}.file-search-container__results .results-container .results-header tr th:nth-child(4){flex-basis:10%}.file-search-container__results .results-container .results{height:100%;overflow:auto}.file-search-container__results .results-container .results tr{display:flex}.file-search-container__results .results-container .results tr .results__hash,.file-search-container__results .results-container .results tr .results__name{text-align:left;flex-basis:40%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.file-search-container__results .results-container .results tr .results__hash span,.file-search-container__results .results-container .results tr .results__name span{margin-left:8px}.file-search-container__results .results-container .results tr .results__size{flex-basis:10%}.file-search-container__results .results-container .results tr .results__download{flex-basis:10%;display:flex;justify-content:start;align-items:center}.search-form{display:flex;width:40%}.search-form input{width:100%}.search-form button{margin-left:.5rem}.shareManagerPopupOverlay{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1;background-color:rgba(0,0,0,.2)}.shareManagerPopupOverlay .shareManagerPopup{position:absolute;inset:0;margin:auto;width:80%;height:90%}.shareManagerPopupOverlay .shareManagerPopup>.widget{padding:1.5rem}.shareManagerPopupOverlay .shareManagerPopup .close-btn{position:absolute;top:1.5rem;right:1.5rem}.share-manager{display:flex;flex-direction:column;justify-content:space-between}.share-manager__table{margin:1rem 0 auto}.share-manager__table thead{font-weight:bold;text-align:left}.share-manager__table thead td:nth-child(1),.share-manager__table thead td:nth-child(2){padding-left:.5rem}.share-manager__table thead td:nth-child(3) .tooltip,.share-manager__table thead td:nth-child(4) .tooltip{font-weight:normal;font-size:1rem}.share-manager__table tbody{text-align:left}.share-manager__table tbody td:nth-child(4){font-size:1rem}.share-manager__table td input{border:0 !important}.share-manager__table td input[type=text]{width:100%}.share-manager__table td:nth-child(1){width:45%}.share-manager__table td:nth-child(2){width:20%}.share-manager__table td:nth-child(3){width:10%}.share-manager__table td:nth-child(4){width:25%}.share-manager__actions{display:flex;justify-content:space-between}.share-manager__form{display:flex;flex-direction:column;gap:.5rem}.share-manager__form_input{display:flex;flex-direction:column;gap:.5rem}.share-manager__form_input input{flex-grow:1}.share-manager .share-flags input.share-flags-check{display:none}.share-manager .share-flags input.share-flags-check+label.share-flags-label{color:gray;margin-right:.25rem;padding:.25rem .25rem .125rem;border:1px solid #6d6d6d;border-radius:.5rem}.share-manager .share-flags input.share-flags-check:checked+label.share-flags-label{color:#118fcc}.share-manager label span{display:inline-block;width:1.125rem}.manage-visibility label{width:100%;cursor:pointer}.manage-visibility{display:flex;justify-content:space-between}@media(max-width: 700px){.file-view__body-details{flex-direction:column;align-items:flex-start;gap:1rem}.file-view__body-details-stat{grid-template-columns:1fr;gap:.5rem}.file-view__body-details-stat span{display:flex;align-items:center}.share-manager__table,.share-manager__table thead,.share-manager__table tbody,.share-manager__table tr,.share-manager__table td{display:block;width:100% !important}.share-manager__table thead{display:none}.share-manager__table tr{border:1px solid #ccc;border-radius:8px;margin-bottom:1rem;padding:.5rem;background:#fff}.share-manager__table td{margin-bottom:.5rem;border:none !important;padding-left:0 !important}table.myfiles,table.myfiles tr,table.myfiles td,table.friendsfiles,table.friendsfiles tr,table.friendsfiles td{display:block;width:100% !important}table.myfiles th,table.friendsfiles th{display:none}table.myfiles tr,table.friendsfiles tr{border:1px solid #ccc;border-radius:8px;margin-bottom:1rem;padding:.5rem;background:#fff}.file-search-container{flex-direction:column}.file-search-container__keywords{flex-basis:auto;width:100%;border-right:none;border-bottom:1px solid rgba(20,20,27,.1);padding-bottom:1rem;margin-bottom:1rem}.results-container,.results-container thead,.results-container tbody,.results-container tr,.results-container td{display:block;width:100% !important}.results-container thead{display:none}.results-container tr{border-bottom:1px solid #eee;padding:1rem 0}.results-container td{margin-bottom:.5rem;word-break:break-all}}.file-section{margin-top:2rem;display:flex;flex-direction:column}.comments-section{margin-top:2rem;display:flex;justify-content:space-between}.comments-section__menu{display:flex;gap:1rem}.comments-section__menu-id{display:flex;align-items:center;gap:.25rem}#toggleunsub{position:relative;background:gray}table.channels th:nth-child(1){width:50%;text-align:start}table.channels td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.channels tr:hover{background-color:#eef3f6;cursor:pointer}table.channels tr.hidden{display:none}table{padding:.5rem}table.comments{border:1px solid #eee}table.comments th{height:40px}table.comments th:nth-child(1){width:2%}table.comments th:nth-child(2){width:40%}table.comments td{word-wrap:break-word}table.comments td:nth-child(2){text-align:start}table.files th:first-child{text-align:start;width:60%}table.files tr td:first-child{text-align:start}table.files td{word-wrap:break-word}#mtags{width:160px;text-align:center;font-size:medium;margin-left:10px;height:40px}.forums-node-panel{position:relative;bottom:200px;margin-left:200px;animation:fadein .5s}table.forums th:nth-child(1){width:50%;text-align:start}table.forums td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.forums tr:hover{background-color:#eef3f6;cursor:pointer}table.forums tr.hidden{display:none}#searchforum{position:relative;margin-left:250px}#forumdetails{position:relative;padding:10px}.p{margin:0}#toggleunsub{position:relative;background:gray}table.threads tr:hover{background-color:#eef3f6;cursor:pointer}table.threads td{word-wrap:break-word}table.threadreply th:nth-child(2){width:50%}table.threadreply th:nth-child(1){width:2%}table.threadreply td:nth-child(2){width:50%;text-align:start}table.threadreply td{word-wrap:break-word}table.threadreply tr:hover{background-color:#eef3f6;cursor:pointer}table.boards th:nth-child(1){width:50%;text-align:start}table.boards td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.boards tr:hover{background-color:#eef3f6;cursor:pointer}table.boards tr.hidden{display:none}#toggleunsub{position:relative;background:gray}#options{width:100px;text-align:center;font-size:medium;margin-left:20px;height:40px}#composepopup{height:80%;width:70%}#mtags{width:160px;text-align:center;font-size:medium;margin-left:10px;height:40px}.mail .permission-flag{margin-bottom:1rem;display:flex;gap:1rem}.mail-tags{padding:.5rem;border:1px solid rgba(20,20,27,.2);border-radius:6px}.mail-tags__container{display:flex;flex-direction:column}.mail-tags__container .tag-item{display:flex;align-items:center;gap:4px;border-bottom:1px solid rgba(20,20,27,.1);padding:2px 0}.mail-tags__container .tag-item:last-child{border:none}.mail-tags__container .tag-item__color{width:1.25rem;height:1.25rem;aspect-ratio:1}.mail-tags__container .tag-item__name{font-size:1.125rem}.mail-tags__container .tag-item__modify{margin-left:auto;font-size:.75rem;display:flex;gap:4px}.mail-tags__container .tag-item:hover{background-color:#eef3f6}.mail-tags__container .tag-item button,.mail-tags__container .tag-item button.red{padding:.25rem .6rem}.mail-tags-form .input-field{margin-bottom:.5rem}.mail-tags-form .input-field label{margin-right:.5rem}.external-address{margin:0;padding-left:1rem;height:100px;overflow:hidden auto}.external-address::-webkit-scrollbar{display:none}.proxy-server{display:flex;flex-direction:column;gap:4px}.proxy-server__tor>h4,.proxy-server__i2p>h4{margin-bottom:.25rem}.proxy-server__tor>input,.proxy-server__i2p>input{margin-right:.5rem}.proxy-server__tor .proxy-outgoing,.proxy-server__i2p .proxy-outgoing{display:inline-flex;align-items:center;gap:.5rem}.proxy-server__tor .proxy-outgoing__status,.proxy-server__i2p .proxy-outgoing__status{width:1rem;height:1rem;aspect-ratio:1;border:1px solid #000;border-radius:50%}.config-files{display:flex;flex-direction:column;gap:1rem}.proxy-server-container{width:100%;display:flex;flex-direction:column;gap:1rem}.proxy-description{color:#334155;font-size:.95rem;margin-bottom:.5rem}.proxy-rows-container{display:flex;flex-direction:column;gap:.75rem;width:100%}.proxy-row{display:grid;grid-template-columns:160px 220px 220px auto;gap:.75rem;align-items:center;width:100%}.proxy-label{font-size:.95rem;font-weight:500;color:#1e293b}.proxy-addr-input,.proxy-port-input{width:100% !important;max-width:none !important}.proxy-status-container{display:flex;align-items:center;gap:.5rem}.proxy-status-bullet{width:14px;height:14px;border-radius:50%;display:inline-block;border:1px solid #475569}.proxy-status-text{font-size:.95rem;color:#1e293b}