diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index ffe077e..c36f5c4 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -8,35 +8,74 @@ function loadLobbyDetails(id, apply) { rs.rsJsonApiRequest( '/rsChats/getChatLobbyInfo', { - id, + id: { xstr64: id }, }, - (detail) => { - if (detail.retval) { + (detail, success) => { + if (success && detail.retval) { + detail.info.chatType = 3; // LOBBY apply(detail.info); + } else { + apply(null); } }, - true, - {}, - undefined, - // Custom serializer NOTE: - // Since id represents 64-bit int(see deserializer note below) - // Instead of using JSON.stringify, this function directly - // creates a json string manually. - () => '{"id":' + id + '}' + 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) { + if (lobbies !== undefined && lobbies !== null) { const list = [...lobbies]; list.sort((a, b) => a.lobby_name.localeCompare(b.lobby_name)); return list; } - // return lobbies; // fallback on reload page in browser, keep undefiend + return []; // return empty array instead of 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: [], // to exclude subscribed from public rooms (subscribedRooms filled to late) @@ -47,40 +86,73 @@ const ChatRoomsModel = { rs.rsJsonApiRequest( '/rsChats/getListOfNearbyChatLobbies', {}, - (data) => (ChatRoomsModel.allRooms = sortLobbies(data.public_lobbies)) + (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) { - // ChatRoomsModel.subscribedRooms = {}; rs.rsJsonApiRequest( '/rsChats/getChatLobbyList', {}, - // JS uses double precision numbers of 64 bit. It is equivalent - // to 53 bits of precision. All large precision ints will - // get truncated to an approximation. - // This API uses Cpp-style 64 bits for `id`. - // So we use the string-value 'xstr64' instead (data) => { - const ids = data.cl_list.map((lid) => lid.xstr64); - ChatRoomsModel.knownSubscrIds = ids; - const rooms = {}; - ids.map((id) => - loadLobbyDetails(id, (info) => { - rooms[id] = info; - if (Object.keys(rooms).length === ids.length) { - // apply rooms to subscribedRooms only after reading all room-details, so sorting all or nothing - ChatRoomsModel.subscribedRooms = rooms; + 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 (after != null) { - after(); + }); + + 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(info.lobby_id.xstr64); + return this.knownSubscrIds.includes(rs.idToHex(info.lobby_id)); }, }; @@ -90,28 +162,42 @@ const ChatRoomsModel = { * msg: Message to Display */ const Message = () => { - let msg = null; // message to display - let text = ''; // extracted text to display - let datetime = ''; // date time to display - let username = ''; // username to display (later may be linked) return { - oninit: (vnode) => { - console.info('chat Message', vnode); - msg = vnode.attrs; - datetime = new Date(msg.sendTime * 1000).toLocaleTimeString(); - username = rs.userList.username(msg.lobby_peer_gxs_id); - text = msg.msg + view: (vnode) => { + const msg = vnode.attrs; + const datetime = new Date(msg.sendTime * 1000).toLocaleTimeString(); + // 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); + } + } + + 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'), ''); - console.info('chat Text', text); - }, - view: () => - m( + return m( '.message', m('span.datetime', datetime), m('span.username', username), m('span.messagetext', text) - ), + ); + }, }; }; @@ -123,12 +209,74 @@ const ChatLobbyModel = { isSubscribed: false, messages: [], users: [], - setupAction: (lobbyId, nick) => {}, + messageKeys: new Set(), + lastLobbyId: 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)) { + 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', {}, - () => m.route.set('/chat/:lobby_id', { lobbyId }), + () => m.route.set('/chat/:lobby', { lobby: lobbyId }), true, {}, JSON.parse, @@ -136,7 +284,7 @@ const ChatLobbyModel = { ); }, enterPublicLobby(lobbyId, nick) { - console.info('joinVisibleChatLobby', nick, '@', lobbyId); + // Set lobby nickname rs.rsJsonApiRequest( '/rsChats/joinVisibleChatLobby', {}, @@ -144,65 +292,130 @@ const ChatLobbyModel = { loadLobbyDetails(lobbyId, (info) => { ChatRoomsModel.subscribedRooms[lobbyId] = info; ChatRoomsModel.loadSubscribedRooms(() => { - m.route.set('/chat/:lobby', { lobby: info.lobby_id.xstr64 }); + m.route.set('/chat/:lobby', { lobby: rs.idToHex(info.lobby_id) }); }); }); }, - true, - {}, - JSON.parse, - () => '{"lobby_id":' + lobbyId + ',"own_id":"' + nick + '"}' + true ); }, unsubscribeChatLobby(lobbyId, follow) { - console.info('unsubscribe lobby', lobbyId); + // Unsubscribe rs.rsJsonApiRequest( '/rsChats/unsubscribeChatLobby', {}, - () => ChatRoomsModel.loadSubscribedRooms(follow), + (data, success) => { + if (success) { + ChatRoomsModel.loadSubscribedRooms(follow); + } + }, true, {}, JSON.parse, () => '{"lobby_id":' + lobbyId + '}' ); }, - chatId(action) { - return { type: 3, lobby_id: { xstr64: m.route.param('lobby') } }; + 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) { - loadLobbyDetails(currentlobbyid, (detail) => { + 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) || '???'; - const lobbyid = currentlobbyid; - // apply existing messages to current lobby view - rs.events[15].chatMessages( - this.chatId(), - rs.events[15], - (l) => (this.messages = l.map((msg) => m(Message, msg))) - ); - // register for chatEvents for future messages + + // 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) => { - if (chatMessage.chat_id.type === 3 && chatMessage.chat_id.lobby_id.xstr64 === lobbyid) { - this.messages.push(m(Message, chatMessage)); - m.redraw(); + // 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 (only snapshot, we don't get notified about changes of participants) - const names = detail.gxs_ids.reduce((a, u) => a.concat(rs.userList.username(u.key)), []); - names.sort((a, b) => a.localeCompare(b)); - this.users = []; - names.forEach((name) => (this.users = this.users.concat([m('.user', name)]))); - return this.users; + + // Lookup for chat-user names (Only for lobbies for now) + // Lookup for chat-user names + if (detail.gxs_ids) { + let names = []; + if (Array.isArray(detail.gxs_ids)) { + names = detail.gxs_ids.reduce((a, u) => a.concat(rs.userList.username(u.key)), []); + } else if (typeof detail.gxs_ids === 'object') { + names = Object.keys(detail.gxs_ids).map(key => rs.userList.username(key)); + } + names.sort((a, b) => a.localeCompare(b)); + this.users = []; + names.forEach((name) => (this.users = this.users.concat([m('.user', name)]))); + } else { + this.users = [m('.user', detail.lobby_name)]; + } + m.redraw(); + }; + + loadLobbyDetails(currentlobbyid, (detail) => { + if (detail) { + finishLoad(detail); + } else { + // Fallback to Distant Chat + loadDistantChatDetails(currentlobbyid, (dDetail) => { + if (dDetail) { + finishLoad(dDetail); + } + }); + } }); }, loadPublicLobby(currentlobbyid) { - console.info('loadPublicLobby ChatRoomsModel:', ChatRoomsModel); this.setupAction = this.enterPublicLobby; this.isSubscribed = false; ChatRoomsModel.allRooms.forEach((it) => { - if (it.lobby_id.xstr64 === currentlobbyid) { + if (rs.idToHex(it.lobby_id) === currentlobbyid) { this.currentLobby = it; this.lobby_user = '???'; this.lobbyid = currentlobbyid; @@ -211,51 +424,52 @@ const ChatLobbyModel = { 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.lobby_user, + }; + this.addMessages([echoMsg], true); + rs.rsJsonApiRequest( '/rsChats/sendChat', {}, - () => { - // adding own message to log - rs.events[15].handler( - { - mChatMessage: { - chat_id: this.chatId(), - msg, - sendTime: new Date().getTime() / 1000, - lobby_peer_gxs_id: this.currentLobby.gxs_id, + (data, success) => { + if (success) { + // adding own message to log + rs.events[15].handler( + { + mChatMessage: { + chat_id: this.chatId(), + msg, + sendTime: new Date().getTime() / 1000, + lobby_peer_gxs_id: this.currentLobby.gxs_id, + }, }, - }, - rs.events[15] - ); - onsuccess(); - }, - true, - {}, - undefined, - () => - '{"id":{"type": 3,"lobby_id":' + - m.route.param('lobby') + - '}, "msg":' + - JSON.stringify(msg) + - '}' + rs.events[15] + ); + onsuccess(); + } + } ); }, selected(info, selName, defaultName) { - const currid = (ChatLobbyModel.currentLobby.lobby_id || { xstr64: m.route.param('lobby') }) - .xstr64; - return (info.lobby_id.xstr64 === currid ? 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: info.lobby_id.xstr64 }); - ChatLobbyModel.loadLobby(info.lobby_id.xstr64); // update + 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: info.lobby_id.xstr64 }); - ChatLobbyModel.loadPublicLobby(info.lobby_id.xstr64); // update + m.route.set('/chat/:lobby/setup', { lobby: rs.idToHex(info.lobby_id) }); + ChatLobbyModel.loadPublicLobby(rs.idToHex(info.lobby_id)); // update }; }, }; @@ -263,23 +477,13 @@ const ChatLobbyModel = { // ************************* views **************************** const Lobby = () => { - let info = {}; - let tagname = ''; - let onclick = (e) => {}; - let lobbytagname = ''; return { - oninit: (v) => { - info = v.attrs.info; - tagname = v.attrs.tagname; - onclick = v.attrs.onclick || ((e) => {}); - lobbytagname = v.attrs.lobbytagname || 'mainname'; - }, - view: (v) => { + view: (vnode) => { + const { info, tagname, onclick, lobbytagname = 'mainname' } = vnode.attrs; return m( ChatLobbyModel.selected(info, '.selected-lobby', tagname), { - key: info.lobby_id.xstr64, - + key: rs.idToHex(info.lobby_id), onclick, }, [ @@ -343,7 +547,7 @@ const PublicLeftLobbies = { return [ m('h5.lefttitle', 'public:'), m(LobbyList, { - rooms: Object.values(ChatRoomsModel.allRooms).filter( + rooms: Object.values(ChatRoomsModel.allRooms || {}).filter( (info) => !ChatRoomsModel.subscribed(info) ), tagname: '.leftlobby.public', @@ -354,89 +558,158 @@ const PublicLeftLobbies = { }, }; -const PublicLobbies = () => { - return m('.widget', [ - m('.widget__heading', m('h3', 'Public chat rooms')), - m('.widget__body', [ - m(LobbyList, { - rooms: ChatRoomsModel.allRooms.filter((info) => !ChatRoomsModel.subscribed(info)), - tagname: '.lobby.public', - onclick: ChatLobbyModel.setupEvent, - }), - ]), - ]); +const PublicLobbies = { + view() { + return m('.widget', [ + m('.widget__heading', m('h3', 'Public chat rooms')), + m('.widget__body', [ + m(LobbyList, { + rooms: (ChatRoomsModel.allRooms || []).filter((info) => !ChatRoomsModel.subscribed(info)), + tagname: '.lobby.public', + onclick: ChatLobbyModel.setupEvent, + }), + ]), + ]); + }, }; 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.route.param('subaction') !== 'setup' + 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', - }, - { replace: true } - ), - }), - ] + m('i.fas.fa-cog.setupicon', { + title: 'configure lobby', + onclick: () => + m.route.set( + '/chat/:lobby/:subaction', + { + lobby: m.route.param('lobby'), + subaction: 'setup', + }, + { 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.leaveicon', { + title: 'leaving lobby', + onclick: () => + ChatLobbyModel.unsubscribeChatLobby(m.route.param('lobby'), () => { + m.route.set('/chat', null, { replace: true }); + }), + }), + ] : [] ); }; // ***************************** Page Layouts ****************************** -const Layout = () => { - return { - view: () => m('.node-panel', [m(SubscribedLobbies), PublicLobbies()]), - }; +const Layout = { + view: () => m('.node-panel.chat-panel.chat-hub', [m(SubscribedLobbies), m(PublicLobbies)]), }; const LayoutSingle = () => { + const onResize = () => { + const element = document.querySelector('.messages'); + if (element) element.scrollTop = element.scrollHeight; + }; return { - oninit: () => ChatLobbyModel.loadLobby(m.route.param('lobby')), - view: (vnode) => - m('.node-panel', [ - LobbyName(), - m('.lobbies', m(SubscribedLeftLobbies), m(PublicLeftLobbies)), - m('.messages', ChatLobbyModel.messages), - m('.rightbar', ChatLobbyModel.users), - m( - '.chatMessage', - {}, - m('textarea.chatMsg', { - placeholder: 'enter new message and press return to send', - onkeydown: (e) => { - if (e.code === 'Enter') { - const msg = e.target.value; - e.target.value = ' sending ... '; - ChatLobbyModel.sendMessage(msg, () => (e.target.value = '')); - return false; - } - }, - }) - ), - ]), + oninit: () => { + ChatLobbyModel.loadLobby(m.route.param('lobby')); + window.addEventListener('resize', onResize); + }, + onremove: () => window.removeEventListener('resize', onResize), + view: (vnode) => { + const chatType = ChatLobbyModel.currentLobby.chatType; + const isPrivate = chatType === 1 || chatType === 2; + return m( + '.node-panel.chat-panel.chat-room', + { + class: + (MobileState.showLobbies ? 'show-lobbies ' : '') + + (MobileState.showUsers ? 'show-users ' : '') + + (isPrivate ? 'no-lobbies' : ''), + }, + [ + 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', + {}, + [ + m('textarea.chatMsg', { + placeholder: 'Type a message...', + 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 = '')); + return false; + } + }, + }), + m( + 'button.chat-send-btn', + { + onclick: (e) => { + const textarea = e.target.closest('.chatMessage').querySelector('textarea'); + const msg = textarea.value; + if (msg.trim() === '') return; + textarea.value = ' sending ... '; + ChatLobbyModel.sendMessage(msg, () => (textarea.value = '')); + }, + }, + m('i.fas.fa-paper-plane') + ), + ] + ), + ] + ); + }, }; }; @@ -445,40 +718,49 @@ const LayoutSetup = () => { return { oninit: () => peopleUtil.ownIds((data) => (ownIds = data)), view: (vnode) => - m('.node-panel', [ - LobbyName(), - m('.lobbies', m(SubscribedLeftLobbies), m(PublicLeftLobbies)), - m('.setup', [ - m('h5.selectidentity', 'Select identity to use'), - ownIds.map((nick) => - m( - '.identity' + + 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) - ) - ), - ]), - ]), + { + onclick: () => ChatLobbyModel.setupAction(m.route.param('lobby'), nick), + }, + rs.userList.username(nick) + ) + ), + ]), + ] + ), }; }; /* /rsChats/initiateDistantChatConnexion - * @param[in] to_pid RsGxsId to start the connection - * @param[in] from_pid owned RsGxsId who start the connection - * @param[out] pid distant chat id - * @param[out] error_code if the connection can't be stablished - * @param[in] notify notify remote that the connection is stablished + * @param[in] to_pid RsGxsId to start the connection + * @param[in] from_pid owned RsGxsId who start the connection + * @param[out] pid distant chat id + * @param[out] error_code if the connection can't be stablished + * @param[in] notify notify remote that the connection is stablished */ const LayoutCreateDistant = () => { let ownIds = []; return { oninit: () => peopleUtil.ownIds((data) => (ownIds = data)), view: (vnode) => - m('.node-panel', [ + m('.node-panel.chat-panel.chat-room', [ m('.createDistantChat', [ 'choose identitiy to chat with ', rs.userList.username(m.route.param('lobby')), @@ -494,9 +776,8 @@ const LayoutCreateDistant = () => { from_pid: id, notify: true, }, - (result) => { - console.info('initiateDistantChatConnexion', result); - m.route.set('/chat/:lobbyid', { lobbyid: result.pid }); + (res) => { + m.route.set('/chat/:lobby', { lobby: rs.idToHex(res.pid) }); } ), }, @@ -511,7 +792,6 @@ const LayoutCreateDistant = () => { module.exports = { oninit: () => { ChatRoomsModel.loadSubscribedRooms(); - ChatRoomsModel.loadPublicRooms(); }, view: (vnode) => { if (m.route.param('lobby') === undefined) { diff --git a/webui-src/app/files/files_downloads.js b/webui-src/app/files/files_downloads.js index 9a41a30..192c0a1 100644 --- a/webui-src/app/files/files_downloads.js +++ b/webui-src/app/files/files_downloads.js @@ -149,24 +149,24 @@ const Component = () => { Downloads.resetSearch(); }, view: () => [ - m('.widget__body-heading', [ - m('h3', `Downloads (${Downloads.hashes ? Downloads.hashes.length : 0} files)`), - m('.action', [ + m('.widget__body-heading', { style: { display: 'flex', flexDirection: 'column', alignItems: 'flex-start' } }, [ + m('.action', { style: { marginBottom: '10px' } }, [ m('button', { onclick: () => widget.popupMessage(m(NewFileDialog)) }, 'Add new file'), m('button', { onclick: clearFileCompleted }, 'Clear completed'), ]), + m('h3', `Downloads (${Downloads.hashes ? Downloads.hashes.length : 0} files)`), ]), m('.widget__body-content', [ Downloads.statusMap && - Object.keys(Downloads.statusMap).map((hash) => - m(util.File, { - info: Downloads.statusMap[hash], - strategy: Downloads.strategies[hash], - direction: 'down', - transferred: Downloads.statusMap[hash].transfered.xint64, - chunksInfo: Downloads.chunksMap[hash], - }) - ), + Object.keys(Downloads.statusMap).map((hash) => + m(util.File, { + info: Downloads.statusMap[hash], + strategy: Downloads.strategies[hash], + direction: 'down', + transferred: Downloads.statusMap[hash].transfered.xint64, + chunksInfo: Downloads.chunksMap[hash], + }) + ), ]), ], }; diff --git a/webui-src/app/files/files_manager.js b/webui-src/app/files/files_manager.js index d92bfb9..332d1bf 100644 --- a/webui-src/app/files/files_manager.js +++ b/webui-src/app/files/files_manager.js @@ -36,11 +36,10 @@ function loadSharedDirectories() { // Update Shared Directories when there is a corresponding event rs.events[rs.RsEventsType.SHARED_DIRECTORIES] = { handler: (event) => { - console.log('Shared Directories Event: ', event); - switch(event.mEventCode) { - case futil.RsSharedDirectoriesEventCode.SHARED_DIRS_LIST_CHANGED: - loadSharedDirectories(); - break; + switch (event.mEventCode) { + case futil.RsSharedDirectoriesEventCode.SHARED_DIRS_LIST_CHANGED: + loadSharedDirectories(); + break; } }, }; @@ -156,85 +155,85 @@ const ShareDirTable = () => { m( 'tbody.share-manager__table_body', sharedDirArr.length && - sharedDirArr.map((sharedDirItem, index) => { - const { - filename, - virtualname, - shareflags, - parent_groups: parentGroups, - } = sharedDirItem; - const sharedFlags = futil.calcIndividualFlags(shareflags); - return m('tr', [ - m( - 'td', - m('input[type=text]', { - value: filename, - disabled: isEditDisabled, - oninput: (e) => { - sharedDirArr[index].filename = e.target.value; - }, - }) - ), - m( - 'td', - m('input[type=text]', { - value: virtualname, - disabled: isEditDisabled, - oninput: (e) => { - sharedDirArr[index].virtualname = e.target.value; - }, - }) - ), - m( - 'td.share-flags', - Object.keys(sharedFlags).map((flag) => { - return [ - m(`input.share-flags-check[type=checkbox][id=${flag}]`, { - checked: sharedFlags[flag], - disabled: isEditDisabled, - }), - m( - `label.share-flags-label[for=${flag}]`, - { - onclick: () => { - if (isEditDisabled) return; - sharedFlags[flag] = !sharedFlags[flag]; - sharedDirArr[index].shareflags = futil.calcShareFlagsValue(sharedFlags); - }, - style: isEditDisabled && { color: '#7D7D7D' }, + sharedDirArr.map((sharedDirItem, index) => { + const { + filename, + virtualname, + shareflags, + parent_groups: parentGroups, + } = sharedDirItem; + const sharedFlags = futil.calcIndividualFlags(shareflags); + return m('tr', [ + m( + 'td', + m('input[type=text]', { + value: filename, + disabled: isEditDisabled, + oninput: (e) => { + sharedDirArr[index].filename = e.target.value; + }, + }) + ), + m( + 'td', + m('input[type=text]', { + value: virtualname, + disabled: isEditDisabled, + oninput: (e) => { + sharedDirArr[index].virtualname = e.target.value; + }, + }) + ), + m( + 'td.share-flags', + Object.keys(sharedFlags).map((flag) => { + return [ + m(`input.share-flags-check[type=checkbox][id=${flag}]`, { + checked: sharedFlags[flag], + disabled: isEditDisabled, + }), + m( + `label.share-flags-label[for=${flag}]`, + { + onclick: () => { + if (isEditDisabled) return; + sharedFlags[flag] = !sharedFlags[flag]; + sharedDirArr[index].shareflags = futil.calcShareFlagsValue(sharedFlags); }, - m( - // check the flag type then if its value is true then only render the icon - flag === 'isAnonymousSearch' - ? sharedFlags[flag] - ? 'i.fas.fa-search' - : 'span' - : flag === 'isAnonymousDownload' + style: isEditDisabled && { color: '#7D7D7D' }, + }, + m( + // check the flag type then if its value is true then only render the icon + flag === 'isAnonymousSearch' + ? sharedFlags[flag] + ? 'i.fas.fa-search' + : 'span' + : flag === 'isAnonymousDownload' ? sharedFlags[flag] ? 'i.fas.fa-download' : 'span' : sharedFlags[flag] - ? 'i.fas.fa-eye' - : 'span' - ) - ), - ]; - }) - ), - m( - 'td', - { - // since this is not an input element, manually change color - style: { color: isEditDisabled ? '#6D6D6D' : 'black' }, - onclick: () => - !isEditDisabled && widget.popupMessage(m(ManageVisibility, { parentGroups })), - }, - parentGroups.length === 0 - ? 'All Friend nodes' - : parentGroups.map((groupFlag) => futil.RsNodeGroupId[groupFlag]).join(', ') - ), - ]); - }) + ? 'i.fas.fa-eye' + : 'span' + ) + ), + ]; + }) + ), + m( + 'td', + { + // since this is not an input element, manually change color + style: { color: isEditDisabled ? '#6D6D6D' : 'black' }, + onclick: () => + !isEditDisabled && widget.popupMessage(m(ManageVisibility, { parentGroups })), + }, + parentGroups.length === 0 + ? 'All Friend nodes' + : parentGroups.map((groupFlag) => futil.RsNodeGroupId[groupFlag]).join(', ') + ), + ]); + }) ), ]); }, diff --git a/webui-src/app/files/files_proxy.js b/webui-src/app/files/files_proxy.js index 74ceeb9..a389a4d 100644 --- a/webui-src/app/files/files_proxy.js +++ b/webui-src/app/files/files_proxy.js @@ -8,14 +8,19 @@ const fileProxyObj = futil.createProxy({}, () => { rs.events[rs.RsEventsType.FILE_TRANSFER] = { handler: (event) => { - console.log('search results : ', event); - // if request item doesn't already exists in Object then create new item if (!Object.prototype.hasOwnProperty.call(fileProxyObj, event.mRequestId)) { fileProxyObj[event.mRequestId] = []; } - fileProxyObj[event.mRequestId].push(...event.mResults); + event.mResults.forEach((newRes) => { + const isAlt = fileProxyObj[event.mRequestId].some( + (oldRes) => oldRes.fHash === newRes.fHash && oldRes.fName === newRes.fName + ); + if (!isAlt) { + fileProxyObj[event.mRequestId].push(newRes); + } + }); }, }; diff --git a/webui-src/app/files/files_search.js b/webui-src/app/files/files_search.js index 0a3f9f0..a27d5c2 100644 --- a/webui-src/app/files/files_search.js +++ b/webui-src/app/files/files_search.js @@ -15,7 +15,7 @@ function handleSubmit() { reqObj['_' + res.body.retval] = matchString; currentItem = '_' + res.body.retval; }) - .catch((error) => console.log(error)); + .catch((error) => { }); } const SearchBar = () => { @@ -31,6 +31,34 @@ const SearchBar = () => { }; }; +const getFileIcon = (fileName) => { + const ext = fileName.split('.').pop().toLowerCase(); + switch (ext) { + case 'pdf': return 'i.fas.fa-file-pdf'; + case 'zip': + case 'rar': + case 'tar': + case 'gz': + case '7z': return 'i.fas.fa-file-archive'; + case 'jpg': + case 'jpeg': + case 'png': + case 'gif': return 'i.fas.fa-file-image'; + case 'mp4': + case 'mkv': + case 'avi': + case 'mov': return 'i.fas.fa-file-video'; + case 'mp3': + case 'wav': + case 'flac': return 'i.fas.fa-file-audio'; + case 'txt': + case 'doc': + case 'docx': + case 'pdf': return 'i.fas.fa-file-alt'; + default: return 'i.fas.fa-file'; + } +}; + const Layout = () => { let active = 0; function handleFileDownload(item) { @@ -54,7 +82,7 @@ const Layout = () => { ); }) .catch((error) => { - console.log('error in sending download request: ', error); + // console.log('error in sending download request: ', error); }); } return { @@ -63,58 +91,76 @@ const Layout = () => { m('.widget__body', [ m('div.file-search-container', [ m('div.file-search-container__keywords', [ - m('h5.bold', 'Keywords'), - Object.keys(reqObj).length !== 0 && + m('.keywords-header', [ + m('h5.bold', 'Keywords'), m( - 'div.keywords-container', - Object.keys(reqObj) - .reverse() - .map((item, index) => { - return m( - m.route.Link, - { - class: active === index ? 'selected' : '', - onclick: () => { - active = index; - currentItem = item; - }, - href: `/files/search/${item}`, - }, - reqObj[item] - ); - }) + 'button.red.clear-btn', + { + onclick: () => { + Object.keys(reqObj).forEach((key) => delete reqObj[key]); + Object.keys(fproxy.fileProxyObj).forEach((key) => delete fproxy.fileProxyObj[key]); + currentItem = 0; + active = 0; + }, + }, + 'Clear' ), + ]), + Object.keys(reqObj).length !== 0 && + m( + 'div.keywords-container', + Object.keys(reqObj) + .reverse() + .map((item, index) => { + return m( + m.route.Link, + { + class: active === index ? 'selected' : '', + onclick: () => { + active = index; + currentItem = item; + }, + href: `/files/search/${item}`, + }, + reqObj[item] + ); + }) + ), ]), m('div.file-search-container__results', [ - Object.keys(fproxy.fileProxyObj).length === 0 + Object.keys(fproxy.fileProxyObj).length === 0 || currentItem === 0 ? m('h5.bold', 'Results') - : m('table.results-container', [ - m( - 'thead.results-header', - m('tr', [ - m('th', 'Name'), - m('th', 'Size'), - m('th', 'Hash'), - m('th', 'Download'), - ]) - ), - m( - 'tbody.results', - fproxy.fileProxyObj[currentItem.slice(1)] - ? fproxy.fileProxyObj[currentItem.slice(1)].map((item) => - m('tr', [ - m('td.results__name', [m('i.fas.fa-file'), m('span', item.fName)]), - m('td.results__size', rs.formatBytes(item.fSize.xint64)), - m('td.results__hash', item.fHash), - m( - 'td.results__download', - m('button', { onclick: () => handleFileDownload(item) }, 'Download') - ), - ]) - ) - : 'No Results.' - ), - ]), + : m('div.results-container', [ + m( + 'div.results-header', + m('.results-row', [ + m('.results-cell.name-col', 'Name'), + m('.results-cell.size-col', 'Size'), + m('.results-cell.hash-col', 'Hash'), + m('.results-cell.action-col', 'Download'), + ]) + ), + m( + 'div.results-list', + fproxy.fileProxyObj[currentItem.slice(1)] + ? fproxy.fileProxyObj[currentItem.slice(1)].map((item) => + m('div.results-row.file-item', [ + m('.results-cell.name-col', [m(getFileIcon(item.fName)), m('span', item.fName)]), + m('.results-cell.size-col', rs.formatBytes((item.fSize && (item.fSize.xint64 || item.fSize.xstr64)) || 0)), + m('.results-cell.hash-col', item.fHash), + m( + '.results-cell.action-col', + m( + 'button.download-btn-v65', + { onclick: () => handleFileDownload(item) }, + 'Download' + ) + ), + ]) + ) + : 'No Results.' + ), + ]), ]), ]), ]), diff --git a/webui-src/app/files/files_util.js b/webui-src/app/files/files_util.js index f322010..6e5ee20 100644 --- a/webui-src/app/files/files_util.js +++ b/webui-src/app/files/files_util.js @@ -183,9 +183,10 @@ const File = () => { let chunkStrat; const chunkStrats = { // rstypes.h :: 366 - 0: 'Streaming', // CHUNK_STRATEGY_STREAMING + 0: 'Sequential', // CHUNK_STRATEGY_SEQUENTIAL 1: 'Random', // CHUNK_STRATEGY_RANDOM 2: 'Progressive', // CHUNK_STRATEGY_PROGRESSIVE + 3: 'Streaming', // CHUNK_STRATEGY_STREAMING }; function fileCancel(hash) { rs.rsJsonApiRequest('/rsFiles/FileCancel', { hash }).then((res) => @@ -220,25 +221,25 @@ const File = () => { }); } return m('.file-view', { style: { display: info.isSearched ? 'block' : 'none' } }, [ - m('.file-view__heading', [ + m('.file-view__heading', { style: { display: 'flex', flexDirection: 'column', alignItems: 'flex-start' } }, [ m('h6', info.fname), chunkStrat !== undefined && - direction === 'down' && [ - m('.file-view__heading-chunk', [ - m('label[for=chunkTag]', 'Set Chunk Strategy: '), - m('select[id=chunkTag]', { value: chunkStrat, onchange: changeChunkStrategy }, [ - Object.keys(chunkStrats).map((strat) => - m('option', { value: strat }, chunkStrats[strat]) - ), - ]), + direction === 'down' && [ + m('.file-view__heading-chunk', [ + m('label[for=chunkTag]', 'Set Chunk Strategy: '), + m('select[id=chunkTag]', { value: chunkStrat, onchange: changeChunkStrategy }, [ + Object.keys(chunkStrats).map((strat) => + m('option', { value: strat }, chunkStrats[strat]) + ), ]), - ], + ]), + ], ]), m('.file-view__body', [ m( '.file-view__body-progress', direction === 'down' && - m(ProgressBar, { rate: (transferred / info.size.xint64) * 100, chunksInfo }) + m(ProgressBar, { rate: (transferred / info.size.xint64) * 100, chunksInfo }) ), m('.file-view__body-details', [ m('.file-view__body-details-stat', [ @@ -255,10 +256,10 @@ const File = () => { `${rs.formatBytes(info.tfRate * 1024)}/s`, ]), direction === 'down' && - m('span', { title: 'time remaining' }, [ - m('i.fas.fa-clock'), - calcRemainingTime(info.size.xint64 - transferred, info.tfRate), - ]), + m('span', { title: 'time remaining' }, [ + m('i.fas.fa-clock'), + calcRemainingTime(info.size.xint64 - transferred, info.tfRate), + ]), m('span', { title: 'peers' }, [m('i.fas.fa-users'), info.peers.length]), ]), m( diff --git a/webui-src/app/files/my_files.js b/webui-src/app/files/my_files.js index d040aeb..2cc332b 100644 --- a/webui-src/app/files/my_files.js +++ b/webui-src/app/files/my_files.js @@ -3,6 +3,14 @@ const rs = require('rswebui'); const util = require('files/files_util'); const manager = require('files/files_manager'); +const translateName = (name) => { + const n = name.toLowerCase().trim(); + if (n === 'extra list' || n === '[extra list]') return 'Temporary shared files'; + // Match hex strings (IDs) or pure numeric strings + if (/^[0-9a-fA-F]{16,}$/.test(name) || /^\d+$/.test(name)) return 'My Files'; + return name; +}; + const DisplayFiles = () => { const childrenList = []; // stores children details let loaded = false; // checks whether we have loaded the children details or not. @@ -15,28 +23,34 @@ const DisplayFiles = () => { }, view: (v) => [ m('tr', [ - parStruct && Object.keys(parStruct.details.children).length + parStruct && parStruct.details.children && parStruct.details.children.length ? m( - 'td', - m('i.fas.fa-angle-right', { - class: `fa-rotate-${parStruct.showChild ? '90' : '0'}`, - style: 'margin-top: 0.5rem', - onclick: () => { - if (!loaded) { - // if it is not already retrieved - parStruct.details.children.map(async (child) => { - const res = await rs.rsJsonApiRequest('/rsfiles/requestDirDetails', { + 'td', + m('i.fas.fa-angle-right', { + class: `fa-rotate-${parStruct.showChild ? '90' : '0'}`, + style: 'margin-top: 0.5rem', + onclick: async () => { + if (!loaded) { + // if it is not already retrieved + const results = await Promise.all( + parStruct.details.children.map((child) => + rs.rsJsonApiRequest('/rsfiles/requestDirDetails', { handle: child.handle.xint64, flags: util.RS_FILE_HINTS_LOCAL, - }); + }) + ) + ); + results.forEach((res) => { + if (res && res.body && res.body.details) { childrenList.push(res.body.details); - loaded = true; - }); - } - parStruct.showChild = !parStruct.showChild; - }, - }) - ) + } + }); + loaded = true; + } + parStruct.showChild = !parStruct.showChild; + }, + }) + ) : m('td', ''), m( 'td', @@ -47,29 +61,49 @@ const DisplayFiles = () => { left: `calc(1.5rem*${v.attrs.replyDepth})`, }, }, - parStruct.details.name + translateName(parStruct.details.name || '') ), - m('td', rs.formatBytes(parStruct.details.size.xint64)), + m('td', rs.formatBytes((parStruct.details.size && parStruct.details.size.xint64) || 0)), ]), parStruct.showChild && - childrenList.map((child) => - m(DisplayFiles, { - // recursive call - par_directory: { details: child, showChild: false }, - replyDepth: v.attrs.replyDepth + 1, - }) - ), + childrenList.map((child) => + m(DisplayFiles, { + // recursive call + par_directory: { details: child, showChild: false }, + replyDepth: v.attrs.replyDepth + 1, + }) + ), ], }; }; const Layout = () => { - // let root_handle; - let parent; - let showShareManager = false; + let displayList = []; + let isLoading = true; + let showShareManager = false; // Retain original declaration + return { oninit: () => { - rs.rsJsonApiRequest('/rsfiles/requestDirDetails', {}).then((res) => (parent = res)); + rs.rsJsonApiRequest('/rsfiles/requestDirDetails', {}).then(async (res) => { + if (res && res.body && res.body.details) { + if (res.body.details.name === 'root') { + // Skip root and fetch full details for each child (Location ID, Extra list, etc) + const results = await Promise.all( + res.body.details.children.map((child) => + rs.rsJsonApiRequest('/rsfiles/requestDirDetails', { + handle: child.handle.xint64, + flags: util.RS_FILE_HINTS_LOCAL, + }) + ) + ); + displayList = results.map((r) => r.body.details); + } else { + displayList = [res.body.details]; + } + } + isLoading = false; + m.redraw(); + }); }, view: () => [ m('.widget__heading', [ @@ -81,11 +115,14 @@ const Layout = () => { util.MyFilesTable, m( 'tbody', - parent && - m(DisplayFiles, { - par_directory: { details: parent.body.details, showChild: false }, - replyDepth: 0, - }) + isLoading + ? m('tr', m('td[colspan=3]', 'Loading...')) + : displayList.map((details) => + m(DisplayFiles, { + par_directory: { details, showChild: false }, + replyDepth: 0, + }) + ) ) ), m( diff --git a/webui-src/app/forums/forum_view.js b/webui-src/app/forums/forum_view.js index 68159df..95dea6d 100644 --- a/webui-src/app/forums/forum_view.js +++ b/webui-src/app/forums/forum_view.js @@ -32,15 +32,15 @@ function createforum() { }, [ vnode.attrs.authorId && - vnode.attrs.authorId.map((o) => - m( - 'option', - { value: o }, - rs.userList.userMap[o] - ? rs.userList.userMap[o].toLocaleString() + ' (' + o.slice(0, 8) + '...)' - : 'No Signature' - ) - ), + vnode.attrs.authorId.map((o) => + m( + 'option', + { value: o }, + rs.userList.userMap[o] + ? rs.userList.userMap[o].toLocaleString() + ' (' + o.slice(0, 8) + '...)' + : 'No Signature' + ) + ), ] ), m('textarea[rows=5][placeholder=Description]', { @@ -64,10 +64,10 @@ function createforum() { res.body.retval === false ? util.popupmessage([m('h3', 'Error'), m('hr'), m('p', res.body.errorMessage)]) : util.popupmessage([ - m('h3', 'Success'), - m('hr'), - m('p', 'Forum created successfully'), - ]); + m('h3', 'Success'), + m('hr'), + m('p', 'Forum created successfully'), + ]); }, }, 'Create' @@ -131,10 +131,10 @@ const EditThread = () => { res.body.retval === false ? util.popupmessage([m('h3', 'Error'), m('hr'), m('p', res.body.errorMessage)]) : util.popupmessage([ - m('h3', 'Success'), - m('hr'), - m('p', 'Thread edited successfully'), - ]); + m('h3', 'Success'), + m('hr'), + m('p', 'Thread edited successfully'), + ]); util.updatedisplayforums(vnode.attrs.forumId); m.redraw(); }, @@ -175,13 +175,13 @@ const AddThread = () => { }, [ vnode.attrs.authorId && - vnode.attrs.authorId.map((o) => - m( - 'option', - { value: o }, - rs.userList.userMap[o].toLocaleString() + ' (' + o.slice(0, 8) + '...)' - ) - ), + vnode.attrs.authorId.map((o) => + m( + 'option', + { value: o }, + rs.userList.userMap[o].toLocaleString() + ' (' + o.slice(0, 8) + '...)' + ) + ), ] ), m('textarea[rows=5]', { @@ -196,26 +196,26 @@ const AddThread = () => { const res = (vnode.attrs.parent_thread !== '') > 0 // is it a reply or a new thread ? await rs.rsJsonApiRequest('/rsgxsforums/createPost', { - forumId: vnode.attrs.forumId, - mBody: body, - title, - authorId: identity, - parentId: vnode.attrs.parentId, - }) + forumId: vnode.attrs.forumId, + mBody: body, + title, + authorId: identity, + parentId: vnode.attrs.parentId, + }) : await rs.rsJsonApiRequest('/rsgxsforums/createPost', { - forumId: vnode.attrs.forumId, - mBody: body, - title, - authorId: identity, - }); + forumId: vnode.attrs.forumId, + mBody: body, + title, + authorId: identity, + }); res.body.retval === false ? util.popupmessage([m('h3', 'Error'), m('hr'), m('p', res.body.errorMessage)]) : util.popupmessage([ - m('h3', 'Success'), - m('hr'), - m('p', 'Thread added successfully'), - ]); + m('h3', 'Success'), + m('hr'), + m('p', 'Thread added successfully'), + ]); util.updatedisplayforums(vnode.attrs.forumId); m.redraw(); }, @@ -255,15 +255,15 @@ function displaythread() { [ Object.keys(parMap).length // if this thread has some replies ? m( - 'td', - m('i.fas.fa-angle-right', { - class: 'fa-rotate-' + (v.attrs.threadStruct.showReplies ? '90' : '0'), - style: 'margin-top:12px', - onclick: () => { - v.attrs.threadStruct.showReplies = !v.attrs.threadStruct.showReplies; - }, - }) - ) + 'td', + m('i.fas.fa-angle-right', { + class: 'fa-rotate-' + (v.attrs.threadStruct.showReplies ? '90' : '0'), + style: 'margin-top:12px', + onclick: () => { + v.attrs.threadStruct.showReplies = !v.attrs.threadStruct.showReplies; + }, + }) + ) : m('td', ''), m( @@ -310,25 +310,25 @@ function displaythread() { 'Reply' ), editpermission && - m( - 'button', - { - style: 'font-size:15px', - onclick: () => - util.popupmessage( - m(EditThread, { - current_thread: thread.mMeta.mMsgName, - forumId: thread.mMeta.mGroupId, - current_title: thread.mMeta.mMsgName, - current_body: thread.mMsg, - authorId: thread.mMeta.mAuthorId, - current_parent: thread.mMeta.mParentId, - current_msgid: thread.mMeta.mOrigMsgId, - }) - ), - }, - 'Edit' - ), + m( + 'button', + { + style: 'font-size:15px', + onclick: () => + util.popupmessage( + m(EditThread, { + current_thread: thread.mMeta.mMsgName, + forumId: thread.mMeta.mGroupId, + current_title: thread.mMeta.mMsgName, + current_body: thread.mMsg, + authorId: thread.mMeta.mAuthorId, + current_parent: thread.mMeta.mParentId, + current_msgid: thread.mMeta.mOrigMsgId, + }) + ), + }, + 'Edit' + ), ]), ] ), @@ -365,32 +365,26 @@ function displaythread() { ] ), v.attrs.threadStruct.showReplies && - Object.keys(parMap).map((key, index) => - m(displaythread, { - // recursive call to all replies - threadStruct: util.Data.Threads[parMap[key].mGroupId][parMap[key].mOrigMsgId], - replyDepth: v.attrs.replyDepth + 1, - identity: v.attrs.identity, - changeThread: v.attrs.changeThread, - }) - ), + Object.keys(parMap).map((key, index) => + m(displaythread, { + // recursive call to all replies + threadStruct: util.Data.Threads[parMap[key].mGroupId][parMap[key].mOrigMsgId], + replyDepth: v.attrs.replyDepth + 1, + identity: v.attrs.identity, + changeThread: v.attrs.changeThread, + }) + ), ]; }, }; } const ThreadView = () => { - let thread = {}; let ownId; return { showThread: '', - oninit: async (v) => { - if ( - util.Data.ParentThreads[v.attrs.forumId] && - util.Data.ParentThreads[v.attrs.forumId][v.attrs.msgId] - ) { - thread = util.Data.ParentThreads[v.attrs.forumId][v.attrs.msgId]; - } + oninit: (v) => { + util.updatedisplayforums(v.attrs.forumId); peopleUtil.ownIds((data) => { ownId = data; for (let i = 0; i < ownId.length; i++) { @@ -400,8 +394,14 @@ const ThreadView = () => { } }); }, - view: (v) => - m('.widget', { key: v.attrs.msgId }, [ + view: (v) => { + const thread = + util.Data.ParentThreads[v.attrs.forumId] && + util.Data.ParentThreads[v.attrs.forumId][v.attrs.msgId] + ? util.Data.ParentThreads[v.attrs.forumId][v.attrs.msgId] + : { mMsgName: '...' }; + + return m('.widget', { key: v.attrs.msgId }, [ m( 'a[title=Back]', { @@ -420,56 +420,34 @@ const ThreadView = () => { m( 'tbody', util.Data.Threads[v.attrs.forumId] && - util.Data.Threads[v.attrs.forumId][v.attrs.msgId] && - m(displaythread, { - threadStruct: util.Data.Threads[v.attrs.forumId][v.attrs.msgId], - replyDepth: 0, - identity: ownId, - changeThread(newThread) { - v.state.showThread = newThread; - // For displaying the messages of the threads. We pass this into the recursive function displaythreads() - }, - }) + util.Data.Threads[v.attrs.forumId][v.attrs.msgId] && + m(displaythread, { + threadStruct: util.Data.Threads[v.attrs.forumId][v.attrs.msgId], + replyDepth: 0, + identity: ownId, + changeThread(newThread) { + v.state.showThread = newThread; + }, + }) ) ), m('hr'), v.state.showThread && [ m('h4', 'Messages'), util.Data.Threads[v.attrs.forumId] && - util.Data.Threads[v.attrs.forumId][v.state.showThread] && - m('p', m.trust(util.Data.Threads[v.attrs.forumId][v.state.showThread].thread.mMsg)), - // m.trust is to render html content directly. + util.Data.Threads[v.attrs.forumId][v.state.showThread] && + m('p', m.trust(util.Data.Threads[v.attrs.forumId][v.state.showThread].thread.mMsg)), ], - ]), + ]); + }, }; }; const ForumView = () => { - let fname = ''; - let fauthor = ''; - let fsubscribed = {}; - let createDate = {}; - let lastActivity = {}; - let topThreads = {}; let ownId = ''; return { oninit: (v) => { - if (util.Data.DisplayForums[v.attrs.id]) { - fname = util.Data.DisplayForums[v.attrs.id].name; - fsubscribed = util.Data.DisplayForums[v.attrs.id].isSubscribed; - createDate = util.Data.DisplayForums[v.attrs.id].created; - lastActivity = util.Data.DisplayForums[v.attrs.id].activity; - if (rs.userList.userMap[util.Data.DisplayForums[v.attrs.id].author]) { - fauthor = rs.userList.userMap[util.Data.DisplayForums[v.attrs.id].author]; - } else if (Number(util.Data.DisplayForums[v.attrs.id].author) === 0) { - fauthor = 'No Contact Author'; - } else { - fauthor = 'Unknown'; - } - } - if (util.Data.ParentThreads[v.attrs.id]) { - topThreads = util.Data.ParentThreads[v.attrs.id]; - } + util.updatedisplayforums(v.attrs.id); peopleUtil.ownIds((data) => { ownId = data; for (let i = 0; i < ownId.length; i++) { @@ -479,117 +457,139 @@ const ForumView = () => { } }); }, - view: (v) => [ - m( - 'a[title=Back]', - { - onclick: () => - m.route.set('/forums/:tab', { - tab: m.route.param().tab, - }), - }, - m('i.fas.fa-arrow-left') - ), + view: (v) => { + const forumDetails = util.Data.DisplayForums[v.attrs.id] || { + name: '...', + isSubscribed: false, + created: {}, + activity: {}, + author: '0', + description: '...', + }; + const topThreads = util.Data.ParentThreads[v.attrs.id] || {}; + const fname = forumDetails.name; + const fsubscribed = forumDetails.isSubscribed; + const createDate = forumDetails.created; + const lastActivity = forumDetails.activity; + let fauthor = 'Unknown'; - m('h3', fname), - m( - 'button', - { - onclick: async () => { - const res = await rs.rsJsonApiRequest('/rsgxsforums/subscribeToForum', { - forumId: v.attrs.id, - subscribe: !fsubscribed, - }); - if (res.body.retval) { - fsubscribed = !fsubscribed; - util.Data.DisplayForums[v.attrs.id].isSubscribed = fsubscribed; - } - }, - }, - fsubscribed ? 'Subscribed' : 'Subscribe' - ), - m('[id=forumdetails]', [ - m( - 'p', - m('b', 'Date created: '), - typeof createDate === 'object' - ? new Date(createDate.xint64 * 1000).toLocaleString() - : 'undefined' - ), - m('p', m('b', 'Admin: '), fauthor), + if (rs.userList.userMap[forumDetails.author]) { + fauthor = rs.userList.userMap[forumDetails.author]; + } else if (Number(forumDetails.author) === 0) { + fauthor = 'No Contact Author'; + } + + return [ m( - 'p', - m('b', 'Last activity: '), - typeof lastActivity === 'object' - ? new Date(lastActivity.xint64 * 1000).toLocaleString() - : 'undefined' + 'a[title=Back]', + { + onclick: () => + m.route.set('/forums/:tab', { + tab: m.route.param().tab, + }), + }, + m('i.fas.fa-arrow-left') ), - ]), - m('hr'), - m('forumdesc', m('b', 'Description: '), util.Data.DisplayForums[v.attrs.id].description), - m('hr'), - m( - 'threaddetails', - { - style: 'display:' + (fsubscribed ? 'block' : 'none'), - }, - m('h3', 'Threads'), + + m('h3', fname), m( 'button', { - onclick: () => { - util.popupmessage( - m(AddThread, { - parent_thread: '', - forumId: v.attrs.id, - authorId: ownId, - parentId: '', - }) - ); + onclick: async () => { + const res = await rs.rsJsonApiRequest('/rsgxsforums/subscribeToForum', { + forumId: v.attrs.id, + subscribe: !fsubscribed, + }); + if (res.body.retval) { + util.Data.DisplayForums[v.attrs.id].isSubscribed = !fsubscribed; + } }, }, - ['New Thread', m('i.fas.fa-pencil-alt')] + fsubscribed ? 'Subscribed' : 'Subscribe' ), + m('[id=forumdetails]', [ + m( + 'p', + m('b', 'Date created: '), + typeof createDate === 'object' + ? new Date(createDate.xint64 * 1000).toLocaleString() + : 'undefined' + ), + m('p', m('b', 'Admin: '), fauthor), + m( + 'p', + m('b', 'Last activity: '), + typeof lastActivity === 'object' + ? new Date(lastActivity.xint64 * 1000).toLocaleString() + : 'undefined' + ), + ]), + m('hr'), + m('forumdesc', m('b', 'Description: '), forumDetails.description), m('hr'), m( - util.ThreadsTable, + 'threaddetails', + { + style: 'display:' + (fsubscribed ? 'block' : 'none'), + }, + m('h3', 'Threads'), m( - 'tbody', - Object.keys(topThreads).map((key, index) => - m( - 'tr', - { - style: - topThreads[key].mMsgStatus === util.THREAD_UNREAD ? { fontWeight: 'bold' } : '', - onclick: () => { - m.route.set('/forums/:tab/:mGroupId/:mMsgId', { - tab: m.route.param().tab, - mGroupId: v.attrs.id, - mMsgId: topThreads[key].mOrigMsgId, - }); + 'button', + { + onclick: () => { + util.popupmessage( + m(AddThread, { + parent_thread: '', + forumId: v.attrs.id, + authorId: ownId, + parentId: '', + }) + ); + }, + }, + ['New Thread', m('i.fas.fa-pencil-alt')] + ), + m('hr'), + m( + util.ThreadsTable, + m( + 'tbody', + Object.keys(topThreads).map((key, index) => + m( + 'tr', + { + style: + topThreads[key].mMsgStatus === util.THREAD_UNREAD ? { fontWeight: 'bold' } : '', + onclick: () => { + m.route.set('/forums/:tab/:mGroupId/:mMsgId', { + tab: m.route.param().tab, + mGroupId: v.attrs.id, + mMsgId: topThreads[key].mOrigMsgId, + }); + }, }, - }, - [ - m('td', topThreads[key].mMsgName), - m( - 'td', - typeof topThreads[key].mPublishTs === 'object' - ? new Date(topThreads[key].mPublishTs.xint64 * 1000).toLocaleString() - : 'undefined' - ), - m( - 'td', - rs.userList.userMap[topThreads[key].mAuthorId] - ? rs.userList.userMap[topThreads[key].mAuthorId] - : 'Unknown' - ), - ] + [ + m('td', topThreads[key].mMsgName), + m( + 'td', + typeof topThreads[key].mPublishTs === 'object' + ? new Date(topThreads[key].mPublishTs.xint64 * 1000).toLocaleString() + : 'undefined' + ), + m( + 'td', + rs.userList.userMap[topThreads[key].mAuthorId] + ? rs.userList.userMap[topThreads[key].mAuthorId] + : 'Unknown' + ), + ] + ) ) ) ) - ) - ), - ], + ), + ]; + }, }; }; diff --git a/webui-src/app/forums/forums.js b/webui-src/app/forums/forums.js index d3e6647..17ecc57 100644 --- a/webui-src/app/forums/forums.js +++ b/webui-src/app/forums/forums.js @@ -12,16 +12,18 @@ const getForums = { MyForums: [], async load() { const res = await rs.rsJsonApiRequest('/rsgxsforums/getForumsSummaries'); - getForums.All = res.body.forums; - getForums.PopularForums = getForums.All; - getForums.SubscribedForums = getForums.All.filter( - (forum) => - forum.mSubscribeFlags === util.GROUP_SUBSCRIBE_SUBSCRIBED || - forum.mSubscribeFlags === util.GROUP_MY_FORUM - ); - getForums.MyForums = getForums.All.filter( - (forum) => forum.mSubscribeFlags === util.GROUP_MY_FORUM - ); + if (res && res.body && res.body.forums) { + getForums.All = res.body.forums; + getForums.PopularForums = getForums.All; + getForums.SubscribedForums = getForums.All.filter( + (forum) => + forum.mSubscribeFlags === util.GROUP_SUBSCRIBE_SUBSCRIBED || + forum.mSubscribeFlags === util.GROUP_MY_FORUM + ); + getForums.MyForums = getForums.All.filter( + (forum) => forum.mSubscribeFlags === util.GROUP_MY_FORUM + ); + } }, }; const sections = { @@ -37,7 +39,7 @@ const Layout = () => { return { oninit: () => { rs.setBackgroundTask(getForums.load, 5000, () => { - // return m.route.get() === '/files/files'; + return m.route.get().includes('/forums'); }); peopleUtil.ownIds((data) => { ownId = data; @@ -71,14 +73,14 @@ const Layout = () => { ]), Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mMsgId') // thread's view ? m(viewUtil.ThreadView, { - msgId: vnode.attrs.pathInfo.mMsgId, - forumId: vnode.attrs.pathInfo.mGroupId, - }) + msgId: vnode.attrs.pathInfo.mMsgId, + forumId: vnode.attrs.pathInfo.mGroupId, + }) : Object.prototype.hasOwnProperty.call(vnode.attrs.pathInfo, 'mGroupId') // Forum's view - ? m(viewUtil.ForumView, { + ? m(viewUtil.ForumView, { id: vnode.attrs.pathInfo.mGroupId, }) - : m(sections[vnode.attrs.pathInfo.tab], { + : m(sections[vnode.attrs.pathInfo.tab], { list: getForums[vnode.attrs.pathInfo.tab], }), ]), diff --git a/webui-src/app/forums/forums_util.js b/webui-src/app/forums/forums_util.js index 5de22b6..c8dec2d 100644 --- a/webui-src/app/forums/forums_util.js +++ b/webui-src/app/forums/forums_util.js @@ -14,73 +14,99 @@ const Data = { Threads: {}, ParentThreads: {}, ParentThreadMap: {}, + loading: new Set(), }; async function updatedisplayforums(keyid, details = {}) { - const res = await rs.rsJsonApiRequest('/rsgxsforums/getForumsInfo', { - forumIds: [keyid], // keyid: Forumid - }); - details = res.body.forumsInfo[0]; - Data.DisplayForums[keyid] = { - // struct for a forum - name: details.mMeta.mGroupName, - author: details.mMeta.mAuthorId, - isSearched: true, - description: details.mDescription, - isSubscribed: - details.mMeta.mSubscribeFlags === GROUP_SUBSCRIBE_SUBSCRIBED || - details.mMeta.mSubscribeFlags === GROUP_MY_FORUM, - activity: details.mMeta.mLastPost, - created: details.mMeta.mPublishTs, - }; - if (Data.Threads[keyid] === undefined) { - Data.Threads[keyid] = {}; - } - const res2 = await rs.rsJsonApiRequest('/rsgxsforums/getForumMsgMetaData', { - forumId: keyid, - }); - if (res2.body.retval) { - res2.body.msgMetas.map(async (thread) => { - const res3 = await rs.rsJsonApiRequest('/rsgxsforums/getForumContent', { + if (Data.loading.has(keyid)) return; + Data.loading.add(keyid); + + try { + const res = await rs.rsJsonApiRequest('/rsgxsforums/getForumsInfo', { + forumIds: [keyid], // keyid: Forumid + }); + if (res && res.body && res.body.forumsInfo && res.body.forumsInfo.length > 0) { + details = res.body.forumsInfo[0]; + Data.DisplayForums[keyid] = { + // struct for a forum + name: details.mMeta.mGroupName, + author: details.mMeta.mAuthorId, + isSearched: true, + description: details.mDescription, + isSubscribed: + details.mMeta.mSubscribeFlags === GROUP_SUBSCRIBE_SUBSCRIBED || + details.mMeta.mSubscribeFlags === GROUP_MY_FORUM, + activity: details.mMeta.mLastPost, + created: details.mMeta.mPublishTs, + }; + if (Data.Threads[keyid] === undefined) { + Data.Threads[keyid] = {}; + } + const res2 = await rs.rsJsonApiRequest('/rsgxsforums/getForumMsgMetaData', { forumId: keyid, - msgsIds: [thread.mMsgId], }); + if (res2 && res2.body && res2.body.retval && res2.body.msgMetas && res2.body.msgMetas.length > 0) { + const ids = res2.body.msgMetas.map((thread) => thread.mMsgId); - if ( - res3.body.retval && - (Data.Threads[keyid][thread.mOrigMsgId] === undefined || - Data.Threads[keyid][thread.mOrigMsgId].thread.mMeta.mPublishTs.xint64 < - thread.mPublishTs.xint64) - // here we get the latest edited thread for each thread by comparing the publish time - ) { - Data.Threads[keyid][thread.mOrigMsgId] = { thread: res3.body.msgs[0], showReplies: false }; - if ( - Data.Threads[keyid][thread.mOrigMsgId] && - Data.Threads[keyid][thread.mOrigMsgId].thread.mMeta.mMsgStatus === THREAD_UNREAD - ) { - let parent = Data.Threads[keyid][thread.mOrigMsgId].thread.mMeta.mParentId; - while (Data.Threads[keyid][parent]) { - // to mark all parent threads of an inread thread - Data.Threads[keyid][parent].thread.mMeta.mMsgStatus = THREAD_UNREAD; - parent = Data.Threads[keyid][parent].thread.mMeta.mParentId; - } + // Chunking: process messages in batches of 100 + const chunkSize = 100; + const chunks = []; + for (let i = 0; i < ids.length; i += chunkSize) { + chunks.push(ids.slice(i, i + chunkSize)); } - if (Data.ParentThreads[keyid] === undefined) { - Data.ParentThreads[keyid] = {}; - } - if (thread.mThreadId === thread.mParentId) { - // top level thread. - Data.ParentThreads[keyid][thread.mOrigMsgId] = - Data.Threads[keyid][thread.mOrigMsgId].thread.mMeta; - } else { - if (Data.ParentThreadMap[thread.mParentId] === undefined) { - Data.ParentThreadMap[thread.mParentId] = {}; - } - Data.ParentThreadMap[thread.mParentId][thread.mOrigMsgId] = thread; - } + // Process chunks in parallel to speed up loading + await Promise.all( + chunks.map(async (chunk) => { + const res3 = await rs.rsJsonApiRequest('/rsgxsforums/getForumContent', { + forumId: keyid, + msgsIds: chunk, + }); + + if (res3 && res3.body && res3.body.retval && res3.body.msgs) { + res3.body.msgs.forEach((msg) => { + const mMeta = msg.mMeta; + if ( + Data.Threads[keyid][mMeta.mOrigMsgId] === undefined || + Data.Threads[keyid][mMeta.mOrigMsgId].thread.mMeta.mPublishTs.xint64 < + mMeta.mPublishTs.xint64 + ) { + Data.Threads[keyid][mMeta.mOrigMsgId] = { thread: msg, showReplies: false }; + if ( + Data.Threads[keyid][mMeta.mOrigMsgId] && + Data.Threads[keyid][mMeta.mOrigMsgId].thread.mMeta.mMsgStatus === THREAD_UNREAD + ) { + let parent = Data.Threads[keyid][mMeta.mOrigMsgId].thread.mMeta.mParentId; + while (Data.Threads[keyid][parent]) { + Data.Threads[keyid][parent].thread.mMeta.mMsgStatus = THREAD_UNREAD; + parent = Data.Threads[keyid][parent].thread.mMeta.mParentId; + } + } + + if (Data.ParentThreads[keyid] === undefined) { + Data.ParentThreads[keyid] = {}; + } + if (mMeta.mThreadId === mMeta.mParentId) { + Data.ParentThreads[keyid][mMeta.mOrigMsgId] = mMeta; + } else { + if (Data.ParentThreadMap[mMeta.mParentId] === undefined) { + Data.ParentThreadMap[mMeta.mParentId] = {}; + } + Data.ParentThreadMap[mMeta.mParentId][mMeta.mOrigMsgId] = mMeta; + } + } + }); + m.redraw(); // Show progress as chunks arrive + } + }) + ); } - }); + } + } catch (e) { + console.error('[RS] Error updating forum display for:', keyid, e); + } finally { + Data.loading.delete(keyid); + m.redraw(); // Final redraw just in case } } @@ -114,7 +140,7 @@ const ForumSummary = () => { keyid = v.attrs.details.mGroupId; updatedisplayforums(keyid); }, - view: (v) => {}, + view: (v) => { }, }; }; @@ -125,7 +151,7 @@ const ForumTable = () => { }; const ThreadsTable = () => { return { - oninit: (v) => {}, + oninit: (v) => { }, view: (v) => m('table.threads', [ m('tr', [m('th', 'Comment'), m('th', 'Date'), m('th', 'Author')]), @@ -135,7 +161,7 @@ const ThreadsTable = () => { }; const ThreadsReplyTable = () => { return { - oninit: (v) => {}, + oninit: (v) => { }, view: (v) => m('table.threadreply', [ m('tr', [ diff --git a/webui-src/app/home.js b/webui-src/app/home.js index 5baf921..bb9e932 100644 --- a/webui-src/app/home.js +++ b/webui-src/app/home.js @@ -117,52 +117,52 @@ function confirmAddPrompt(details, cert, long) { long ? m( - 'button', - { - onclick: async () => { - const res = await rs.rsJsonApiRequest('/rsPeers/loadCertificateFromString', { cert }); - if (res.body.retval) { - widget.popupMessage([ - m('h3', 'Successful'), - m('hr'), - m('p', 'Successfully added friend.'), - ]); - } else { - widget.popupMessage([ - m('h3', 'Error'), - m('hr'), - m('p', 'An error occoured during adding. Friend not added.'), - ]); - } - }, + 'button', + { + onclick: async () => { + const res = await rs.rsJsonApiRequest('/rsPeers/loadCertificateFromString', { cert }); + if (res.body.retval) { + widget.popupMessage([ + m('h3', 'Successful'), + m('hr'), + m('p', 'Successfully added friend.'), + ]); + } else { + widget.popupMessage([ + m('h3', 'Error'), + m('hr'), + m('p', 'An error occoured during adding. Friend not added.'), + ]); + } }, - 'Finish' - ) + }, + 'Finish' + ) : m( - 'button', - { - onclick: async () => { - const res = await rs.rsJsonApiRequest('/rsPeers/addSslOnlyFriend', { - sslId: details.id, - pgpId: details.gpg_id, - }); - if (res.body.retval) { - widget.popupMessage([ - m('h3', 'Successful'), - m('hr'), - m('p', 'Successfully added friend.'), - ]); - } else { - widget.popupMessage([ - m('h3', 'Error'), - m('hr'), - m('p', 'An error occoured during adding. Friend not added.'), - ]); - } - }, + 'button', + { + onclick: async () => { + const res = await rs.rsJsonApiRequest('/rsPeers/addSslOnlyFriend', { + sslId: details.id, + pgpId: details.gpg_id, + }); + if (res.body.retval) { + widget.popupMessage([ + m('h3', 'Successful'), + m('hr'), + m('p', 'Successfully added friend.'), + ]); + } else { + widget.popupMessage([ + m('h3', 'Error'), + m('hr'), + m('p', 'An error occoured during adding. Friend not added.'), + ]); + } }, - 'Finish' - ), + }, + 'Finish' + ), ]); } @@ -170,7 +170,7 @@ async function addFriendFromCert(cert) { const res = await rs.rsJsonApiRequest('/rsPeers/parseShortInvite', { invite: cert }); if (res.body.retval) { - console.log(res.body); + // console.log(res.body); confirmAddPrompt(res.body.details, cert, false); } else { rs.rsJsonApiRequest('/rsPeers/loadDetailsFromStringCert', { cert }, (data) => { diff --git a/webui-src/app/login.js b/webui-src/app/login.js index 1f5faca..3b967b2 100644 --- a/webui-src/app/login.js +++ b/webui-src/app/login.js @@ -16,7 +16,7 @@ const verifyLogin = async function (uname, passwd, url, displayAuthError = true) rs.setKeys('', '', url, false); rs.logon( loginHeader, - displayAuthError ? displayErrorMessage : () => {}, + displayAuthError ? displayErrorMessage : () => { }, displayErrorMessage, () => { rs.setKeys(uname, passwd, url); @@ -33,9 +33,9 @@ function loginComponent() { urlParams.get('Url') || window.location.protocol === 'file:' ? 'http://127.0.0.1:9092' : window.location.protocol + - '//' + - window.location.host + - window.location.pathname.replace('/index.html', ''); + '//' + + window.location.host + + window.location.pathname.replace('/index.html', ''); let withOptions = false; const logo = () => @@ -83,7 +83,13 @@ function loginComponent() { m('a', { onclick: () => (withOptions = !withOptions) }, `${action} options`); const textError = () => m('p.error[id=error]'); + return { + oninit: () => { + if (rs.loginKey.isVerified && rs.loginKey.username && rs.loginKey.passwd) { + verifyLogin(rs.loginKey.username, rs.loginKey.passwd, rs.loginKey.url, false); + } + }, view: () => { return m( 'form.login-page', @@ -91,14 +97,14 @@ function loginComponent() { '.login-container', withOptions ? [ - logo(), - m('.extra', [m('label', 'Username:'), m('br'), inputName()]), - m('.extra', [m('label', 'Password:'), m('br'), inputPassword()]), - m('.extra', [m('label', 'Url:'), m('br'), inputUrl()]), - linkOptions('hide'), - buttonLogin(), - textError(), - ] + logo(), + m('.extra', [m('label', 'Username:'), m('br'), inputName()]), + m('.extra', [m('label', 'Password:'), m('br'), inputPassword()]), + m('.extra', [m('label', 'Url:'), m('br'), inputUrl()]), + linkOptions('hide'), + buttonLogin(), + textError(), + ] : [logo(), inputPassword(), linkOptions('show'), buttonLogin(), textError()] ) ); diff --git a/webui-src/app/mail/mail_resolver.js b/webui-src/app/mail/mail_resolver.js index 4945375..7568d84 100644 --- a/webui-src/app/mail/mail_resolver.js +++ b/webui-src/app/mail/mail_resolver.js @@ -21,33 +21,27 @@ const Messages = { later: [], load() { rs.rsJsonApiRequest('/rsMail/getMessageSummaries', { box: util.BOX_ALL }, (data) => { - Messages.all = data.msgList; - Messages.inbox = Messages.all.filter( - (msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_INBOX - ); - Messages.sent = Messages.all.filter( - (msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_SENTBOX - ); - Messages.outbox = Messages.all.filter( - (msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_OUTBOX - ); - Messages.drafts = Messages.all.filter( - (msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_DRAFTBOX - ); - Messages.trash = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_TRASH); - Messages.starred = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_STAR); - Messages.system = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_SYSTEM); - Messages.spam = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_SPAM); + if (data && data.msgList) { + Messages.all = data.msgList; + Messages.inbox = Messages.all.filter( + (msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_INBOX + ); + Messages.sent = Messages.all.filter( + (msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_SENTBOX + ); + Messages.outbox = Messages.all.filter( + (msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_OUTBOX + ); + Messages.drafts = Messages.all.filter( + (msg) => (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_DRAFTBOX + ); + Messages.trash = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_TRASH); + Messages.starred = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_STAR); + Messages.system = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_SYSTEM); + 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.msgflags & util.RS_MSGTAGTYPE_IMPORTANT - // ); - // Messages.work = Messages.all.filter((msg) => msg.msgflags & util.RS_MSGTAGTYPE_WORK); - // Messages.personal = Messages.all.filter((msg) => msg.msgflags & util.RS_MSGTAGTYPE_PERSONAL); - // Messages.todo = Messages.all.filter((msg) => msg.msgflags & util.RS_MSGTAGTYPE_TODO); - // Messages.later = Messages.all.filter((msg) => msg.msgflags & util.RS_MSGTAGTYPE_LATER); + Messages.attachment = Messages.all.filter((msg) => msg.count); + } }); }, }; @@ -84,22 +78,22 @@ const Layout = () => { oninit: () => Messages.load(), view: (vnode) => { const sectionsSize = { - inbox: Messages.inbox.length, - outbox: Messages.outbox.length, - drafts: Messages.drafts.length, - sent: Messages.sent.length, - trash: Messages.trash.length, + inbox: (Messages.inbox || []).length, + outbox: (Messages.outbox || []).length, + drafts: (Messages.drafts || []).length, + sent: (Messages.sent || []).length, + trash: (Messages.trash || []).length, }; const sectionsQuickviewSize = { - starred: Messages.starred.length, - system: Messages.system.length, - spam: Messages.spam.length, - attachment: Messages.attachment.length, - important: Messages.important.length, - work: Messages.work.length, - todo: Messages.todo.length, - later: Messages.later.length, - personal: Messages.personal.length, + starred: (Messages.starred || []).length, + system: (Messages.system || []).length, + spam: (Messages.spam || []).length, + attachment: (Messages.attachment || []).length, + important: (Messages.important || []).length, + work: (Messages.work || []).length, + todo: (Messages.todo || []).length, + later: (Messages.later || []).length, + personal: (Messages.personal || []).length, }; return [ @@ -120,17 +114,17 @@ const Layout = () => { '.node-panel', m('.widget', [ m.route.get().split('/').length < 4 && - m('.top-heading', [ - m( - 'select.mail-tag', - { - value: tagselect.showval, - onchange: (e) => (tagselect.showval = tagselect.opts[e.target.selectedIndex]), - }, - [tagselect.opts.map((opt) => m('option', { value: opt }, opt.toLocaleString()))] - ), - m(util.SearchBar, { list: {} }), - ]), + m('.top-heading', [ + m( + 'select.mail-tag', + { + value: tagselect.showval, + onchange: (e) => (tagselect.showval = tagselect.opts[e.target.selectedIndex]), + }, + [tagselect.opts.map((opt) => m('option', { value: opt }, opt.toLocaleString()))] + ), + m(util.SearchBar, { list: {} }), + ]), vnode.children, ]) ), @@ -157,9 +151,9 @@ module.exports = { return m( Layout, m(sections[tab] || sectionsquickview[tab], { - list: Messages[tab].sort((msgA, msgB) => { - const msgADate = new Date(msgA.ts.xint64 * 1000); - const msgBDate = new Date(msgB.ts.xint64 * 1000); + 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; }), }) diff --git a/webui-src/app/mail/mail_util.js b/webui-src/app/mail/mail_util.js index 65e6fe4..ca5511f 100644 --- a/webui-src/app/mail/mail_util.js +++ b/webui-src/app/mail/mail_util.js @@ -128,7 +128,7 @@ const AttachmentSection = () => { m('i.fas.fa-file-medical'), m('h3', `File is ${status.retval ? 'being' : 'already'} downloaded!`), ]) - ).catch((error) => console.log('error: ', error)); + ).catch((error) => { }); } return { view: (v) => @@ -251,69 +251,69 @@ const MessageView = () => { m('h3', MailData.subject), m('.msg-details', [ 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() - : '', - }), + 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() + : '', + }), m('.msg-details__info', [ MailData.sender && - m('.msg-details__info-item', [ - m('b', 'From: '), - rs.userList.userMap[MailData.sender._addr_string] || 'Unknown', - ]), + m('.msg-details__info-item', [ + m('b', 'From: '), + rs.userList.userMap[MailData.sender._addr_string] || 'Unknown', + ]), m('.msg-details__info-item', [ m('b', 'To: '), MailData.toList && Object.keys(MailData.toList).length > 0 ? [ - m('#truncate.truncated-view', [ - Object.keys(MailData.toList).map((key, index) => - m('span', { key: index }, `${rs.userList.userMap[key] || 'Unknown'}, `) - ), - ]), - m( - 'button.toggle-truncate', - { - style: { - display: Object.keys(MailData.toList).length > 10 ? 'block' : 'none', - }, - onclick: () => { - document - .querySelector('#truncate') - .classList.toggle('truncated-view'); - }, - }, - '...' + m('#truncate.truncated-view', [ + Object.keys(MailData.toList).map((key, index) => + m('span', { key: index }, `${rs.userList.userMap[key] || 'Unknown'}, `) ), - ] + ]), + m( + 'button.toggle-truncate', + { + style: { + display: Object.keys(MailData.toList).length > 10 ? 'block' : 'none', + }, + onclick: () => { + document + .querySelector('#truncate') + .classList.toggle('truncated-view'); + }, + }, + '...' + ), + ] : m('span', 'Unknown'), ]), MailData.ccList && - Object.keys(MailData.ccList).length > 0 && - m('.msg-details__info-item', [ - m('b', 'Cc: '), - Object.keys(MailData.ccList).map((key, index) => - m('p', { key: index }, `${rs.userList.userMap[key]}, `) - ), - ]), + Object.keys(MailData.ccList).length > 0 && + m('.msg-details__info-item', [ + m('b', 'Cc: '), + Object.keys(MailData.ccList).map((key, index) => + m('p', { key: index }, `${rs.userList.userMap[key]}, `) + ), + ]), MailData.bccList && - Object.keys(MailData.bccList).length > 0 && - m('.msg-details__info-item', [ - m('b', 'Bcc: '), - Object.keys(MailData.bccList).map((key, index) => - m('p', { key: index }, `${rs.userList.userMap[key]}, `) - ), - ]), + Object.keys(MailData.bccList).length > 0 && + m('.msg-details__info-item', [ + m('b', 'Bcc: '), + Object.keys(MailData.bccList).map((key, index) => + m('p', { key: index }, `${rs.userList.userMap[key]}, `) + ), + ]), ]), ]), ]), m('.msg-view__body', m('#msgView')), MailData.files.length > 0 && - m('.msg-view__attachment', [ - m('h3', 'Attachments'), - m('.msg-view__attachment-items', m(AttachmentSection, { files: MailData.files })), - ]), + m('.msg-view__attachment', [ + m('h3', 'Attachments'), + m('.msg-view__attachment-items', m(AttachmentSection, { files: MailData.files })), + ]), ], m( '.composePopupOverlay#mailComposerPopup', @@ -322,14 +322,14 @@ const MessageView = () => { '.composePopup', MailData.sender._addr_string ? m(compose, { - msgType: 'reply', - senderId: MailData.sender._addr_string, - recipientList: MailData.toList, - subject: MailData.subject, - replyMessage: MailData.message, - timeStamp: new Date(MailData.timeStamp * 1000), - setShowCompose, - }) + msgType: 'reply', + senderId: MailData.sender._addr_string, + recipientList: MailData.toList, + subject: MailData.subject, + replyMessage: MailData.message, + timeStamp: new Date(MailData.timeStamp * 1000), + setShowCompose, + }) : m('.widget', m('.widget__heading', m('h3', 'Sender is not known'))), m('button.red.close-btn', { onclick: () => setShowCompose(false) }, m('i.fas.fa-times')) ) diff --git a/webui-src/app/main.js b/webui-src/app/main.js index fba7d8c..ea178aa 100644 --- a/webui-src/app/main.js +++ b/webui-src/app/main.js @@ -1,6 +1,7 @@ const m = require('mithril'); const login = require('login'); +const rs = require('rswebui'); const home = require('home'); const network = require('network/network'); const people = require('people/people_resolver'); @@ -36,24 +37,65 @@ const navbar = () => { }, [ m('.nav-menu__logo', [ - m('img', { - src: 'images/retroshare.svg', - alt: 'retroshare_icon', - }), - m('h5', 'Retroshare'), + m( + '.logo-container', + { + style: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + marginRight: '10px', + }, + }, + [ + m('img', { + 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' } }, 'v108'), + 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__box', [ - Object.keys(vnode.attrs.links).map((linkName, i) => { + Object.keys(vnode.attrs.links).map((linkName) => { const active = m.route.get().split('/')[1] === linkName; return m( m.route.Link, { href: vnode.attrs.links[linkName], - class: 'item' + (active ? ' item-selected' : ''), + class: (active ? 'active-link' : '') + ' item', }, - [navIcon[linkName], m('p', linkName)] + [ + 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', { @@ -157,3 +199,13 @@ m.route(document.getElementById('main'), '/', { render: (v) => m(Layout, m(config, v.attrs)), }, }); + +// v51 architectural fix: ensure event queue starts on direct route refresh +if (rs.loginKey.isVerified && rs.loginKey.username && rs.loginKey.passwd) { + rs.logon( + { Authorization: `Basic ${btoa(`${rs.loginKey.username}:${rs.loginKey.passwd}`)}` }, + () => { }, // displayAuthError + () => { }, // displayErrorMessage + () => { } + ); +} diff --git a/webui-src/app/people/people_ownids.js b/webui-src/app/people/people_ownids.js index 9f17946..ae02731 100644 --- a/webui-src/app/people/people_ownids.js +++ b/webui-src/app/people/people_ownids.js @@ -24,31 +24,28 @@ const SignedIdentiy = () => { style: 'margin-top:160px;', onclick: () => { rs.rsJsonApiRequest('/rsIdentity/getOwnSignedIds', {}, (owns) => { - console.log(owns.ids[0]); - console.log(v.attrs.name); owns.ids.length > 0 ? rs.rsJsonApiRequest( - '/rsIdentity/createIdentity', - { - id: owns.ids[0], - name: v.attrs.name, - pseudonimous: false, - pgpPassword: passphase, - }, - (data) => { - const message = data.retval - ? 'Successfully created identity.' - : 'An error occured while creating identity.'; - console.log(message); - widget.popupMessage([m('h3', 'Create new Identity'), m('hr'), message]); - } - ) + '/rsIdentity/createIdentity', + { + id: owns.ids[0], + name: v.attrs.name, + pseudonimous: false, + pgpPassword: passphase, + }, + (data) => { + const message = data.retval + ? 'Successfully created identity.' + : 'An error occured while creating identity.'; + widget.popupMessage([m('h3', 'Create new Identity'), m('hr'), message]); + } + ) : widget.popupMessage([ - m('h3', 'Create new Identity'), - m('hr'), - 'An error occured while creating identity.', - ]); + m('h3', 'Create new Identity'), + m('hr'), + 'An error occured while creating identity.', + ]); }); }, }, @@ -84,7 +81,6 @@ const CreateIdentity = () => { style: 'border:1px solid black', oninput: (e) => { pseudonimous = e.target.value === 'true'; - console.log(pseudonimous); }, }, [ @@ -99,10 +95,10 @@ const CreateIdentity = () => { m( 'p', 'You can have one or more identities. ' + - 'They are used when you chat in lobbies, ' + - 'forums and channel comments. ' + - 'They act as the destination for distant chat and ' + - 'the Retroshare distant mail system.' + 'They are used when you chat in lobbies, ' + + 'forums and channel comments. ' + + 'They act as the destination for distant chat and ' + + 'the Retroshare distant mail system.' ), m( 'button', @@ -111,18 +107,18 @@ const CreateIdentity = () => { !pseudonimous ? widget.popupMessage(m(SignedIdentiy, { name })) : rs.rsJsonApiRequest( - '/rsIdentity/createIdentity', - { - name, - pseudonimous, - }, - (data) => { - const message = data.retval - ? 'Successfully created identity.' - : 'An error occured while creating identity.'; - widget.popupMessage([m('h3', 'Create new Identity'), m('hr'), message]); - } - ); + '/rsIdentity/createIdentity', + { + name, + pseudonimous, + }, + (data) => { + const message = data.retval + ? 'Successfully created identity.' + : 'An error occured while creating identity.'; + widget.popupMessage([m('h3', 'Create new Identity'), m('hr'), message]); + } + ); }, }, 'Create' @@ -192,28 +188,28 @@ const EditIdentity = () => { onclick: () => { !peopleUtil.checksudo(v.attrs.details.mPgpId) ? widget.popupMessage([ - m(SignedEditIdentity, { - name, - details: v.attrs.details, - }), - ]) + m(SignedEditIdentity, { + name, + details: v.attrs.details, + }), + ]) : rs.rsJsonApiRequest( - '/rsIdentity/updateIdentity', - { - id: v.attrs.details.mId, + '/rsIdentity/updateIdentity', + { + id: v.attrs.details.mId, - name, + name, - // avatar: v.attrs.details.mAvatar.mData.base64, - pseudonimous: true, - }, - (data) => { - const message = data.retval - ? 'Successfully Updated identity.' - : 'An error occured while updating identity.'; - widget.popupMessage([m('h3', 'Update Identity'), m('hr'), message]); - } - ); + // avatar: v.attrs.details.mAvatar.mData.base64, + pseudonimous: true, + }, + (data) => { + const message = data.retval + ? 'Successfully Updated identity.' + : 'An error occured while updating identity.'; + widget.popupMessage([m('h3', 'Update Identity'), m('hr'), message]); + } + ); }, }, 'Save' @@ -277,10 +273,10 @@ const Identity = () => { [ m('h4', details.mNickname), details.mNickname && - m(peopleUtil.UserAvatar, { - avatar: details.mAvatar, - firstLetter: details.mNickname.slice(0, 1).toUpperCase(), - }), + m(peopleUtil.UserAvatar, { + avatar: details.mAvatar, + firstLetter: details.mNickname.slice(0, 1).toUpperCase(), + }), m('.details', [ m('p', 'ID:'), m('p', details.mId), @@ -303,6 +299,16 @@ const Identity = () => { : 'undefiend' ), ]), + m( + 'button', + { + onclick: () => + m.route.set('/chat/:userid/createdistantchat', { + userid: details.mId, + }), + }, + 'Chat' + ), m( 'button', { diff --git a/webui-src/app/people/people_util.js b/webui-src/app/people/people_util.js index 6cbf320..80c4322 100644 --- a/webui-src/app/people/people_util.js +++ b/webui-src/app/people/people_util.js @@ -10,33 +10,28 @@ const UserAvatar = () => ({ 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) - ) + 'div.defaultAvatar', + { + // image isn't getting loaded + // ? m('img.defaultAvatar', { + // src: '../data/user.png' + // }) + }, + m('p', v.attrs.firstLetter) + ) : m('img.avatar', { - src: 'data:image/png;base64,' + imageURI.mData.base64, - }); + src: 'data:image/png;base64,' + imageURI.mData.base64, + }); }, }); function contactlist(list) { - const result = []; - if (list !== undefined) { - list.map((id) => { - id.isSearched = true; - rs.rsJsonApiRequest('/rsIdentity/isARegularContact', { id: id.mGroupId }, (data) => { - if (data.retval) result.push(id); - console.log(data); - }); - }); - } - return result; + if (list === undefined) return []; + return list.filter((id) => { + id.isSearched = true; + const entry = rs.userList.userMap[id.mGroupId]; + return entry && entry.isContact; + }); } function sortUsers(list) { @@ -63,7 +58,7 @@ function sortIds(list) { return list; } -async function ownIds(consumer = () => {}, onlySigned = false) { +async function ownIds(consumer = () => { }, onlySigned = false) { await rs.rsJsonApiRequest('/rsIdentity/getOwnSignedIds', {}, (owns) => { if (onlySigned) { consumer(sortIds(owns.ids)); @@ -122,10 +117,10 @@ const regularcontactInfo = () => { [ m('h4', details.mNickname), details.mNickname && - m(UserAvatar, { - avatar: details.mAvatar, - firstLetter: details.mNickname.slice(0, 1).toUpperCase(), - }), + m(UserAvatar, { + avatar: details.mAvatar, + firstLetter: details.mNickname.slice(0, 1).toUpperCase(), + }), m('.details', [ m('p', 'ID:'), m('p', details.mId), diff --git a/webui-src/app/rswebui.js b/webui-src/app/rswebui.js index 7fa22ed..97a84c2 100644 --- a/webui-src/app/rswebui.js +++ b/webui-src/app/rswebui.js @@ -68,10 +68,10 @@ const RsEventsType = { const API_URL = 'http://127.0.0.1:9092'; const loginKey = { - username: '', - passwd: '', - isVerified: false, - url: API_URL, + username: localStorage.getItem('rs_username') || '', + passwd: localStorage.getItem('rs_passwd') || '', + isVerified: localStorage.getItem('rs_isVerified') === 'true', + url: localStorage.getItem('rs_url') || API_URL, }; // Make this as object property? @@ -80,12 +80,30 @@ function setKeys(username, password, url = API_URL, verified = true) { loginKey.passwd = password; loginKey.url = url; loginKey.isVerified = verified; + + if (verified) { + localStorage.setItem('rs_username', username); + localStorage.setItem('rs_passwd', password); + localStorage.setItem('rs_url', url); + localStorage.setItem('rs_isVerified', 'true'); + } else { + localStorage.removeItem('rs_isVerified'); + } } +function logout() { + setKeys('', '', loginKey.url, false); + m.route.set('/'); +} + +const connectionState = { + status: true, +}; + function rsJsonApiRequest( path, data = {}, - callback = () => {}, + callback = () => { }, async = true, headers = {}, handleDeserialize = JSON.parse, @@ -94,7 +112,9 @@ function rsJsonApiRequest( ) { headers['Accept'] = 'application/json'; if (loginKey.isVerified) { - headers['Authorization'] = 'Basic ' + btoa(loginKey.username + ':' + loginKey.passwd); + if (loginKey.username && loginKey.passwd) { + headers['Authorization'] = 'Basic ' + btoa(loginKey.username + ':' + loginKey.passwd); + } } // NOTE: After upgrading to mithrilv2, options.extract is no longer required // since the status will become part of return value and then @@ -117,23 +137,38 @@ function rsJsonApiRequest( headers, body: data, - config, + xhr: config, }) .then((result) => { if (result.status === 200) { - callback(result.body, true); + connectionState.status = true; + try { + callback(result.body, true); + } catch (e) { + console.error('[RS] Error in success callback for path:', path, e); + } } else { - if(result.status === 403 || result.status === 401) - loginKey.isVerified = false; - callback(result, false); - m.route.set('/'); + connectionState.status = false; + if (result.status === 401 || result.status === 403) { + setKeys(loginKey.username, loginKey.passwd, loginKey.url, false); + m.route.set('/'); + } + try { + callback(result, false); + } catch (e) { + console.error('[RS] Error in error callback for path:', path, e); + } } return result; }) .catch(function (e) { - callback(e, false); - console.error('Error: While sending request for path:', path, '\ninfo:', e); - m.route.set('/'); + connectionState.status = false; + try { + callback(e, false); + } catch (cbErr) { + // console.error('[RS] Error in catch callback for path:', path, cbErr); + } + console.error('[RS] Error: While sending request for path:', path, '\ninfo:', e); }); } @@ -176,10 +211,10 @@ const eventQueue = { // #define RS_CHAT_TYPE_PUBLIC 1 // #define RS_CHAT_TYPE_PRIVATE 2 - 2: (chatId) => chatId.distant_chat_id, // distant chat (initiate? -> todo accept) - // #define RS_CHAT_TYPE_LOBBY 3 - 3: (chatId) => chatId.lobby_id.xstr64, // lobby_id - // #define RS_CHAT_TYPE_DISTANT 4 + 1: (cid) => hexId(cid), + 2: (cid) => hexId(cid), + 3: (cid) => hexId(cid), + 4: (cid) => hexId(cid), }, messages: {}, chatMessages: (chatId, owner, action) => { @@ -195,25 +230,36 @@ const eventQueue = { ) ) ) { - console.info('unknown chat event', chatId); + if (chatId) { + // Silent match + } + } + }, + handler: (event, owner) => { + if (event && event.mChatMessage && event.mChatMessage.chat_id) { + owner.chatMessages(event.mChatMessage.chat_id, owner, (r) => { + r.push(event.mChatMessage); + owner.notify(event.mChatMessage); + }); + } else if (event && event.mCid) { + // Administrative chat event (e.g. lobby info change, peer join/leave) + // Silent for now to avoid console spam, as actual messages use mChatMessage } }, - handler: (event, owner) => - owner.chatMessages(event.mChatMessage.chat_id, owner, (r) => { - console.info('adding chat', r, event.mChatMessage); - r.push(event.mChatMessage); - owner.notify(event.mChatMessage); - }), - notify: () => {}, + notify: () => { }, }, [RsEventsType.GXS_CIRCLES]: { // Circles (ignore in the meantime) - handler: (event, owner) => {}, + handler: (event, owner) => { }, + }, + [RsEventsType.SHARED_DIRECTORIES]: { + // Deprecated/Administrative (ignore quietly) + handler: (event, owner) => { }, }, }, handler: (event) => { if (!deeperIfExist(eventQueue.events, event.mType, (owner) => owner.handler(event, owner))) { - console.info('unhandled event', event); + // Ignore unhandled events silently } }, }; @@ -221,20 +267,71 @@ const eventQueue = { const userList = { users: [], userMap: {}, + pendingIds: new Set(), + fetchTimer: null, + + triggerFetch: () => { + if (userList.fetchTimer) return; + userList.fetchTimer = setTimeout(() => { + userList.fetchTimer = null; + if (userList.pendingIds.size === 0) return; + + const ids = Array.from(userList.pendingIds); + userList.pendingIds.clear(); + + userList.fetchBulk(ids); + }, 1000); + }, + + fetchBulk: (ids) => { + // Chunk requests to avoid too large payloads if necessary, but for now 100 is safe + const chunkSize = 100; + for (let i = 0; i < ids.length; i += chunkSize) { + const chunk = ids.slice(i, i + chunkSize); + rsJsonApiRequest('/rsIdentity/getIdentitiesInfo', { ids: chunk }, (data, success) => { + if (success && data.idsInfo) { + data.idsInfo.forEach((info) => { + const gid = info.mMeta && info.mMeta.mGroupId; + if (gid) { + userList.userMap[gid] = { + name: info.mMeta.mGroupName, + isContact: info.mIsAContact, + }; + } + }); + m.redraw(); + } + }); + } + }, + loadUsers: () => { rsJsonApiRequest('/rsIdentity/getIdentitiesSummaries', {}, (list) => { - if (list !== undefined) { - console.info('loading ' + list.ids.length + ' users ...'); + if (list !== undefined && list.ids) { userList.users = list.ids; userList.userMap = list.ids.reduce((a, c) => { - a[c.mGroupId] = c.mGroupName; + a[c.mGroupId] = { name: c.mGroupName, isContact: false }; return a; }, {}); + + // Fetch contact status and details in bulk immediately + userList.fetchBulk(list.ids.map((u) => u.mGroupId)); } }); }, username: (id) => { - return userList.userMap[id] || id; + if (!id) return ''; + const entry = userList.userMap[id]; + const name = typeof entry === 'object' ? entry.name : entry; + + if (!name && id.length > 10) { + if (!userList.pendingIds.has(id)) { + userList.pendingIds.add(id); + userList.triggerFetch(); + } + return id; + } + return name || id; }, }; @@ -251,70 +348,91 @@ const userList = { function startEventQueue( info, loginHeader = {}, - displayAuthError = () => {}, - displayErrorMessage = () => {}, - successful = () => {} + displayAuthError = () => { }, + displayErrorMessage = () => { }, + successful = () => { } ) { - return rsJsonApiRequest( - '/rsEvents/registerEventsHandler', - {}, - (data, success) => { - if (success) { - // unused - } else if (data.status === 401) { + const xhr = new window.XMLHttpRequest(); + let lastIndex = 0; + xhr.open('POST', loginKey.url + '/rsEvents/registerEventsHandler', true); + + // Set headers for authentication + const headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + ...loginHeader, + }; + + if (loginKey.isVerified && !headers['Authorization']) { + if (loginKey.username && loginKey.passwd) { + headers['Authorization'] = 'Basic ' + btoa(loginKey.username + ':' + loginKey.passwd); + } + } + + Object.keys(headers).forEach((key) => { + xhr.setRequestHeader(key, headers[key]); + }); + + xhr.onreadystatechange = () => { + if (xhr.readyState === 4) { + if (xhr.status === 401) { displayAuthError('Incorrect login/password.'); - } else if (data.status === 0) { - displayErrorMessage([ - 'Retroshare-jsonapi not available.', - m('br'), - 'Please fix host and/or port.', - ]); - } else { - displayErrorMessage('Login failed: HTTP ' + data.status + ' ' + data.statusText); } - }, - true, - loginHeader, - JSON.parse, - JSON.stringify, - (xhr, args, url) => { - let lastIndex = 0; - xhr.onprogress = (ev) => { - const currIndex = xhr.responseText.length; - if (currIndex > lastIndex) { - const parts = xhr.responseText.substring(lastIndex, currIndex); - lastIndex = currIndex; - parts - .trim() - .split('\n\n') - .filter((e) => e.startsWith('data: {')) - .map((e) => e.substr(6)) - .map(JSON.parse) - .forEach((data) => { + } + }; + + xhr.onprogress = (ev) => { + const currIndex = xhr.responseText.length; + if (currIndex > lastIndex) { + const parts = xhr.responseText.substring(lastIndex, currIndex); + lastIndex = currIndex; + parts + .trim() + .split('\n\n') + .filter((e) => e.trim().length > 0) + .forEach((e) => { + if (e.startsWith('data: {')) { + try { + const data = JSON.parse(e.substr(6)); if (Object.prototype.hasOwnProperty.call(data, 'retval')) { - console.info( - info + ' [' + data.retval.errorCategory + '] ' + data.retval.errorMessage - ); - data.retval.errorNumber === 0 - ? successful() - : displayErrorMessage( - `${info} failed: [${data.retval.errorCategory}] ${data.retval.errorMessage}` - ); + if (data.retval.errorNumber !== 0) { + displayErrorMessage( + `${info} failed: [${data.retval.errorCategory}] ${data.retval.errorMessage}` + ); + } else { + successful(); + } } else if (Object.prototype.hasOwnProperty.call(data, 'event')) { data.event.queueSize = currIndex; - eventQueue.handler(data.event); + try { + eventQueue.handler(data.event); + } catch (err) { + console.error('[RS] Error in event handler:', err, data.event); + } } - }); - if (currIndex > 1e5) { - // max 100 kB eventQueue - startEventQueue('restart queue'); - xhr.abort(); + } catch (err) { + console.error('[RS] JSON parse error for part:', e, err); + } } - } - }; - return xhr; + }); + if (currIndex > 1e6) { + // max 1 MB eventQueue + startEventQueue('restart queue'); + xhr.abort(); + } } - ); + }; + + xhr.onload = () => { }; + + xhr.onerror = (err) => { + console.error('[RS] Event Queue XHR error occurred:', err); + }; + + // We need to send an eventType to registerEventsHandler + // 0 means all events + xhr.send(JSON.stringify({ eventType: 0 })); + return xhr; } function logon(loginHeader, displayAuthError, displayErrorMessage, successful) { @@ -333,8 +451,32 @@ function formatBytes(bytes, decimals = 2) { return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } +function hexId(id) { + if (!id) return ''; + if (typeof id === 'string') return id; + if (typeof id === 'number') return String(id); + if (typeof id === 'object') { + // 1. Check for xstr64 (64-bit wrapped ID) + if (id.xstr64 && id.xstr64 !== '0') return id.xstr64; + + // 2. Search for any hex string of appropriate length (128-bit or 64-bit) + const keys = Object.keys(id); + for (let i = 0; i < keys.length; i++) { + const val = id[keys[i]]; + if (typeof val === 'string' && val.length >= 16 && val !== '00000000000000000000000000000000') return val; + // Search deeper for nested xstr64 + if (val && typeof val === 'object' && val.xstr64 && val.xstr64 !== '0') return val.xstr64; + } + // 3. Last resort fallbacks + if (id.xstr64 !== undefined) return String(id.xstr64); + } + return String(id); +} + module.exports = { rsJsonApiRequest, + idToHex: hexId, + connectionState, setKeys, setBackgroundTask, logon, @@ -343,4 +485,5 @@ module.exports = { userList, loginKey, formatBytes, + logout, }; diff --git a/webui-src/app/scss/pages/_chat.scss b/webui-src/app/scss/pages/_chat.scss index 03b3f60..3b06260 100644 --- a/webui-src/app/scss/pages/_chat.scss +++ b/webui-src/app/scss/pages/_chat.scss @@ -14,7 +14,7 @@ color: #666; } -.lobby > .topic { +.lobby>.topic { font-size: 0.95em; margin-left: 25px; margin-bottom: 5px; @@ -35,7 +35,7 @@ font-size: 1em; } -.leftlobby > .topic { +.leftlobby>.topic { font-size: 0.75em; margin-left: 15px; margin-bottom: 5px; @@ -106,7 +106,7 @@ margin-right: 5px; } -.message > * { +.message>* { margin-left: 5px; } @@ -151,7 +151,7 @@ textarea.chatMsg { font-size: 1.2em; } -.setup > .identity { +.setup>.identity { cursor: pointer; } @@ -162,3 +162,12 @@ textarea.chatMsg { .createDistantChat { margin-top: 1em; } + +.no-lobbies { + + .messages, + .chatMessage, + .setup { + left: 165px; + } +} \ No newline at end of file diff --git a/webui-src/pnpm-lock.yaml b/webui-src/pnpm-lock.yaml index 583dde5..9dbf4bd 100644 --- a/webui-src/pnpm-lock.yaml +++ b/webui-src/pnpm-lock.yaml @@ -1,132 +1,240 @@ -lockfileVersion: '6.0' +lockfileVersion: '9.0' -devDependencies: - sass: - specifier: ^1.60.0 - version: 1.60.0 +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false -packages: +importers: - /anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - dev: true + .: + devDependencies: + sass: + specifier: ^1.60.0 + version: 1.97.3 - /binary-extensions@2.2.0: - resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} - engines: {node: '>=8'} - dev: true - - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} - dependencies: - fill-range: 7.0.1 - dev: true - - /chokidar@3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} - engines: {node: '>= 8.10.0'} - dependencies: - anymatch: 3.1.3 - braces: 3.0.2 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.2 - dev: true +packages: - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} - dependencies: - to-regex-range: 5.0.1 - dev: true + '@parcel/watcher-android-arm64@2.5.6': + resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] - /fsevents@2.3.2: - resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + '@parcel/watcher-darwin-arm64@2.5.6': + resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] os: [darwin] - requiresBuild: true - dev: true - optional: true - /glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - dependencies: - is-glob: 4.0.3 - dev: true - - /immutable@4.3.0: - resolution: {integrity: sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==} - dev: true + '@parcel/watcher-darwin-x64@2.5.6': + resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] - /is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + '@parcel/watcher-freebsd-x64@2.5.6': + resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.6': + resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.6': + resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.6': + resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.6': + resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.6': + resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.6': + resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.6': + resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} + engines: {node: '>= 10.0.0'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - dependencies: - binary-extensions: 2.2.0 - dev: true - /is-extglob@2.1.1: + immutable@5.1.5: + resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} + + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - dev: true - /is-glob@4.0.3: + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - dependencies: - is-extglob: 2.1.1 - dev: true - /is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - dev: true + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} - /normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + sass@1.97.3: + resolution: {integrity: sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==} + engines: {node: '>=14.0.0'} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - dev: true - /picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - dev: true +snapshots: + + '@parcel/watcher-android-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-x64@2.5.6': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.6': + optional: true + + '@parcel/watcher-win32-arm64@2.5.6': + optional: true + + '@parcel/watcher-win32-ia32@2.5.6': + optional: true + + '@parcel/watcher-win32-x64@2.5.6': + optional: true + + '@parcel/watcher@2.5.6': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.3 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.6 + '@parcel/watcher-darwin-arm64': 2.5.6 + '@parcel/watcher-darwin-x64': 2.5.6 + '@parcel/watcher-freebsd-x64': 2.5.6 + '@parcel/watcher-linux-arm-glibc': 2.5.6 + '@parcel/watcher-linux-arm-musl': 2.5.6 + '@parcel/watcher-linux-arm64-glibc': 2.5.6 + '@parcel/watcher-linux-arm64-musl': 2.5.6 + '@parcel/watcher-linux-x64-glibc': 2.5.6 + '@parcel/watcher-linux-x64-musl': 2.5.6 + '@parcel/watcher-win32-arm64': 2.5.6 + '@parcel/watcher-win32-ia32': 2.5.6 + '@parcel/watcher-win32-x64': 2.5.6 + optional: true - /readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} + chokidar@4.0.3: dependencies: - picomatch: 2.3.1 - dev: true + readdirp: 4.1.2 - /sass@1.60.0: - resolution: {integrity: sha512-updbwW6fNb5gGm8qMXzVO7V4sWf7LMXnMly/JEyfbfERbVH46Fn6q02BX7/eHTdKpE7d+oTkMMQpFWNUMfFbgQ==} - engines: {node: '>=12.0.0'} - hasBin: true + detect-libc@2.1.2: + optional: true + + immutable@5.1.5: {} + + is-extglob@2.1.1: + optional: true + + is-glob@4.0.3: dependencies: - chokidar: 3.5.3 - immutable: 4.3.0 - source-map-js: 1.0.2 - dev: true + is-extglob: 2.1.1 + optional: true - /source-map-js@1.0.2: - resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} - engines: {node: '>=0.10.0'} - dev: true + node-addon-api@7.1.1: + optional: true - /to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} + picomatch@4.0.3: + optional: true + + readdirp@4.1.2: {} + + sass@1.97.3: dependencies: - is-number: 7.0.0 - dev: true + chokidar: 4.0.3 + immutable: 5.1.5 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.6 + + source-map-js@1.2.1: {} diff --git a/webui-src/styles.css b/webui-src/styles.css index ce1517a..c278882 100644 --- a/webui-src/styles.css +++ b/webui-src/styles.css @@ -4,4 +4,4 @@ */.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 #005f9a}button:active{outline:none;box-shadow:inset 3px 3px 0 #005f9a}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 #d30011}button.red:active{outline:none;box-shadow:inset 3px 3px 0 #d30011}.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%}.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:#4ebbff;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 h5{display:none}.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 p{display:none}.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}}.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}.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:#4f7b96}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{display:flex;justify-content:space-between}.manage-visibility label{width:100%;cursor:pointer}.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%}.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 h5{display:none}.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 p{display:none}.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}}.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}.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}.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} diff --git a/webui.pro b/webui.pro index 25c8f77..9d14a65 100644 --- a/webui.pro +++ b/webui.pro @@ -72,7 +72,7 @@ win32-g++ { } else { # create dummy files, we need it to include files on first try - system(webui-src/make-src/build.sh .) + system(bash webui-src/make-src/build.sh .) WEBUI_SRC_SCRIPT = webui-src/make-src/build.sh @@ -80,19 +80,19 @@ win32-g++ { create_webfiles_html.output = webui/index.html create_webfiles_html.input = WEBUI_SRC_HTML - create_webfiles_html.commands = sh $$_PRO_FILE_PWD_/webui-src/make-src/build.sh $$_PRO_FILE_PWD_ index.html . + create_webfiles_html.commands = bash $$_PRO_FILE_PWD_/webui-src/make-src/build.sh $$_PRO_FILE_PWD_ index.html . create_webfiles_html.variable_out = JUNK create_webfiles_html.CONFIG = combine no_link create_webfiles_js.output = webui/app.js create_webfiles_js.input = WEBUI_SRC_JS - create_webfiles_js.commands = sh $$_PRO_FILE_PWD_/webui-src/make-src/build.sh $$_PRO_FILE_PWD_ app.js . + create_webfiles_js.commands = bash $$_PRO_FILE_PWD_/webui-src/make-src/build.sh $$_PRO_FILE_PWD_ app.js . create_webfiles_js.variable_out = JUNK create_webfiles_js.CONFIG = combine no_link create_webfiles_css.output = webui/styles.css create_webfiles_css.input = WEBUI_SRC_CSS - create_webfiles_css.commands = sh $$_PRO_FILE_PWD_/webui-src/make-src/build.sh $$_PRO_FILE_PWD_ styles.css . + create_webfiles_css.commands = bash $$_PRO_FILE_PWD_/webui-src/make-src/build.sh $$_PRO_FILE_PWD_ styles.css . create_webfiles_css.variable_out = JUNK create_webfiles_css.CONFIG = combine no_link