diff --git a/webui-src/app/boards/board_kanban.js b/webui-src/app/boards/board_kanban.js new file mode 100644 index 00000000..b3ed06a9 --- /dev/null +++ b/webui-src/app/boards/board_kanban.js @@ -0,0 +1,694 @@ +const m = require('mithril'); +const util = require('boards/boards_util'); + +const PAGE_SIZE = 25; + +function numberValue(value) { + if (value && typeof value === 'object' && value.xint64 !== undefined) value = value.xint64; + const number = Number(value); + return Number.isFinite(number) ? number : 0; +} + +/** + * Fallback SVG Thumbnail when no image is available + */ +const FallbackImage = () => + m('.board-card__placeholder-content', { + style: { + display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', + gap: '.3rem', color: '#64748b', fontSize: '.72rem', fontWeight: '600', textAlign: 'center', + }, + }, [ + m('i.fas.fa-image[aria-hidden=true]', { style: { fontSize: '1.35rem' } }), + m('span', 'No image'), + ]); + +/** + * Check if notes string contains non-whitespace text + */ +function hasNotesText(notes) { + if (notes === null || notes === undefined) return false; + if (typeof notes !== 'string') notes = String(notes); + return notes.trim().length > 0; +} + +/** + * Robust image extraction helper for RetroShare post items + */ +function extractImageSrc(item) { + if (!item) return ''; + const p = item.post || item; + + if (item.thumbnail && typeof item.thumbnail === 'string' && item.thumbnail.trim() !== '') { + return item.thumbnail.startsWith('data:') ? item.thumbnail : `data:image/png;base64,${item.thumbnail}`; + } + if (item.image && typeof item.image === 'string' && item.image.trim() !== '') { + return item.image.startsWith('data:') ? item.image : `data:image/png;base64,${item.image}`; + } + if (p.mImage) { + if (p.mImage.mData && p.mImage.mData.base64 && p.mImage.mData.base64.trim() !== '') { + return `data:image/png;base64,${p.mImage.mData.base64}`; + } + if (typeof p.mImage.base64 === 'string' && p.mImage.base64.trim() !== '') { + return `data:image/png;base64,${p.mImage.base64}`; + } + if (typeof p.mImage === 'string' && p.mImage.trim() !== '') { + return p.mImage.startsWith('data:') ? p.mImage : `data:image/png;base64,${p.mImage}`; + } + } + if (p.mThumbnail) { + if (p.mThumbnail.mData && p.mThumbnail.mData.base64 && p.mThumbnail.mData.base64.trim() !== '') { + return `data:image/png;base64,${p.mThumbnail.mData.base64}`; + } + if (typeof p.mThumbnail.base64 === 'string' && p.mThumbnail.base64.trim() !== '') { + return `data:image/png;base64,${p.mThumbnail.base64}`; + } + if (typeof p.mThumbnail === 'string' && p.mThumbnail.trim() !== '') { + return p.mThumbnail.startsWith('data:') ? p.mThumbnail : `data:image/png;base64,${p.mThumbnail}`; + } + } + + // Check notes/body text for embedded data:image or web URL + const text = p.mNotes || p.mBody || item.notes || item.body || ''; + if (typeof text === 'string') { + const dataMatch = text.match(/data:image\/[a-zA-Z]+;base64,[^"\s)]+/); + if (dataMatch) return dataMatch[0]; + const urlMatch = text.match(/https?:\/\/[^\s")<]+\.(?:png|jpg|jpeg|gif|webp)/i); + if (urlMatch) return urlMatch[0]; + } + + return ''; +} + +/** + * Dedicated fullscreen photo overlay appended directly to document.body. + * Bypasses #modal-container entirely so z-index is guaranteed. + */ +let _photoOverlayEl = null; + +function getPhotoOverlay() { + if (!_photoOverlayEl) { + _photoOverlayEl = document.createElement('div'); + _photoOverlayEl.id = 'photo-view-overlay'; + document.body.appendChild(_photoOverlayEl); + } + return _photoOverlayEl; +} + +function closePhotoOverlay() { + if (_photoOverlayEl) { + _photoOverlayEl.style.display = 'none'; + m.render(_photoOverlayEl, null); + } +} + +/** + * PhotoView Lightbox — Qt GUI style: nav arrows outside the image in a 3-col flex row + */ +function PhotoViewModal() { + let currentIndex = 0; + + function navigate(photoList, newIndex) { + currentIndex = newIndex; + m.render(getPhotoOverlay(), m(PhotoViewModal, { + photoList, + photoIndex: currentIndex, + })); + } + + return { + oninit: (vnode) => { + currentIndex = vnode.attrs.photoIndex || 0; + }, + view: (vnode) => { + const { photoList = [] } = vnode.attrs; + if (!photoList || photoList.length === 0) return null; + + if (currentIndex < 0) currentIndex = 0; + if (currentIndex >= photoList.length) currentIndex = photoList.length - 1; + + const currentItem = photoList[currentIndex]; + if (!currentItem) return null; + + const p = currentItem.post || currentItem; + const meta = (p && p.mMeta) ? p.mMeta : (currentItem.mMeta || {}); + const title = currentItem.title || meta.mMsgName || 'Photo View'; + const imgSrc = extractImageSrc(currentItem); + const author = meta.mAuthorId ? meta.mAuthorId.substring(0, 10) : 'Unknown'; + const publishTs = meta.mPublishTs || currentItem.created; + const dateStr = publishTs + ? (typeof publishTs === 'object' && publishTs.xint64 + ? new Date(publishTs.xint64 * 1000).toLocaleString() + : new Date(publishTs * 1000).toLocaleString()) + : ''; + + const hasPrev = currentIndex > 0; + const hasNext = currentIndex < photoList.length - 1; + + return m('.photo-view-dialog', [ + // Header: italic title + X close (Qt style) + m('.photo-view-header', [ + m('h3.photo-view-title', title), + m('button.photo-view-close-btn', { + type: 'button', + onclick: closePhotoOverlay, + title: 'Close', + }, '\u00d7'), + ]), + + // Body: 3-column flex [left-nav] [image] [right-nav] + // Arrows are outside the image, matching Qt GUI + m('.photo-view-body', [ + m('.photo-view-nav-col', [ + hasPrev + ? m('button.photo-view-nav-btn', { + type: 'button', + title: 'Previous', + onclick: (e) => { e.stopPropagation(); navigate(photoList, currentIndex - 1); }, + }, m('i.fas.fa-chevron-left')) + : null, + ]), + m('.photo-view-img-wrap', [ + imgSrc + ? m('img.photo-view-img', { src: imgSrc, alt: title }) + : m('.photo-view-no-img', 'No image available'), + ]), + m('.photo-view-nav-col', [ + hasNext + ? m('button.photo-view-nav-btn', { + type: 'button', + title: 'Next', + onclick: (e) => { e.stopPropagation(); navigate(photoList, currentIndex + 1); }, + }, m('i.fas.fa-chevron-right')) + : null, + ]), + ]), + + // Footer: author + date only + m('.photo-view-footer', [ + m('.photo-view-meta', [ + m('span', 'Posted by '), + m('b', author), + dateStr ? m('span', ` \u2022 ${dateStr}`) : null, + ]), + ]), + ]); + }, + }; +} + +/** + * Open PhotoView — renders into a dedicated body-appended overlay. + * No #modal-container dependency, guaranteed z-index 999999. + */ +function openPhotoModal(photoList, photoIndex) { + const overlay = getPhotoOverlay(); + Object.assign(overlay.style, { + position: 'fixed', + inset: '0', + zIndex: '999999', + backgroundColor: 'rgba(0,0,0,0.85)', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }); + m.render(overlay, m(PhotoViewModal, { + photoList, + photoIndex, + })); +} + +/** + * BoardCard Component Factory + */ +function BoardCard() { + return { + view: (vnode) => { + const { item, viewMode, onOpenComments, onOpenPhoto, forumId, voterId } = vnode.attrs; + if (!item) return null; + + // Extract item properties with fallback defaults + const title = item.title || item.mMsgName || (item.post && item.post.mMeta && item.post.mMeta.mMsgName) || 'Untitled Post'; + const notes = util.plainText(item.notes || item.mNotes || item.mBody || (item.post && (item.post.mNotes || item.post.mBody)) || ''); + const hasNotes = hasNotesText(notes); + + // Author & Date details + const meta = (item.post && item.post.mMeta) ? item.post.mMeta : (item.mMeta || {}); + const author = meta.mAuthorId ? meta.mAuthorId.substring(0, 10) : (item.author || 'cluster'); + const publishTs = meta.mPublishTs ? meta.mPublishTs : item.created; + const dateString = publishTs + ? (typeof publishTs === 'object' && publishTs.xint64 ? new Date(publishTs.xint64 * 1000).toLocaleString() : new Date(publishTs * 1000).toLocaleString()) + : ''; + + // RsPostedPost keeps calculated vote totals on the post, not mMeta. + const post = item.post || item; + const upVotes = numberValue(post.mUpVotes !== undefined ? post.mUpVotes : meta.mUpVotes); + const downVotes = numberValue(post.mDownVotes !== undefined ? post.mDownVotes : meta.mDownVotes); + const score = upVotes - downVotes; + + // Thumbnail resolution via extractImageSrc + const thumbnailSrc = extractImageSrc(item); + + // Comment count + const commentCount = item.commentCount !== undefined + ? item.commentCount + : item.mCommentCount !== undefined + ? item.mCommentCount + : item.mComments !== undefined + ? item.mComments + : (meta.mComments !== undefined + ? meta.mComments + : (meta.mChildCount !== undefined ? meta.mChildCount : 0)); + + const msgId = item.msgId || item.mMsgId || (item.key ? item.key : null); + + return m( + '.board-card', + { + class: `board-card board-card--${viewMode}`, + tabindex: 0, + role: 'article', + 'aria-label': title, + }, + [ + // Image / Thumbnail Section (Clicking opens PhotoView modal!) + m( + '.board-card__image-container', + { + title: thumbnailSrc ? 'Click to view photo' : 'View photo', + style: 'cursor: pointer', + onclick: (e) => { + e.stopPropagation(); + if (onOpenPhoto) { + onOpenPhoto(item); + } + }, + }, + [ + thumbnailSrc + ? m('img.board-card__image', { + src: thumbnailSrc, + alt: title, + loading: 'lazy', + onerror: (e) => { + e.target.style.display = 'none'; + if (e.target.nextSibling) { + e.target.nextSibling.style.display = 'flex'; + } + }, + }) + : null, + m( + '.board-card__placeholder-wrapper', + { style: { display: thumbnailSrc ? 'none' : 'flex' } }, + m(FallbackImage) + ), + ] + ), + + // Card Content Body + m('.board-card__content', [ + // Title (blue link matching Qt GUI) + m( + 'h4.board-card__title', + { + title, + tabindex: 0, + onclick: (e) => { + e.stopPropagation(); + if (onOpenComments) { + onOpenComments(item, msgId, forumId); + } + }, + }, + title + ), + + // Metadata Line (Posted by ) + m('.board-card__meta', [ + m('span', 'Posted by '), + m('b', author), + dateString ? m('span', ` ${dateString}`) : null, + ]), + + // Card Actions Line. Notes stay out of the card preview and open in a dedicated dialog. + m('.board-card__footer', [ + hasNotes ? m( + 'button.board-card__notes-btn[type=button]', + { + title: 'View notes', + onclick: (e) => { + e.stopPropagation(); + util.popupmessage(m('.board-notes-dialog', [ + m('h3', title), + m('p.board-notes-dialog__label', 'Notes'), + m('p.board-notes-dialog__content', notes), + ])); + }, + }, + [m('i.fas.fa-sticky-note'), m('span', 'View notes')] + ) : null, + m( + 'button.board-card__comments-btn', + { + type: 'button', + 'aria-label': `View ${commentCount} comments for ${title}`, + title: `Comments (${commentCount})`, + onclick: (e) => { + e.stopPropagation(); + if (onOpenComments) { + onOpenComments(item, msgId, forumId); + } else if (msgId && forumId) { + m.route.set('/boards/:tab/:mGroupId/:mMsgId', { + tab: m.route.param().tab || 'Subscribed', + mGroupId: forumId, + mMsgId: msgId, + }); + } + }, + }, + [ + m('i.fas.fa-comment-alt.board-card__comments-icon'), + m('span.board-card__comments-label', commentCount > 0 ? `${commentCount} comment${commentCount === 1 ? '' : 's'}` : 'Comment'), + ] + ), + m('.board-card__vote-pill', [ + m( + 'button.board-card__vote-btn.board-card__vote-btn--up[type=button][title=Upvote]', + { + disabled: !voterId, + title: voterId ? 'Upvote' : 'Select a voter identity first', + onclick: async (e) => { + e.stopPropagation(); + if (forumId && msgId) { + const voted = await util.voteForPost(forumId, msgId, util.GXS_VOTE_UP, voterId); + if (voted) { + post.mUpVotes = numberValue(post.mUpVotes) + 1; + m.redraw(); + } + } + }, + }, + [m('i.fas.fa-arrow-up')] + ), + m('span.board-card__vote-score', score), + m( + 'button.board-card__vote-btn.board-card__vote-btn--down[type=button][title=Downvote]', + { + disabled: !voterId, + title: voterId ? 'Downvote' : 'Select a voter identity first', + onclick: async (e) => { + e.stopPropagation(); + if (forumId && msgId) { + const voted = await util.voteForPost(forumId, msgId, util.GXS_VOTE_DOWN, voterId); + if (voted) { + post.mDownVotes = numberValue(post.mDownVotes) + 1; + m.redraw(); + } + } + }, + }, + [m('i.fas.fa-arrow-down')] + ), + ]), + ]), + ]), + ] + ); + }, + }; +} + +/** + * Toolbar Component Factory + */ +function Toolbar() { + return { + view: (vnode) => { + const { + viewMode, + onViewModeChange, + itemCount, + searchString, + onSearchInput, + currentPage, + totalPages, + onPageChange, + startItem, + endItem, + voterIdentities = [], + voterId, + voterIdentitiesLoading, + onVoterIdChange, + } = vnode.attrs; + + return m('.board-toolbar', { role: 'toolbar', 'aria-label': 'Board View Controls' }, [ + // Left section: Search Filter + m('.board-toolbar__left', [ + onSearchInput + ? m('.board-toolbar__search', [ + m('i.fas.fa-search.board-toolbar__search-icon'), + m('input.board-toolbar__search-input[type=text][placeholder=Search...]', { + value: searchString || '', + oninput: (e) => onSearchInput(e.target.value), + }), + ]) + : null, + ]), + + // Right section: View Switcher AND Pagination inline + m('.board-toolbar__right', [ + // View Mode Switcher + m('.board-toolbar__view-toggle', { role: 'radiogroup', 'aria-label': 'Display Mode' }, [ + m( + 'button.board-toolbar__toggle-btn', + { + type: 'button', + class: viewMode === 'compact' ? 'board-toolbar__toggle-btn--active' : '', + role: 'radio', + 'aria-checked': viewMode === 'compact', + title: 'Switch to Compact View', + onclick: () => onViewModeChange('compact'), + }, + [ + m('i.fas.fa-bars'), + m('span', 'Compact View'), + ] + ), + m( + 'button.board-toolbar__toggle-btn', + { + type: 'button', + class: viewMode === 'card' ? 'board-toolbar__toggle-btn--active' : '', + role: 'radio', + 'aria-checked': viewMode === 'card', + title: 'Switch to Card View', + onclick: () => onViewModeChange('card'), + }, + [ + m('i.fas.fa-th-large'), + m('span', 'Card View'), + ] + ), + ]), + + // Pagination Controls (< 1 - 25 >) + itemCount > 0 + ? m('.board-pagination', { 'aria-label': 'Pagination Controls' }, [ + m( + 'button.board-pagination__btn.board-pagination__btn--prev', + { + type: 'button', + title: 'Previous Page', + disabled: currentPage <= 1, + onclick: () => onPageChange(currentPage - 1), + }, + m('i.fas.fa-chevron-left') + ), + m( + 'span.board-pagination__label', + `${startItem} - ${endItem}` + ), + m( + 'button.board-pagination__btn.board-pagination__btn--next', + { + type: 'button', + title: 'Next Page', + disabled: currentPage >= totalPages, + onclick: () => onPageChange(currentPage + 1), + }, + m('i.fas.fa-chevron-right') + ), + ]) + : null, + m('.board-toolbar__voter', [ + m('select#board-post-voter', { + value: voterId || '', + disabled: voterIdentitiesLoading || voterIdentities.length === 0, + onchange: (e) => onVoterIdChange && onVoterIdChange(e.target.value), + title: 'Identity used to vote on posts', + 'aria-label': 'Identity used to vote on posts', + }, voterIdentities.length > 0 + ? voterIdentities.map((identity) => m('option', { value: identity.id }, identity.label)) + : m('option', { value: '' }, voterIdentitiesLoading ? 'Loading identities...' : 'No identity available')), + ]), + ]), + ]); + }, + }; +} + +/** + * CommentsViewer Modal Trigger — navigates to the boards post detail route + */ +function openCommentsModal(item, msgId, forumId) { + const tab = m.route.param().tab || 'Subscribed'; + m.route.set('/boards/:tab/:mGroupId/:mMsgId', { + tab, + mGroupId: forumId, + mMsgId: msgId, + }); +} + +/** + * Main BoardView Component Factory + * Manages view mode (default: compact), search filtering, 25-item page pagination + */ +function BoardView() { + let viewMode = 'compact'; + let filterText = ''; + let currentPage = 1; + + return { + view: (vnode) => { + const { + items = [], forumId, onOpenComments, voterIdentities = [], voterId, + voterIdentitiesLoading, onVoterIdChange, + } = vnode.attrs; + + // Filter items + const filteredItems = items.filter((item) => { + if (!filterText.trim()) return true; + const query = filterText.toLowerCase(); + const title = (item.title || item.mMsgName || (item.post && item.post.mMeta && item.post.mMeta.mMsgName) || '').toLowerCase(); + const notes = (item.notes || item.mNotes || item.mBody || (item.post && (item.post.mNotes || item.post.mBody)) || '').toLowerCase(); + return title.includes(query) || notes.includes(query); + }); + + // Automatically sort posts by publish timestamp descending (newest posts on top) + filteredItems.sort((a, b) => { + const getTs = (item) => { + const p = item.post || item; + const meta = p.mMeta || item.mMeta || {}; + const ts = meta.mPublishTs || p.mPublishTs || item.created || 0; + if (ts && typeof ts === 'object' && ts.xint64 !== undefined) return Number(ts.xint64); + if (typeof ts === 'number') return ts; + if (typeof ts === 'string') { const n = Number(ts); return isNaN(n) ? 0 : n; } + return 0; + }; + return getTs(b) - getTs(a); + }); + + // Pagination math (25 posts max per page) + const totalFiltered = filteredItems.length; + const totalPages = Math.max(1, Math.ceil(totalFiltered / PAGE_SIZE)); + if (currentPage > totalPages) { + currentPage = totalPages; + } + if (currentPage < 1) { + currentPage = 1; + } + + const startIndex = (currentPage - 1) * PAGE_SIZE; + const endIndex = Math.min(startIndex + PAGE_SIZE, totalFiltered); + const pagedItems = filteredItems.slice(startIndex, endIndex); + + const startItemNum = totalFiltered > 0 ? startIndex + 1 : 0; + const endItemNum = endIndex; + + // Items with photos for PhotoView modal + const photoItems = pagedItems.filter((item) => { + return extractImageSrc(item) !== ''; + }); + + const modalPhotos = photoItems.length > 0 ? photoItems : pagedItems; + + return m('.board-view-container', [ + // Top Toolbar with Pagination + m(Toolbar, { + key: 'toolbar-node', + viewMode, + onViewModeChange: (newMode) => { + viewMode = newMode; + m.redraw(); + }, + itemCount: totalFiltered, + searchString: filterText, + onSearchInput: (text) => { + filterText = text; + currentPage = 1; + }, + currentPage, + totalPages, + onPageChange: (newPage) => { + currentPage = newPage; + m.redraw(); + }, + startItem: startItemNum, + endItem: endItemNum, + voterIdentities, + voterId, + voterIdentitiesLoading, + onVoterIdChange, + }), + + // Board Grid (rendering paged slice of 25 items max) + pagedItems.length > 0 + ? m( + '.board-grid', + { + key: 'grid-node', + class: `board-grid board-grid--${viewMode}`, + role: 'region', + 'aria-label': 'Board items', + }, + pagedItems.map((item, index) => { + const itemKey = item.key || item.msgId || item.mMsgId || index; + return m(BoardCard, { + key: `card-${itemKey}`, + item, + viewMode, + forumId, + voterId, + onOpenComments: onOpenComments || ((itemObj, mId, fId) => openCommentsModal(itemObj, mId, fId)), + onOpenPhoto: (clickedItem) => { + const photoIdx = modalPhotos.findIndex((pi) => { + const k1 = pi.key || pi.msgId || pi.mMsgId || (pi.post && pi.post.mMeta && pi.post.mMeta.mMsgId); + const k2 = clickedItem.key || clickedItem.msgId || clickedItem.mMsgId || (clickedItem.post && clickedItem.post.mMeta && clickedItem.post.mMeta.mMsgId); + return (k1 && k2 && k1 === k2) || pi === clickedItem; + }); + openPhotoModal(modalPhotos, photoIdx >= 0 ? photoIdx : 0); + }, + }); + }) + ) + : m('.board-grid__empty', { key: 'empty-node' }, [ + m('i.fas.fa-inbox.board-grid__empty-icon'), + m('p.board-grid__empty-title', 'No items found'), + m('p.board-grid__empty-desc', filterText ? 'Try adjusting your search criteria.' : 'This board currently has no posts.'), + ]), + ]); + }, + }; +} + +module.exports = { + BoardView, + BoardCard, + Toolbar, + PhotoViewModal, + openPhotoModal, + openCommentsModal, + extractImageSrc, + hasNotesText, +}; diff --git a/webui-src/app/boards/board_view.js b/webui-src/app/boards/board_view.js index fe604982..36424838 100644 --- a/webui-src/app/boards/board_view.js +++ b/webui-src/app/boards/board_view.js @@ -1,30 +1,21 @@ const m = require('mithril'); -const rs = require('rswebui'); const util = require('boards/boards_util'); +const boardKanban = require('boards/board_kanban'); +const rs = require('rswebui'); +const peopleUtil = require('people/people_util'); +const chatEmoji = require('chat/chat_emoji'); const Data = util.Data; -const messageGroups = ['Public', 'Restricted Circle', 'Restricted Node Group']; -const messageGroupsCode = [util.PUBLIC, util.EXTERNAL, util.NODES_GROUP]; // rsgxscirles.h:50 - function createboard() { let title; let body; let identity; - let thumbnail; - let selectedGroup = messageGroups[0]; - let selectedGroupCode = messageGroupsCode[0]; - let selectedCircle; - let circles; + let circle; return { - oninit: async (vnode) => { + oninit: (vnode) => { if (vnode.attrs.authorId) { identity = vnode.attrs.authorId[0]; - } - - const res = await rs.rsJsonApiRequest('/rsgxscircles/getCirclesSummaries'); - if (res.body.retval) { - circles = res.body.circles; - selectedCircle = circles[0].mGroupName; + circle = util.PUBLIC; } }, view: (vnode) => @@ -32,120 +23,70 @@ function createboard() { m('h3', 'Create Board'), m('hr'), m('input[type=text][placeholder=Title]', { - style: { float: 'left' }, oninput: (e) => (title = e.target.value), }), - m('div', { style: { float: 'right', marginTop: '10px', marginBottom: '10px' } }, [ - m('label[for=thumbnail]', 'Thumbnail: '), - m('input[type=file][name=files][id=thumbnail][accept=image/*]', { - onchange: async (e) => { - const reader = new FileReader(); - reader.onloadend = function () { - thumbnail = reader.result.substring(reader.result.indexOf(',') + 1); - }; - reader.readAsDataURL(e.target.files[0]); - }, - }), - ]), - - m('div', { style: { float: 'right', marginTop: '10px', marginBottom: '10px' } }, [ - m('label[for=idtags]', 'Select identity: '), - m( - 'select[id=idtags]', - { - value: identity, - onchange: (e) => { - identity = vnode.attrs.authorId[e.target.selectedIndex]; - }, - }, - [ - vnode.attrs.authorId && - vnode.attrs.authorId.map((o) => - m( - 'option', - { value: o }, - rs.userList.userMap[o] - ? rs.userList.userMap[o].toLocaleString() - : 'No Signature' - ) - ), - ] - ), - ]), - m('div', { style: { float: 'left', marginTop: '10px', marginBottom: '10px' } }, [ - m('label[for=mtags]', 'Message Distribution: '), - m( - 'select[id=mtags]', - { - value: selectedGroup, - onchange: (e) => { - selectedGroup = messageGroups[e.target.selectedIndex]; - selectedGroupCode = messageGroupsCode[e.target.selectedIndex]; - util.popupmessage(m(createboard, { authorId: vnode.attrs.authorId })); - }, - }, - [messageGroups.map((group) => m('option', { value: group }, group))] - ), - ]), - circles && - m( - 'div', - { - style: { - float: 'left', - marginTop: '10px', - marginBottom: '10px', - display: selectedGroupCode === util.EXTERNAL ? 'block' : 'none', - }, + m('label[for=idtags]', 'Select identity'), + m( + 'select[id=idtags]', + { + value: identity, + onchange: (e) => { + identity = vnode.attrs.authorId[e.target.selectedIndex]; }, - [ - m('label[for=circlestag]', 'Circles: '), - m( - 'select[id=circlestag]', - { - value: selectedCircle, - onchange: (e) => { - selectedCircle = circles[e.target.selectedIndex]; - console.log(selectedCircle); - // selectedGroupCode = messageGroupsCode[e.target.selectedIndex]; - }, - }, - [ - circles.map((circle) => - m('option', { value: circle.mGroupName }, circle.mGroupName) - ), - ] + }, + [ + vnode.attrs.authorId && + vnode.attrs.authorId.map((o) => + m( + 'option', + { value: o }, + rs.userList.username(o) + ? rs.userList.username(o) + ' (' + o.slice(0, 8) + '...)' + : 'No Signature' + ) ), - ] - ), + ] + ), + m('textarea[rows=5][placeholder=Description]', { - style: { width: '100%', display: 'block' }, + style: { width: '90%', display: 'block' }, oninput: (e) => (body = e.target.value), value: body, }), + m('label[for=circletags]', 'Select Distribution'), + m( + 'select[id=circletags]', + { + value: circle, + onchange: (e) => { + circle = e.target.value; + }, + }, + [ + m('option', { value: util.PUBLIC }, 'Public'), + m('option', { value: util.EXTERNAL }, 'Restricted to External Circle'), + ] + ), m( 'button', { onclick: async () => { - const res = await rs.rsJsonApiRequest('/rsposted/createBoardV2', { + const res = await rs.rsJsonApiRequest('/rsposted/createBoard', { name: title, description: body, - thumbnail: { mData: { base64: thumbnail } }, - ...(Number(identity) !== 0 && { authorId: identity }), - circleType: selectedGroupCode, - ...(selectedGroupCode === util.EXTERNAL && - selectedCircle && { circleId: selectedCircle.mGroupId }), + authorId: identity, + circleType: Number(circle), }); - if (res.body.retval) { - util.updatedisplayboards(res.body.boardId); - m.redraw(); - } - res.body.retval === false - ? util.popupmessage([m('h3', 'Error'), m('hr'), m('p', res.body.errorMessage)]) - : util.popupmessage([ + res.body.retval + ? util.popupmessage([ m('h3', 'Success'), m('hr'), m('p', 'Board created successfully'), + ]) + : util.popupmessage([ + m('h3', 'Error'), + m('hr'), + m('p', 'Error in creating Board'), ]); }, }, @@ -155,144 +96,480 @@ function createboard() { }; } -const BoardView = () => { - let bname = ''; - let bimage = ''; - let bauthor = ''; - let bsubscribed = {}; - let bposts = 0; - let plist = {}; - let createDate = {}; - let lastActivity = {}; +function BoardView() { + let lastLoadedBoardId = null; + let voterIdentities = []; + let voterId = null; + let voterIdentitiesLoading = true; + return { oninit: (v) => { - if (Data.DisplayBoards[v.attrs.id]) { - bname = Data.DisplayBoards[v.attrs.id].name; - bimage = Data.DisplayBoards[v.attrs.id].image; - if (rs.userList.userMap[Data.DisplayBoards[v.attrs.id].author]) { - bauthor = rs.userList.userMap[Data.DisplayBoards[v.attrs.id].author]; - } else if (Number(Data.DisplayBoards[v.attrs.id].author) === 0) { + lastLoadedBoardId = v.attrs.id; + util.updateDisplayBoards(v.attrs.id); + peopleUtil.ownIds((ids) => { + voterIdentities = (ids || []) + .filter((id) => Number(id) !== 0) + .map((id) => ({ + id, + label: rs.userList.username(id) || rs.userList.userMap[id] || `${String(id).slice(0, 10)}...`, + })); + voterId = voterIdentities[0] ? voterIdentities[0].id : null; + voterIdentitiesLoading = false; + m.redraw(); + }); + }, + onupdate: (v) => { + if (v.attrs.id && v.attrs.id !== lastLoadedBoardId) { + lastLoadedBoardId = v.attrs.id; + util.updateDisplayBoards(v.attrs.id); + } + }, + view: (v) => { + const boardInfo = Data.DisplayBoards[v.attrs.id] || {}; + const bname = boardInfo.name || ''; + const bimage = boardInfo.image || { mData: { base64: '' } }; + let bauthor = 'Unknown'; + if (boardInfo.author) { + if (rs.userList.userMap[boardInfo.author]) { + bauthor = rs.userList.userMap[boardInfo.author]; + } else if (Number(boardInfo.author) === 0) { bauthor = 'No Contact Author'; - } else { - bauthor = 'Unknown'; } - bsubscribed = Data.DisplayBoards[v.attrs.id].isSubscribed; - bposts = Data.DisplayBoards[v.attrs.id].posts; - createDate = Data.DisplayBoards[v.attrs.id].created; - lastActivity = Data.DisplayBoards[v.attrs.id].activity; } - if (Data.Posts[v.attrs.id]) { - plist = Data.Posts[v.attrs.id]; - } - }, - view: (v) => [ - m( - 'a[title=Back]', - { - onclick: () => - m.route.set('/boards/:tab', { - tab: m.route.param().tab, - }), - }, - m('i.fas.fa-arrow-left') - ), - m('.widget__heading', [ - m('h3', bname), + const bsubscribed = boardInfo.isSubscribed; + const bposts = boardInfo.posts || 0; + const createDate = boardInfo.created; + const lastActivity = boardInfo.activity; + const plist = Data.Posts[v.attrs.id] || {}; + + const items = Object.keys(plist) + .filter((key) => plist[key] && (plist[key].isSearched === undefined || plist[key].isSearched)) + .map((key) => { + const itemObj = plist[key] || {}; + const p = itemObj.post || itemObj; + const meta = p.mMeta || {}; + + let thumb = ''; + if (p.mImage && p.mImage.mData && p.mImage.mData.base64) { + thumb = p.mImage.mData.base64; + } else if (p.mImage && typeof p.mImage.base64 === 'string') { + thumb = p.mImage.base64; + } else if (typeof p.mImage === 'string') { + thumb = p.mImage; + } else if (p.mThumbnail && p.mThumbnail.mData && p.mThumbnail.mData.base64) { + thumb = p.mThumbnail.mData.base64; + } else if (typeof p.thumbnail === 'string') { + thumb = p.thumbnail; + } + + const notesText = util.plainText(p.mNotes || p.mBody || meta.mNotes || p.notes || p.body || ''); + const titleText = meta.mMsgName || p.mMsgName || p.title || 'Untitled Post'; + // RsPosted exposes the calculated count as mComments on the post. + const commentCount = p.mComments !== undefined + ? p.mComments + : (meta.mChildCount !== undefined + ? meta.mChildCount + : (p.mCommentCount !== undefined ? p.mCommentCount : (p.commentCount !== undefined ? p.commentCount : 0))); + + return { + key, + msgId: key, + title: titleText, + thumbnail: thumb, + notes: notesText, + commentCount, + post: p, + }; + }); + + // Automatically sort posts by publish timestamp descending (newest on top) + items.sort((a, b) => { + const getTs = (item) => { + const p = item.post || item; + const meta = p.mMeta || item.mMeta || {}; + const ts = meta.mPublishTs || p.mPublishTs || item.created || 0; + if (ts && typeof ts === 'object' && ts.xint64 !== undefined) return Number(ts.xint64); + if (typeof ts === 'number') return ts; + if (typeof ts === 'string') { const n = Number(ts); return isNaN(n) ? 0 : n; } + return 0; + }; + return getTs(b) - getTs(a); + }); + + return [ m( - 'button', + 'a[title=Back]', { - onclick: async () => { - const res = await rs.rsJsonApiRequest('/rsposted/subscribeToBoard', { - boardId: v.attrs.id, - subscribe: !bsubscribed, - }); - if (res.body.retval) { - bsubscribed = !bsubscribed; - Data.DisplayBoards[v.attrs.id].isSubscribed = bsubscribed; - } - }, + onclick: () => + m.route.set('/boards/:tab', { + tab: m.route.param().tab || 'Subscribed', + }), }, - bsubscribed ? 'Subscribed' : 'Subscribe' + m('i.fas.fa-arrow-left') ), - ]), - m('.widget__body', [ - m('.media-item', [ - m('.media-item__details', [ - m('img', { - src: - bimage.mData.base64 === '' - ? 'data/streaming.png' - : `data:image/png;base64,${bimage.mData.base64}`, - }), - m('.media-item__details-info', [ - m('div', [m('b', 'Posts: '), m('span', bposts)]), - m('div', [ - m('b', 'Date created: '), - m( - 'span', - typeof createDate === 'object' - ? new Date(createDate.xint64 * 1000).toLocaleString() - : 'Unknown' - ), - ]), - m('div', [m('b', 'Admin: '), m('span', bauthor)]), - m('div', [ - m('b', 'Last activity: '), - m( - 'span', - typeof lastActivity === 'object' - ? new Date(lastActivity.xint64 * 1000).toLocaleString() - : 'Unknown' - ), + m('.widget__heading', [ + m('h3', bname), + m( + 'button', + { + onclick: async () => { + const res = await rs.rsJsonApiRequest('/rsposted/subscribeToBoard', { + boardId: v.attrs.id, + subscribe: !bsubscribed, + }); + if (res.body.retval) { + boardInfo.isSubscribed = !bsubscribed; + m.redraw(); + } + }, + }, + bsubscribed ? 'Subscribed' : 'Subscribe' + ), + ]), + m('.widget__body', [ + m('.media-item', [ + m('.media-item__details', [ + bimage && bimage.mData && bimage.mData.base64 + ? m('img', { src: `data:image/png;base64,${bimage.mData.base64}` }) + : null, + m('.media-item__details-info', [ + m('div', [m('b', 'Posts: '), m('span', bposts)]), + m('div', [ + m('b', 'Date created: '), + m( + 'span', + typeof createDate === 'object' && createDate !== null + ? new Date(createDate.xint64 * 1000).toLocaleString() + : 'Unknown' + ), + ]), + m('div', [m('b', 'Admin: '), m('span', bauthor)]), + m('div', [ + m('b', 'Last activity: '), + m( + 'span', + typeof createDate === 'object' && lastActivity !== null && typeof lastActivity === 'object' + ? new Date(lastActivity.xint64 * 1000).toLocaleString() + : 'Unknown' + ), + ]), ]), ]), + m('.media-item__desc', [ + m('b', 'Description: '), + m('span', boardInfo.description || 'No Description'), + ]), ]), - m('.media-item__desc', [ - m('b', 'Description: '), - m('span', Data.DisplayBoards[v.attrs.id].description || 'No Description'), - ]), + m( + '.posts', + { + style: 'display:' + (bsubscribed ? 'block' : 'none'), + }, + m('.posts__heading', m('h3', 'Posts')), + m(boardKanban.BoardView, { + forumId: v.attrs.id, + items, + voterIdentities, + voterId, + voterIdentitiesLoading, + onVoterIdChange: (id) => { + voterId = id || null; + }, + }) + ), ]), + ]; + }, + }; +} + +/** + * PostView: Board post detail page (shown at /boards/:tab/:mGroupId/:mMsgId) + * Reads from Data.Posts[forumId][msgId]. The Posted API returns comments together + * with board content, so comments for this post are filtered by their thread id. + */ +function PostView() { + let comments = []; + let loadingComments = true; + let identities = []; + let authorId = null; + let voteIdentity = null; + let postVoteSubmitting = false; + let replyTo = null; + let composerText = ''; + let submitting = false; + let submitError = ''; + let notesExpanded = false; + let showEmojiPicker = false; + const expandedReplies = {}; + + const metaOf = (comment) => (comment && comment.mMeta) || {}; + const idOf = (comment) => metaOf(comment).mMsgId || comment.msgId || comment.id; + const parentOf = (comment) => metaOf(comment).mParentId || comment.parentId || ''; + const textOf = (comment) => comment.mComment || comment.comment || comment.mBody || ''; + const nameOf = (id) => !id || Number(id) === 0 ? 'Anonymous' : (rs.userList.username(id) || rs.userList.userMap[id] || `${String(id).slice(0, 10)}…`); + const initials = (name) => String(name || '?').split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]).join('').toUpperCase(); + const timeOf = (value) => { + const seconds = value && typeof value === 'object' ? value.xint64 : value; + const date = Number(seconds) ? new Date(Number(seconds) * 1000) : null; + return date && !Number.isNaN(date.getTime()) ? date.toLocaleString() : ''; + }; + + function treeOfComments() { + const nodes = {}; + const roots = []; + comments.forEach((comment) => { + const id = idOf(comment); + if (id) nodes[id] = { comment, children: [] }; + }); + Object.keys(nodes).forEach((id) => { + const node = nodes[id]; + const parent = parentOf(node.comment); + if (parent && nodes[parent] && parent !== id) nodes[parent].children.push(node); + else roots.push(node); + }); + const chronological = (a, b) => Number(metaOf(a.comment).mPublishTs && (metaOf(a.comment).mPublishTs.xint64 || metaOf(a.comment).mPublishTs)) - Number(metaOf(b.comment).mPublishTs && (metaOf(b.comment).mPublishTs.xint64 || metaOf(b.comment).mPublishTs)); + roots.sort(chronological); + Object.keys(nodes).forEach((id) => nodes[id].children.sort(chronological)); + return roots; + } + + async function loadComments(forumId, msgId) { + loadingComments = true; + comments = []; + try { + const res = await rs.rsJsonApiRequest('/rsPosted/getBoardAllContent', { boardId: forumId }); + if (res && res.body && res.body.retval) { + comments = (res.body.comments || res.body.commentList || []).filter((comment) => { + const meta = metaOf(comment); + return meta.mThreadId === msgId || (!meta.mThreadId && meta.mParentId === msgId); + }); + } + } catch (e) { + console.warn('PostView: failed to load comments', e); + } + loadingComments = false; + m.redraw(); + } + + async function submitComment(forumId, msgId) { + const comment = composerText.trim(); + if (!comment || !authorId || submitting) return; + submitting = true; + submitError = ''; + try { + const res = await rs.rsJsonApiRequest('/rsPosted/createCommentV2', { + boardId: forumId, + postId: msgId, + comment, + authorId, + parentId: replyTo ? idOf(replyTo) : msgId, + }); + if (!res || !res.body || res.body.retval === false) { + submitError = (res && res.body && res.body.errorMessage) || 'Your comment could not be posted.'; + return; + } + composerText = ''; + replyTo = null; + await loadComments(forumId, msgId); + await util.updateDisplayBoards(forumId); + } catch (e) { + console.warn('PostView: failed to submit comment', e); + submitError = 'Your comment could not be posted. Please try again.'; + } finally { + submitting = false; + m.redraw(); + } + } + + return { + oninit: (v) => { + // Ensure board data is loaded + if (!Data.Posts[v.attrs.forumId] || !Data.Posts[v.attrs.forumId][v.attrs.msgId]) { + util.updateDisplayBoards(v.attrs.forumId); + } + loadComments(v.attrs.forumId, v.attrs.msgId); + + // A board comment must be signed by one of the user's identities. + peopleUtil.ownIds((ids) => { + identities = (ids || []).filter((id) => Number(id) !== 0); + authorId = identities[0] || null; + voteIdentity = identities[0] || null; + m.redraw(); + }); + }, + view: (v) => { + const { forumId, msgId } = v.attrs; + const plist = Data.Posts[forumId] || {}; + const itemObj = plist[msgId] || {}; + const p = itemObj.post || itemObj; + const meta = (p && p.mMeta) ? p.mMeta : {}; + + const title = meta.mMsgName || p.mMsgName || p.title || 'Post'; + const notes = util.plainText(p.mNotes || p.mBody || p.notes || p.body || ''); + const hasLongNotes = notes.length > 280; + const author = meta.mAuthorId ? meta.mAuthorId.substring(0, 10) : 'Unknown'; + const publishTs = meta.mPublishTs || p.mPublishTs || null; + const dateStr = publishTs + ? (typeof publishTs === 'object' && publishTs.xint64 + ? new Date(publishTs.xint64 * 1000).toLocaleString() + : new Date(publishTs * 1000).toLocaleString()) + : ''; + const numberValue = (value) => { + if (value && typeof value === 'object') return Number(value.xint64 || value.xint32 || 0); + return Number(value || 0); + }; + const postUpVotes = numberValue(p.mUpVotes !== undefined ? p.mUpVotes : meta.mUpVotes); + const postDownVotes = numberValue(p.mDownVotes !== undefined ? p.mDownVotes : meta.mDownVotes); + + let imgSrc = ''; + if (p.mImage && p.mImage.mData && p.mImage.mData.base64 && p.mImage.mData.base64.trim()) { + imgSrc = `data:image/png;base64,${p.mImage.mData.base64}`; + } else if (p.mThumbnail && p.mThumbnail.mData && p.mThumbnail.mData.base64 && p.mThumbnail.mData.base64.trim()) { + imgSrc = `data:image/png;base64,${p.mThumbnail.mData.base64}`; + } + + return [ m( - '.posts', + 'a[title=Back]', { - style: 'display:' + (bsubscribed ? 'flex' : 'none'), + onclick: () => + m.route.set('/boards/:tab/:mGroupId', { + tab: m.route.param().tab || 'Subscribed', + mGroupId: forumId, + }), }, - m('.posts__heading', m('h3', 'Posts')), - m( - '.posts-container', - Object.keys(plist).map((key, index) => [ - m( - '.posts-container-card', - { - style: 'display: ' + (plist[key].isSearched ? 'flex' : 'none'), - onclick: () => { - m.route.set('/boards/:tab/:mGroupId/:mMsgId', { - tab: m.route.param().tab, - mGroupId: v.attrs.id, - mMsgId: key, - }); - }, - }, - [ - m('img', { - src: - plist[key].post.mThumbnail.mData.base64 === '' - ? 'data/streaming.png' - : 'data:image/png;base64,' + plist[key].post.mThumbnail.mData.base64, - alt: 'No Thumbnail', - }), - m('p', plist[key].post.mMeta.mMsgName), - ] - ), - ]) - ) + m('i.fas.fa-arrow-left') ), - ]), - ], + m('.widget__heading', m('h3', title)), + m('.widget__body', [ + imgSrc + ? m('img', { + src: imgSrc, + alt: title, + style: { maxWidth: '100%', maxHeight: '400px', display: 'block', marginBottom: '1rem', borderRadius: '8px' }, + }) + : null, + m('.board-post-meta', [ + m('span', 'Posted by '), + m('b', author), + dateStr ? m('span', ` • ${dateStr}`) : null, + ]), + m('.board-post-voting', [ + m('.board-post-voting__identity', [ + m('label[for=board-post-voter]', 'Vote as'), + m('select#board-post-voter', { + value: voteIdentity || '', + disabled: identities.length === 0 || postVoteSubmitting, + onchange: (e) => { voteIdentity = e.target.value; }, + }, identities.length + ? identities.map((id) => m('option', { value: id }, nameOf(id))) + : m('option', { value: '' }, 'Loading identities…')), + ]), + m('.board-post-voting__buttons', [ + m('button[type=button][title=Upvote post]', { + disabled: !voteIdentity || postVoteSubmitting, + onclick: async () => { + postVoteSubmitting = true; + m.redraw(); + await util.voteForPost(forumId, msgId, util.GXS_VOTE_UP, voteIdentity); + postVoteSubmitting = false; + m.redraw(); + }, + }, [m('i.fas.fa-arrow-up'), ` ${postUpVotes}`]), + m('span.board-post-voting__score', postUpVotes - postDownVotes), + m('button[type=button][title=Downvote post]', { + disabled: !voteIdentity || postVoteSubmitting, + onclick: async () => { + postVoteSubmitting = true; + m.redraw(); + await util.voteForPost(forumId, msgId, util.GXS_VOTE_DOWN, voteIdentity); + postVoteSubmitting = false; + m.redraw(); + }, + }, [m('i.fas.fa-arrow-down'), ` ${postDownVotes}`]), + ]), + ]), + notes ? m('.post-description.board-post-description', [ + m('.post-description__text', { class: notesExpanded ? '' : 'post-description__text--collapsed', style: { whiteSpace: 'pre-wrap', maxHeight: notesExpanded ? 'none' : '4.5em', overflow: 'hidden', lineHeight: '1.5' } }, notes), + hasLongNotes ? m('button.post-description__toggle[type=button]', { onclick: () => { notesExpanded = !notesExpanded; } }, notesExpanded ? 'Show less' : '…more') : null, + ]) : null, + m('hr'), + m('.board-comments', [ + m('.board-comments__heading', [ + m('h3', `${comments.length} Comment${comments.length === 1 ? '' : 's'}`), + m('span', [m('i.fas.fa-sort-amount-down'), ' Oldest first']), + ]), + m('.board-comment-composer', [ + m('.board-comment-avatar', initials(nameOf(authorId))), + m('.board-comment-composer__body', [ + replyTo ? m('.board-comment-composer__replying', ['Replying to ', m('b', nameOf(metaOf(replyTo).mAuthorId)), m('button[type=button][aria-label=Cancel reply]', { onclick: () => { replyTo = null; composerText = ''; } }, m('i.fas.fa-times'))]) : null, + identities.length ? m('select.board-comment-composer__identity', { value: authorId, onchange: (e) => { authorId = e.target.value; } }, identities.map((id) => m('option', { value: id }, nameOf(id)))) : null, + m('textarea.board-comment-composer__input[rows=1][placeholder=Add a comment…]', { value: composerText, disabled: !authorId || submitting, oninput: (e) => { composerText = e.target.value; }, onkeydown: (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') submitComment(forumId, msgId); } }), + !authorId ? m('p.board-comment-composer__hint', 'Create or select an identity to post a comment.') : null, + submitError ? m('p.board-comment-composer__error', submitError) : null, + m('.board-comment-composer__actions', [ + m('.board-comment-composer__emoji', { style: { position: 'relative', marginRight: 'auto' } }, [ + m('button[type=button][title=Insert emoji][aria-label=Insert emoji]', { style: { width: '32px', height: '32px', padding: '0', borderRadius: '50%', border: '0', boxShadow: 'none', background: showEmojiPicker ? '#e0f2fe' : 'transparent', color: '#475569', fontSize: '1.15rem' }, onclick: () => { showEmojiPicker = !showEmojiPicker; } }, m('i.fas.fa-smile')), + showEmojiPicker ? m('.board-comment-emoji-popover', { style: { position: 'absolute', zIndex: '20', top: '38px', left: '0', width: '250px', maxHeight: '180px', overflowY: 'auto', padding: '.5rem', display: 'grid', gridTemplateColumns: 'repeat(8, 1fr)', gap: '.2rem', background: '#fff', border: '1px solid #cbd5e1', borderRadius: '8px', boxShadow: '0 8px 20px rgba(0,0,0,.16)' } }, chatEmoji.EMOJI_DATA.Smileys.slice(0, 48).map((emoji) => m('button[type=button]', { style: { width: '28px', height: '28px', padding: '0', border: '0', boxShadow: 'none', background: 'transparent', fontSize: '1.1rem' }, onclick: () => { composerText += emoji; showEmojiPicker = false; } }, emoji))) : null, + ]), + composerText || replyTo ? m('button.board-comment-composer__cancel[type=button]', { onclick: () => { composerText = ''; replyTo = null; submitError = ''; } }, 'Cancel') : null, + m('button.board-comment-composer__submit[type=button]', { disabled: !composerText.trim() || !authorId || submitting, onclick: () => submitComment(forumId, msgId) }, submitting ? 'Posting…' : 'Comment') + ]) + ]) + ]), + loadingComments ? m('.board-comments__status', [m('i.fas.fa-spinner.fa-spin'), ' Loading comments…']) + : comments.length === 0 ? m('.board-comments__empty', [m('i.fas.fa-comment'), m('p', 'No comments yet. Start the conversation.')]) + : m('.board-comments__list', treeOfComments().map((node) => renderComment(node, 0, forumId, msgId))), + ]), + ]), + ]; + }, }; -}; + + function renderComment(node, depth, forumId, msgId) { + const comment = node.comment; + const key = idOf(comment); + const meta = metaOf(comment); + const name = nameOf(meta.mAuthorId); + const repliesCount = node.children.length; + const repliesExpanded = expandedReplies[key] === true; + return m('.board-comment', { key: idOf(comment), class: depth ? 'board-comment--reply' : '' }, [ + m('.board-comment-avatar', initials(name)), + m('.board-comment__content', [ + m('.board-comment__header', [ + m('.board-comment__meta', [m('b', name), timeOf(meta.mPublishTs) ? m('span', timeOf(meta.mPublishTs)) : null]), + m('button.board-comment__menu[type=button][aria-label=Comment options][title=Comment options]', m('i.fas.fa-ellipsis-v')), + ]), + m('p.board-comment__text', textOf(comment)), + m('.board-comment__actions', [ + m('button[type=button]', { + disabled: !voteIdentity, + onclick: () => util.voteForComment(forumId, msgId, key, util.GXS_VOTE_UP, voteIdentity), + }, [m('i.fas.fa-thumbs-up'), ` ${comment.mUpVotes || 0}`]), + m('button[type=button]', { + disabled: !voteIdentity, + onclick: () => util.voteForComment(forumId, msgId, key, util.GXS_VOTE_DOWN, voteIdentity), + }, m('i.fas.fa-thumbs-down')), + m('button[type=button]', { onclick: () => { replyTo = comment; composerText = ''; submitError = ''; } }, 'Reply') + ]), + repliesCount ? m('button.board-comment__replies-toggle[type=button]', { + 'aria-expanded': repliesExpanded, + onclick: () => { expandedReplies[key] = !repliesExpanded; }, + }, [ + `${repliesCount} ${repliesCount === 1 ? 'reply' : 'replies'} `, + m('i.fas', { class: repliesExpanded ? 'fa-chevron-up' : 'fa-chevron-down' }), + ]) : null, + repliesCount && repliesExpanded + ? m('.board-comment__replies', node.children.map((reply) => renderComment(reply, depth + 1, forumId, msgId))) + : null, + ]) + ]); + } +} module.exports = { BoardView, + PostView, createboard, }; diff --git a/webui-src/app/boards/boards.js b/webui-src/app/boards/boards.js index e31c11ad..53a4688e 100644 --- a/webui-src/app/boards/boards.js +++ b/webui-src/app/boards/boards.js @@ -7,32 +7,40 @@ const peopleUtil = require('people/people_util'); const getBoards = { All: [], - PopularBoards: [], - SubscribedBoards: [], + Popular: [], + Subscribed: [], MyBoards: [], - OtherBoards: [], + Other: [], async load() { - const res = await rs.rsJsonApiRequest('/rsPosted/getBoardsSummaries'); - const data = res.body; - getBoards.All = data.groupInfo; - getBoards.PopularBoards = getBoards.All; - getBoards.PopularBoards.sort((a, b) => b.mPop - a.mPop); - getBoards.OtherBoards = getBoards.PopularBoards.slice(5); - getBoards.PopularBoards = getBoards.PopularBoards.slice(0, 5); - getBoards.SubscribedBoards = getBoards.All.filter( - (board) => board.mSubscribeFlags === util.GROUP_SUBSCRIBE_SUBSCRIBED - ); - getBoards.MyBoards = getBoards.All.filter( - (board) => board.mSubscribeFlags === util.GROUP_MY_BOARD - ); + try { + const res = await rs.rsJsonApiRequest('/rsPosted/getBoardsSummaries'); + const boards = res && res.body && Array.isArray(res.body.groupInfo) ? res.body.groupInfo : null; + if (!boards) { + console.warn('Boards summaries response did not include groupInfo', res && res.body); + return; + } + getBoards.All = boards; + const popular = [...boards].sort((a, b) => (b.mPop || 0) - (a.mPop || 0)); + getBoards.Other = popular.slice(5); + getBoards.Popular = popular.slice(0, 5); + getBoards.Subscribed = boards.filter( + (board) => board.mSubscribeFlags === util.GROUP_SUBSCRIBE_SUBSCRIBED + ); + getBoards.MyBoards = boards.filter( + (board) => board.mSubscribeFlags === util.GROUP_MY_BOARD + ); + m.redraw(); + } catch (error) { + console.warn('Failed to load board summaries', error); + } }, }; const sections = { MyBoards: require('boards/my_boards'), - SubscribedBoards: require('boards/subscribed_boards'), - PopularBoards: require('boards/popular_boards'), - OtherBoards: require('boards/other_boards'), + Subscribed: require('boards/subscribed_boards'), + Popular: require('boards/popular_boards'), + Other: require('boards/other_boards'), }; const Layout = () => { @@ -40,8 +48,8 @@ const Layout = () => { return { oninit: () => { - rs.setBackgroundTask(getBoards.load, 5000, () => { - // return m.route.get() === '/files/files'; + rs.setBackgroundTask(getBoards.load, 30000, () => { + return m.route.get().startsWith('/boards'); }); peopleUtil.ownIds((data) => { ownId = data; @@ -95,6 +103,7 @@ module.exports = { m(widget.Sidebar, { tabs: Object.keys(sections), baseRoute: '/boards/', + mobileDrawer: true, }), m('.node-panel', m(Layout, { pathInfo: vnode.attrs })), ]; diff --git a/webui-src/app/boards/boards_util.js b/webui-src/app/boards/boards_util.js index 1519b81c..2a7a3f9b 100644 --- a/webui-src/app/boards/boards_util.js +++ b/webui-src/app/boards/boards_util.js @@ -1,5 +1,6 @@ const m = require('mithril'); const rs = require('rswebui'); +const peopleUtil = require('people/people_util'); const GROUP_SUBSCRIBE_ADMIN = 0x01; // means: you have the admin key for this group const GROUP_SUBSCRIBE_PUBLISH = 0x02; // means: you have the publish key for thiss group. Typical use: publish key in channels are shared with specific friends. @@ -20,90 +21,220 @@ const Data = { Comments: {}, // threadID, msgID -> {Comment, showReplies} }; +// Older Qt clients store board notes as rich HTML. Render them as readable, +// inert text in the web UI instead of exposing the markup and embedded CSS. +function plainText(value) { + if (value === null || value === undefined) return ''; + const text = String(value); + if (!/<\/?[a-z][^>]*>/i.test(text)) return text.trim(); + return text + .replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, '') + .replace(/<(br|\/p|\/div|\/li|\/h[1-6])\b[^>]*>/gi, '\n') + .replace(/<[^>]*>/g, '') + .replace(/&(nbsp|#160);/gi, ' ') + .replace(/&/gi, '&') + .replace(/</gi, '<') + .replace(/>/gi, '>') + .replace(/"/gi, '"') + .replace(/'/gi, '\'') + .replace(/\n\s*\n+/g, '\n') + .trim(); +} + +async function updateContent(content, boardid) { + const msgId = content.mMsgId || content.msgId || content; + try { + const res = await rs.rsJsonApiRequest('/rsPosted/getBoardContent', { + boardId: boardid, + contentsIds: [msgId], + }); + if (res && res.body && res.body.retval) { + const posts = res.body.posts || res.body.postList || []; + const comments = res.body.comments || res.body.commentList || []; + const votes = res.body.votes || res.body.voteList || []; + + if (posts.length > 0) { + if (!Data.Posts[boardid]) Data.Posts[boardid] = {}; + Data.Posts[boardid][msgId] = { post: posts[0], isSearched: true }; + m.redraw(); + } else if (comments.length > 0) { + const threadId = content.mThreadId || comments[0].mMeta.mThreadId; + if (Data.Comments[threadId] === undefined) { + Data.Comments[threadId] = {}; + } + Data.Comments[threadId][msgId] = comments[0]; + m.redraw(); + } else if (votes.length > 0) { + const vote = votes[0]; + if ( + Data.Comments[vote.mMeta.mThreadId] && + Data.Comments[vote.mMeta.mThreadId][vote.mMeta.mParentId] + ) { + if (vote.mVoteType === GXS_VOTE_UP) { + Data.Comments[vote.mMeta.mThreadId][vote.mMeta.mParentId].mUpVotes += 1; + } + if (vote.mVoteType === GXS_VOTE_DOWN) { + Data.Comments[vote.mMeta.mThreadId][vote.mMeta.mParentId].mDownVotes += 1; + } + m.redraw(); + } + } + } + } catch (err) { + console.warn('updateContent error:', err); + } +} + +const inFlightBoards = {}; + async function updateDisplayBoards(keyid, details) { - const res1 = await rs.rsJsonApiRequest('/rsPosted/getBoardsInfo', { - boardsIds: [keyid], - }); - details = res1.body.boardsInfo[0]; - Data.DisplayBoards[keyid] = { - name: details.mMeta.mGroupName, - isSearched: true, - description: details.mDescription, - image: details.mGroupImage, - author: details.mMeta.mAuthorId, - isSubscribed: - details.mMeta.mSubscribeFlags === GROUP_SUBSCRIBE_SUBSCRIBED || - details.mMeta.mSubscribeFlags === GROUP_MY_BOARD, - posts: details.mMeta.mVisibleMsgCount, - activity: details.mMeta.mLastPost, - created: details.mMeta.mPublishTs, - all: details, - }; + if (!keyid) return Promise.resolve(); + + // 1. Fast path: if posts for this board are already loaded in memory, render instantly and do not re-fetch + if (Data.DisplayBoards[keyid] && Data.Posts[keyid] && Object.keys(Data.Posts[keyid]).length > 0) { + m.redraw(); + return Promise.resolve(); + } - if (Data.Posts[keyid] === undefined) { - Data.Posts[keyid] = {}; + // 2. Prevent duplicate concurrent HTTP requests for the same board ID + if (inFlightBoards[keyid]) { + return inFlightBoards[keyid]; } - /* const res2 = await rs.rsJsonApiRequest('/rsPosted/getContentSummaries', { - boardId: keyid, - }); + inFlightBoards[keyid] = (async () => { + try { + // Fetch board info metadata if missing + if (!Data.DisplayBoards[keyid]) { + const res1 = await rs.rsJsonApiRequest('/rsPosted/getBoardsInfo', { + boardsIds: [keyid], + }); + if (res1 && res1.body && res1.body.boardsInfo && res1.body.boardsInfo.length > 0) { + details = res1.body.boardsInfo[0]; + Data.DisplayBoards[keyid] = { + name: details.mMeta.mGroupName, + isSearched: true, + description: details.mDescription, + image: details.mGroupImage, + author: details.mMeta.mAuthorId, + isSubscribed: + details.mMeta.mSubscribeFlags === GROUP_SUBSCRIBE_SUBSCRIBED || + details.mMeta.mSubscribeFlags === GROUP_MY_BOARD, + posts: details.mMeta.mVisibleMsgCount, + activity: details.mMeta.mLastPost, + created: details.mMeta.mPublishTs, + all: details, + }; + m.redraw(); + } + } - if (res2.body.retval) { - res2.body.summaries.map((content) => { - updateContent(content, keyid); - }); - }*/ + if (!Data.Posts[keyid]) { + Data.Posts[keyid] = {}; + } + + // Fetch all board content via /rsPosted/getBoardAllContent + const resAll = await rs.rsJsonApiRequest('/rsPosted/getBoardAllContent', { + boardId: keyid, + groupId: keyid, + handle: keyid, + }); + + if (resAll && resAll.body && resAll.body.retval) { + const posts = resAll.body.posts || resAll.body.postList || []; + if (posts.length > 0) { + posts.forEach((post) => { + const msgId = (post.mMeta && post.mMeta.mMsgId) ? post.mMeta.mMsgId : post.mMsgId; + if (msgId) { + Data.Posts[keyid][msgId] = { post, isSearched: true }; + } + }); + m.redraw(); + } + } + } catch (err) { + console.warn('updateDisplayBoards network error for board:', keyid, err); + } finally { + delete inFlightBoards[keyid]; + } + })(); + + return inFlightBoards[keyid]; } -const DisplayBoardsFromList = () => { +const BoardSummary = () => { return { - oninit: (v) => {}, - view: (v) => - m( + view: (vnode) => { + const details = vnode.attrs.details; + const bname = details.mGroupName || details.name || ''; + const bsubscribed = + details.mSubscribeFlags === GROUP_SUBSCRIBE_SUBSCRIBED || + details.mSubscribeFlags === GROUP_MY_BOARD; + const bposts = details.mVisibleMsgCount || details.posts || 0; + const createDate = details.mPublishTs || details.created; + const lastActivity = details.mLastPost || details.activity; + + return m( 'tr', { - key: v.attrs.id, - class: - Data.DisplayBoards[v.attrs.id] && Data.DisplayBoards[v.attrs.id].isSearched - ? '' - : 'hidden', + key: details.mGroupId, onclick: () => { m.route.set('/boards/:tab/:mGroupId', { - tab: v.attrs.category, - mGroupId: v.attrs.id, + tab: vnode.attrs.category, + mGroupId: details.mGroupId, }); }, }, - [m('td', Data.DisplayBoards[v.attrs.id] ? Data.DisplayBoards[v.attrs.id].name : '')] - ), + [ + m('td', bname), + ] + ); + }, }; }; -const BoardSummary = () => { - let keyid = {}; +const BoardTable = () => { return { - oninit: (v) => { - keyid = v.attrs.details.mGroupId; - updateDisplayBoards(keyid); - }, - - view: (v) => {}, + view: (vnode) => + m('table.board-table', [ + m('thead', [ + m('tr', [ + m('th', 'Board Name'), + ]), + ]), + vnode.children, + ]), }; }; -const BoardTable = () => { +const SearchBar = () => { + let searchString = ''; return { - oninit: (v) => {}, - view: (v) => m('table.boards', [m('tr', [m('th', 'Board Name')]), v.children]), + view: (vnode) => + m('.search-bar', [ + m('input[type=text][placeholder=Search Boards...]', { + value: searchString, + oninput: (e) => { + searchString = e.target.value; + const query = searchString.toLowerCase(); + if (vnode.attrs.list) { + vnode.attrs.list.forEach((board) => { + const name = (board.mGroupName || board.name || '').toLowerCase(); + board.isSearched = name.includes(query); + }); + } + }, + }), + ]), }; }; function popupmessage(message) { const container = document.getElementById('modal-container'); + if (!container) return; container.style.display = 'block'; m.render( container, - m('.modal-content[id=composepopup]', [ + m('.modal-content', [ m( 'button.red', { @@ -116,41 +247,78 @@ function popupmessage(message) { ); } -const SearchBar = () => { - let searchString = ''; - return { - view: (v) => - m('input[type=text][id=searchboard][placeholder=Search Subject].searchbar', { - value: searchString, - oninput: (e) => { - searchString = e.target.value.toLowerCase(); - for (const hash in Data.DisplayBoards) { - if (Data.DisplayBoards[hash].name.toLowerCase().indexOf(searchString) > -1) { - Data.DisplayBoards[hash].isSearched = true; - } else { - Data.DisplayBoards[hash].isSearched = false; - } - } - }, - }), - }; -}; +async function voteForPost(postGrpId, postMsgId, voteType, voterId = null) { + try { + let authorId = voterId; + if (!authorId) { + // Goes through people_util so the endpoints and their caching stay in + // one place: /rsIdentity/getOwnIds is deprecated and answers 404. + const ownIds = await peopleUtil.ownIds(); + if (ownIds.length === 0) { + alert('No identity found to vote.'); + return false; + } + authorId = ownIds[0]; + } + + const res = await rs.rsJsonApiRequest('/rsPosted/voteForPost', { + postGrpId, + postMsgId, + authorId, + vote: voteType, + }); + + if (res && res.body && res.body.retval) { + updateDisplayBoards(postGrpId); + m.redraw(); + return true; + } + } catch (e) { + console.error('voteForPost error:', e); + } + return false; +} + +async function voteForComment(boardId, postId, commentId, voteType, authorId) { + if (!authorId) return false; + try { + const res = await rs.rsJsonApiRequest('/rsPosted/voteForComment', { + boardId, + postId, + commentId, + authorId, + vote: voteType, + }); + if (res && res.body && res.body.retval) { + updateDisplayBoards(boardId); + m.redraw(); + return true; + } + console.warn('voteForComment failed:', res && res.body && res.body.errorMessage); + } catch (error) { + console.error('voteForComment error:', error); + } + return false; +} module.exports = { Data, - SearchBar, - popupmessage, - BoardSummary, - DisplayBoardsFromList, updateDisplayBoards, + updateContent, + BoardSummary, BoardTable, + SearchBar, + popupmessage, + voteForPost, + voteForComment, + plainText, + GXS_VOTE_UP, + GXS_VOTE_DOWN, GROUP_SUBSCRIBE_ADMIN, - GROUP_SUBSCRIBE_NOT_SUBSCRIBED, GROUP_SUBSCRIBE_PUBLISH, GROUP_SUBSCRIBE_SUBSCRIBED, + GROUP_SUBSCRIBE_NOT_SUBSCRIBED, GROUP_MY_BOARD, - GXS_VOTE_DOWN, - GXS_VOTE_UP, PUBLIC, EXTERNAL, NODES_GROUP, diff --git a/webui-src/app/boards/my_boards.js b/webui-src/app/boards/my_boards.js index dd345e74..48e0c399 100644 --- a/webui-src/app/boards/my_boards.js +++ b/webui-src/app/boards/my_boards.js @@ -9,18 +9,14 @@ const Layout = () => { m( util.BoardTable, m('tbody', [ - v.attrs.list.map((board) => - m(util.BoardSummary, { - details: board, - category: 'MyBoards', - }) - ), - v.attrs.list.map((board) => - m(util.DisplayBoardsFromList, { - id: board.mGroupId, - category: 'MyBoards', - }) - ), + v.attrs.list && + v.attrs.list.map((board) => + m(util.BoardSummary, { + key: board.mGroupId, + details: board, + category: 'MyBoards', + }) + ), ]) ), ]), @@ -28,4 +24,4 @@ const Layout = () => { }; }; -module.exports = Layout(); +module.exports = Layout; diff --git a/webui-src/app/boards/other_boards.js b/webui-src/app/boards/other_boards.js index 685e67b4..a430e83b 100644 --- a/webui-src/app/boards/other_boards.js +++ b/webui-src/app/boards/other_boards.js @@ -9,18 +9,14 @@ const Layout = () => { m( util.BoardTable, m('tbody', [ - v.attrs.list.map((board) => - m(util.BoardSummary, { - details: board, - category: 'OtherBoards', - }) - ), - v.attrs.list.map((board) => - m(util.DisplayBoardsFromList, { - id: board.mGroupId, - category: 'OtherBoards', - }) - ), + v.attrs.list && + v.attrs.list.map((board) => + m(util.BoardSummary, { + key: board.mGroupId, + details: board, + category: 'Other', + }) + ), ]) ), ]), @@ -28,4 +24,4 @@ const Layout = () => { }; }; -module.exports = Layout(); +module.exports = Layout; diff --git a/webui-src/app/boards/popular_boards.js b/webui-src/app/boards/popular_boards.js index 752b24e2..ef200443 100644 --- a/webui-src/app/boards/popular_boards.js +++ b/webui-src/app/boards/popular_boards.js @@ -9,18 +9,14 @@ const Layout = () => { m( util.BoardTable, m('tbody', [ - v.attrs.list.map((board) => - m(util.BoardSummary, { - details: board, - category: 'PopularBoards', - }) - ), - v.attrs.list.map((board) => - m(util.DisplayBoardsFromList, { - id: board.mGroupId, - category: 'PopularBoards', - }) - ), + v.attrs.list && + v.attrs.list.map((board) => + m(util.BoardSummary, { + key: board.mGroupId, + details: board, + category: 'Popular', + }) + ), ]) ), ]), diff --git a/webui-src/app/boards/subscribed_boards.js b/webui-src/app/boards/subscribed_boards.js index 812933ee..6d97a94c 100644 --- a/webui-src/app/boards/subscribed_boards.js +++ b/webui-src/app/boards/subscribed_boards.js @@ -9,18 +9,14 @@ const Layout = () => { m( util.BoardTable, m('tbody', [ - v.attrs.list.map((board) => - m(util.BoardSummary, { - details: board, - category: 'SubscribedBoards', - }) - ), - v.attrs.list.map((board) => - m(util.DisplayBoardsFromList, { - id: board.mGroupId, - category: 'SubscribedBoards', - }) - ), + v.attrs.list && + v.attrs.list.map((board) => + m(util.BoardSummary, { + key: board.mGroupId, + details: board, + category: 'Subscribed', + }) + ), ]) ), ]), diff --git a/webui-src/app/channels/channel_view.js b/webui-src/app/channels/channel_view.js index 31101cc2..75ac7d13 100644 --- a/webui-src/app/channels/channel_view.js +++ b/webui-src/app/channels/channel_view.js @@ -7,6 +7,7 @@ const peopleUtil = require('people/people_util'); const sha1 = require('channels/sha1'); const fileUtil = require('files/files_util'); const fileDown = require('files/files_downloads'); +const chatEmoji = require('chat/chat_emoji'); const filesUploadHashes = { // figure out a better way later. @@ -14,6 +15,29 @@ const filesUploadHashes = { Thumbnail: [], }; +function channelThumbnailSrc(post) { + const thumbnail = post && (post.mThumbnail || post.thumbnail || post.mImage); + const base64 = thumbnail && thumbnail.mData && thumbnail.mData.base64 + ? thumbnail.mData.base64 + : typeof thumbnail === 'string' + ? thumbnail + : thumbnail && thumbnail.base64; + if (!base64 || !String(base64).trim()) return ''; + return String(base64).startsWith('data:') ? base64 : `data:image/png;base64,${base64}`; +} + +const ChannelFallbackThumbnail = () => ({ + view: (vnode) => m('.channel-post__placeholder', { style: { + display: vnode.attrs.hidden ? 'none' : 'flex', flex: '1 1 auto', minHeight: '0', + flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: '.35rem', + color: '#64748b', background: 'linear-gradient(135deg, #f8fafc, #dbe5f1)', + } }, [ + m('i.fas.fa-image[aria-hidden=true]', { style: { fontSize: '1.35rem', color: '#64748b' } }), + m('span', { style: { fontSize: '2rem', fontWeight: '700', color: '#2563eb' } }, (vnode.attrs.title || 'Post').trim().slice(0, 1).toUpperCase()), + m('small', { style: { fontSize: '.72rem', fontWeight: '600' } }, 'No image'), + ]), +}); + async function parsefile(file, type) { const fileSize = file.size; const chunkSize = 1024 * 1024; // bytes @@ -373,12 +397,9 @@ const ChannelView = () => { m('.widget__body', [ m('.media-item', [ m('.media-item__details', [ - m('img', { - src: - cimage.mData.base64 === '' - ? 'data/streaming.png' - : `data:image/png;base64,${cimage.mData.base64}`, - }), + cimage && cimage.mData && cimage.mData.base64 + ? m('img', { src: `data:image/png;base64,${cimage.mData.base64}` }) + : null, m('.media-item__details-info', [ m('div', [m('b', 'Posts: '), m('span', cposts)]), m('div', [ @@ -428,7 +449,14 @@ const ChannelView = () => { m( '.posts-container-card', { - style: 'display: ' + (plist[key].isSearched ? 'flex' : 'none'), // for search + style: { + display: plist[key].isSearched ? 'flex' : 'none', // for search + height: '240px', + minHeight: '0', + overflow: 'hidden', + flexDirection: 'column', + alignSelf: 'start', + }, onclick: () => { m.route.set('/channels/:tab/:mGroupId/:mMsgId', { tab: m.route.param().tab, @@ -438,13 +466,19 @@ const ChannelView = () => { }, }, [ - m('img', { - src: - plist[key].post.mThumbnail.mData.base64 === '' - ? 'data/streaming.png' - : 'data:image/png;base64,' + plist[key].post.mThumbnail.mData.base64, - alt: 'No Thumbnail', - }), + channelThumbnailSrc(plist[key].post) + ? [ + m('img', { + src: channelThumbnailSrc(plist[key].post), + alt: plist[key].post.mMeta.mMsgName || 'Post thumbnail', + onerror: (e) => { + e.target.style.display = 'none'; + if (e.target.nextSibling) e.target.nextSibling.style.display = 'flex'; + }, + }), + m(ChannelFallbackThumbnail, { title: plist[key].post.mMeta.mMsgName, hidden: true }), + ] + : m(ChannelFallbackThumbnail, { title: plist[key].post.mMeta.mMsgName }), m('p', plist[key].post.mMeta.mMsgName), ] ), @@ -665,20 +699,156 @@ function displaycomment() { }; } +/* Modern threaded comment experience for channel posts. */ +const ChannelComments = () => { + let replyTo = null; + let text = ''; + let identity = null; + let submitting = false; + let error = ''; + let showEmojiPicker = false; + const expandedReplies = {}; + + const metaOf = (comment) => (comment && comment.mMeta) || {}; + const idOf = (comment) => metaOf(comment).mMsgId || comment.msgId; + const nameOf = (id) => rs.userList.username(id) || rs.userList.userMap[id] || `${String(id || 'Unknown').slice(0, 10)}…`; + const initials = (name) => String(name || '?').split(/\s+/).filter(Boolean).slice(0, 2).map((part) => part[0]).join('').toUpperCase(); + const dateOf = (value) => { + const seconds = value && typeof value === 'object' ? value.xint64 : value; + return Number(seconds) ? new Date(Number(seconds) * 1000).toLocaleString() : ''; + }; + + function tree(threadId) { + const nodes = {}; + const roots = []; + Object.keys(Data.Comments[threadId] || {}).forEach((key) => { + const entry = Data.Comments[threadId][key]; + const comment = entry.comment || entry; + if (idOf(comment)) nodes[idOf(comment)] = { comment, children: [] }; + }); + Object.keys(nodes).forEach((key) => { + const node = nodes[key]; + const parent = metaOf(node.comment).mParentId; + if (parent && parent !== threadId && nodes[parent]) nodes[parent].children.push(node); + else roots.push(node); + }); + const chronological = (a, b) => Number(metaOf(a.comment).mPublishTs && (metaOf(a.comment).mPublishTs.xint64 || metaOf(a.comment).mPublishTs)) - Number(metaOf(b.comment).mPublishTs && (metaOf(b.comment).mPublishTs.xint64 || metaOf(b.comment).mPublishTs)); + roots.sort(chronological); + Object.keys(nodes).forEach((key) => nodes[key].children.sort(chronological)); + return roots; + } + + async function submit(vnode) { + const comment = text.trim(); + if (!comment || !identity || submitting) return; + submitting = true; + error = ''; + try { + const res = await rs.rsJsonApiRequest('/rsgxschannels/createCommentV2', { + channelId: vnode.attrs.channelId, + threadId: vnode.attrs.threadId, + comment, + authorId: identity, + parentId: replyTo ? idOf(replyTo) : vnode.attrs.threadId, + }); + if (!res || !res.body || res.body.retval === false) { + error = (res && res.body && res.body.errorMessage) || 'Your comment could not be posted.'; + return; + } + text = ''; + replyTo = null; + await util.updatedisplaychannels(vnode.attrs.channelId); + } catch (submitError) { + console.warn('Channel comment submission failed', submitError); + error = 'Your comment could not be posted. Please try again.'; + } finally { + submitting = false; + m.redraw(); + } + } + + function renderComment(node, vnode) { + const comment = node.comment; + const meta = metaOf(comment); + const id = idOf(comment); + const name = nameOf(meta.mAuthorId); + const votes = (Data.Votes[meta.mThreadId] && Data.Votes[meta.mThreadId][id]) || { upvotes: 0, downvotes: 0 }; + const repliesExpanded = expandedReplies[id] === true; + return m('.board-comment', { key: id }, [ + m('.board-comment-avatar', initials(name)), + m('.board-comment__content', [ + m('.board-comment__header', [ + m('.board-comment__meta', [m('b', name), dateOf(meta.mPublishTs) ? m('span', dateOf(meta.mPublishTs)) : null]), + m('button.board-comment__menu[type=button][aria-label=Comment options]', m('i.fas.fa-ellipsis-v')), + ]), + m('p.board-comment__text', comment.mComment || comment.comment || ''), + m('.board-comment__actions', [ + m('button[type=button]', { disabled: !vnode.attrs.voteIdentity, onclick: () => addvote(util.GXS_VOTE_UP, vnode.attrs.channelId, vnode.attrs.threadId, vnode.attrs.voteIdentity, id) }, [m('i.fas.fa-thumbs-up'), ` ${votes.upvotes || 0}`]), + m('button[type=button]', { disabled: !vnode.attrs.voteIdentity, onclick: () => addvote(util.GXS_VOTE_DOWN, vnode.attrs.channelId, vnode.attrs.threadId, vnode.attrs.voteIdentity, id) }, m('i.fas.fa-thumbs-down')), + m('button[type=button]', { onclick: () => { replyTo = comment; text = ''; error = ''; } }, 'Reply'), + ]), + node.children.length ? m('button.board-comment__replies-toggle[type=button]', { 'aria-expanded': repliesExpanded, onclick: () => { expandedReplies[id] = !repliesExpanded; } }, [`${node.children.length} ${node.children.length === 1 ? 'reply' : 'replies'} `, m('i.fas', { class: repliesExpanded ? 'fa-chevron-up' : 'fa-chevron-down' })]) : null, + node.children.length && repliesExpanded ? m('.board-comment__replies', node.children.map((child) => renderComment(child, vnode))) : null, + ]), + ]); + } + + return { + view: (vnode) => { + const identities = (vnode.attrs.identities || []).filter((id) => Number(id) !== 0); + if (!identity && identities.length) identity = identities[0]; + const comments = tree(vnode.attrs.threadId); + return m('.board-comments.channel-comments', [ + m('.board-comments__heading', [ + m('h3', `${Object.keys(Data.Comments[vnode.attrs.threadId] || {}).length} Comment${Object.keys(Data.Comments[vnode.attrs.threadId] || {}).length === 1 ? '' : 's'}`), + m('span', [m('i.fas.fa-sort-amount-down'), ' Oldest first']), + m('.board-comments__voter', [ + m('label[for=channel-comment-voter]', 'Voter identity'), + m('select#channel-comment-voter', { + value: vnode.attrs.voteIdentity || '', + disabled: identities.length === 0, + onchange: (e) => vnode.attrs.onVoteIdentity(e.target.value), + }, identities.length + ? identities.map((id) => m('option', { value: id }, nameOf(id))) + : m('option', { value: '' }, vnode.attrs.identitiesLoading ? 'Loading identities…' : 'No identity available')), + ]), + ]), + m('.board-comment-composer', [ + m('.board-comment-avatar', initials(nameOf(identity))), + m('.board-comment-composer__body', [ + replyTo ? m('.board-comment-composer__replying', ['Replying to ', m('b', nameOf(metaOf(replyTo).mAuthorId)), m('button[type=button][aria-label=Cancel reply]', { onclick: () => { replyTo = null; text = ''; } }, m('i.fas.fa-times'))]) : null, + identities.length ? m('select.board-comment-composer__identity', { value: identity, onchange: (e) => { identity = e.target.value; } }, identities.map((id) => m('option', { value: id }, nameOf(id)))) : null, + m('textarea.board-comment-composer__input[rows=1][placeholder=Add a comment…]', { value: text, disabled: !identity || submitting, oninput: (e) => { text = e.target.value; }, onkeydown: (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') submit(vnode); } }), + !identity ? m('p.board-comment-composer__hint', vnode.attrs.identitiesLoading ? 'Loading identities…' : 'Create or select an identity to post a comment.') : null, + error ? m('p.board-comment-composer__error', error) : null, + m('.board-comment-composer__actions', [ + m('.board-comment-composer__emoji', { style: { position: 'relative', marginRight: 'auto' } }, [ + m('button[type=button][title=Insert emoji][aria-label=Insert emoji]', { style: { width: '32px', height: '32px', padding: '0', borderRadius: '50%', border: '0', boxShadow: 'none', background: showEmojiPicker ? '#e0f2fe' : 'transparent', color: '#475569', fontSize: '1.15rem' }, onclick: () => { showEmojiPicker = !showEmojiPicker; } }, m('i.fas.fa-smile')), + showEmojiPicker ? m('.board-comment-emoji-popover', { style: { position: 'absolute', zIndex: '20', top: '38px', left: '0', width: '250px', maxHeight: '180px', overflowY: 'auto', padding: '.5rem', display: 'grid', gridTemplateColumns: 'repeat(8, 1fr)', gap: '.2rem', background: '#fff', border: '1px solid #cbd5e1', borderRadius: '8px', boxShadow: '0 8px 20px rgba(0,0,0,.16)' } }, chatEmoji.EMOJI_DATA.Smileys.slice(0, 48).map((emoji) => m('button[type=button]', { style: { width: '28px', height: '28px', padding: '0', border: '0', boxShadow: 'none', background: 'transparent', fontSize: '1.1rem' }, onclick: () => { text += emoji; showEmojiPicker = false; } }, emoji))) : null, + ]), + text || replyTo ? m('button.board-comment-composer__cancel[type=button]', { onclick: () => { text = ''; replyTo = null; error = ''; } }, 'Cancel') : null, + m('button.board-comment-composer__submit[type=button]', { disabled: !text.trim() || !identity || submitting, onclick: () => submit(vnode) }, submitting ? 'Posting…' : 'Comment'), + ]), + ]), + ]), + comments.length ? m('.board-comments__list', comments.map((node) => renderComment(node, vnode))) : m('.board-comments__empty', [m('i.fas.fa-comment'), m('p', 'No comments yet. Start the conversation.')]), + ]); + }, + }; +}; + const PostView = () => { let post = {}; - let topComments = {}; const filesInfo = {}; let voteIdentity; let ownId; + let identitiesLoading = true; + let messageExpanded = false; return { oninit: async (v) => { if (Data.Posts[v.attrs.channelId] && Data.Posts[v.attrs.channelId][v.attrs.msgId]) { post = Data.Posts[v.attrs.channelId][v.attrs.msgId].post; } - if (Data.TopComments[v.attrs.msgId]) { - topComments = Data.TopComments[v.attrs.msgId]; // get all the top level parent comments - } if (post) { post.mFiles.map(async (file) => { const res = await rs.rsJsonApiRequest('/rsfiles/alreadyHaveFile', { @@ -696,10 +866,16 @@ const PostView = () => { } } voteIdentity = ownId[0]; + identitiesLoading = false; }); fileDown.Downloads.loadStatus(); // for retrieving downloading files. }, - view: (v) => [ + view: (v) => { + const message = post.mMsg || ''; + const messageText = String(message).replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); + const hasEmbeddedImage = / 280 || hasEmbeddedImage; + return [ m( 'a[title=Back]', { @@ -713,7 +889,19 @@ const PostView = () => { ), m('.widget__heading', m('h3', post.mMeta.mMsgName)), m('.widget__body', [ - m('p', { style: { whiteSpace: 'normal' } }, m.trust(post.mMsg)), + message ? m('.post-description', [ + m('.post-description__text', { + style: { + maxHeight: messageExpanded ? 'none' : '4.5em', + overflow: 'hidden', + lineHeight: '1.5', + }, + }, m.trust(message)), + hasLongMessage ? m('button.post-description__toggle[type=button]', { + style: { marginTop: '.35rem', padding: '0', border: '0', boxShadow: 'none', background: 'transparent', color: '#0f172a', fontSize: '.85rem', fontWeight: '700' }, + onclick: () => { messageExpanded = !messageExpanded; }, + }, messageExpanded ? 'Show less' : '…more') : null, + ]) : null, m('.file-section', [ m('h3', 'Files(' + post.mAttachmentCount + ')'), m( @@ -722,132 +910,77 @@ const PostView = () => { 'tbody', post.mFiles.map((file) => m('tr', [ - m('td', file.mName), - m('td', rs.formatBytes(file.mSize.xint64)), - m( - 'button', - { - style: { fontSize: '0.9em' }, - onclick: async () => - widget.popupMessage([ - m('p', 'Start Download?'), - m( - 'button', - { - onclick: async () => { - if (filesInfo[file.mHash] && !filesInfo[file.mHash].retval) { - const res = await rs.rsJsonApiRequest('/rsFiles/FileRequest', { - fileName: file.mName, - hash: file.mHash, - flags: util.RS_FILE_REQ_ANONYMOUS_ROUTING, - size: { - xstr64: file.mSize.xstr64, - }, - }); - res.body.retval === false - ? widget.popupMessage([ - m('h3', 'Error'), - m('hr'), - m('p', res.body.errorMessage), - ]) - : widget.popupMessage([ - m('h3', 'Success'), - m('hr'), - m('p', 'Download Started'), - ]); - m.redraw(); - } + m('td.channel-file__name[data-label=File name]', file.mName), + m('td.channel-file__size[data-label=Size]', rs.formatBytes(file.mSize.xint64)), + m('td.channel-file__action[data-label=Download]', [ + m( + 'button', + { + style: { fontSize: '0.9em' }, + onclick: async () => + widget.popupMessage([ + m('p', 'Start Download?'), + m( + 'button', + { + onclick: async () => { + if (filesInfo[file.mHash] && !filesInfo[file.mHash].retval) { + const res = await rs.rsJsonApiRequest('/rsFiles/FileRequest', { + fileName: file.mName, + hash: file.mHash, + flags: util.RS_FILE_REQ_ANONYMOUS_ROUTING, + size: { + xstr64: file.mSize.xstr64, + }, + }); + res.body.retval === false + ? widget.popupMessage([ + m('h3', 'Error'), + m('hr'), + m('p', res.body.errorMessage), + ]) + : widget.popupMessage([ + m('h3', 'Success'), + m('hr'), + m('p', 'Download Started'), + ]); + m.redraw(); + } + }, }, - }, - 'Start Download' - ), - ]), - }, - filesInfo[file.mHash] - ? filesInfo[file.mHash].retval - ? 'Open File' - : ['Download', m('i.fas.fa-download')] - : 'Please Wait...' - ), - fileDown.list[file.mHash] && // using the file from files_util to display download. - m(fileUtil.File, { + 'Start Download' + ), + ]), + }, + filesInfo[file.mHash] + ? filesInfo[file.mHash].retval + ? 'Open File' + : ['Download ', m('i.fas.fa-download')] + : 'Please Wait...' + ), + fileDown.list[file.mHash] && m(fileUtil.File, { info: fileDown.list[file.mHash], direction: 'down', transferred: fileDown.list[file.mHash].transfered.xint64, parts: [], }), + ]), ]) ) ) ), ]), - m('.comments-section', [ - m('h3', 'Comments'), - m('.comments-section__menu', [ - m( - 'button', - { - onclick: () => { - widget.popupMessage( - m(AddComment, { - parent_comment: '', - channelId: v.attrs.channelId, - authorId: ownId, - threadId: v.attrs.msgId, - parentId: v.attrs.msgId, - }) - ); - }, - }, - 'Add Comment' - ), - m('.comments-section__menu-id', [ - m('label[for=idtags', 'Voter ID: '), - m( - 'select[id=idtags]', - { - value: voteIdentity, - onchange: (e) => { - voteIdentity = ownId[e.target.selectedIndex]; - }, - }, - [ - ownId && - ownId.map((o) => - m( - 'option', - { value: o }, - `${rs.userList.userMap[o].toLocaleString()} (${o.slice(0, 8)}...)` - ) - ), - ] - ), - ]), - ]), - ]), - m( - util.CommentsTable, - m( - 'tbody', - Object.keys(topComments).map((key, index) => - Data.Comments[topComments[key].mMeta.mThreadId] && - Data.Comments[topComments[key].mMeta.mThreadId][topComments[key].mMeta.mMsgId] - ? m(displaycomment, { - // calls the recursive function for all the parents. - identity: ownId, - voteIdentity, - commentStruct: - Data.Comments[topComments[key].mMeta.mThreadId][ - topComments[key].mMeta.mMsgId - ], - replyDepth: 0, - }) - : '' - ) - ) - ), + m(ChannelComments, { + channelId: v.attrs.channelId, + threadId: v.attrs.msgId, + identities: ownId, + voteIdentity, + identitiesLoading, + onVoteIdentity: (id) => { voteIdentity = id; }, + }), ]), - ], + ]; + }, }; }; diff --git a/webui-src/app/channels/channels.js b/webui-src/app/channels/channels.js index d9dde399..cc423a90 100644 --- a/webui-src/app/channels/channels.js +++ b/webui-src/app/channels/channels.js @@ -7,38 +7,44 @@ const peopleUtil = require('people/people_util'); const getChannels = { All: [], - PopularChannels: [], - SubscribedChannels: [], + Popular: [], + Subscribed: [], MyChannels: [], - OtherChannels: [], + Other: [], async load() { - const res = await rs.rsJsonApiRequest('/rsgxschannels/getChannelsSummaries'); - const data = res.body; - getChannels.All = data.channels; - getChannels.SubscribedChannels = getChannels.All.filter( + try { + const res = await rs.rsJsonApiRequest('/rsgxschannels/getChannelsSummaries'); + const channels = res && res.body && Array.isArray(res.body.channels) ? res.body.channels : null; + if (!channels) { + console.warn('Channels summaries response did not include channels', res && res.body); + return; + } + getChannels.All = channels; + getChannels.Subscribed = channels.filter( (channel) => channel.mSubscribeFlags === util.GROUP_SUBSCRIBE_SUBSCRIBED || channel.mSubscribeFlags === util.GROUP_MY_CHANNEL // my channel is subscribed - ); - // getChannels.PopularChannels = getChannels.All; - getChannels.PopularChannels = getChannels.All.filter( - (a) => !getChannels.SubscribedChannels.includes(a) - ); - getChannels.PopularChannels.sort((a, b) => b.mPop - a.mPop); - getChannels.OtherChannels = getChannels.PopularChannels.slice(5); - getChannels.PopularChannels = getChannels.PopularChannels.slice(0, 5); + ); + const popular = channels.filter((channel) => !getChannels.Subscribed.includes(channel)); + popular.sort((a, b) => (b.mPop || 0) - (a.mPop || 0)); + getChannels.Other = popular.slice(5); + getChannels.Popular = popular.slice(0, 5); - getChannels.MyChannels = getChannels.All.filter( - (channel) => channel.mSubscribeFlags === util.GROUP_MY_CHANNEL - ); + getChannels.MyChannels = channels.filter( + (channel) => channel.mSubscribeFlags === util.GROUP_MY_CHANNEL + ); + m.redraw(); + } catch (error) { + console.warn('Failed to load channel summaries', error); + } }, }; const sections = { MyChannels: require('channels/my_channels'), - SubscribedChannels: require('channels/subscribed_channels'), - PopularChannels: require('channels/popular_channels'), - OtherChannels: require('channels/other_channels'), + Subscribed: require('channels/subscribed_channels'), + Popular: require('channels/popular_channels'), + Other: require('channels/other_channels'), }; const Layout = () => { @@ -110,6 +116,7 @@ module.exports = { m(widget.Sidebar, { tabs: Object.keys(sections), baseRoute: '/channels/', + mobileDrawer: true, }), m('.node-panel', m(Layout, { pathInfo: vnode.attrs })), ]; diff --git a/webui-src/app/channels/channels_util.js b/webui-src/app/channels/channels_util.js index 9e272d87..30f6249b 100644 --- a/webui-src/app/channels/channels_util.js +++ b/webui-src/app/channels/channels_util.js @@ -31,54 +31,83 @@ const Data = { Votes: {}, }; -async function updatecontent(content, channelid) { - const res = await rs.rsJsonApiRequest('/rsgxschannels/getChannelContent', { - channelId: channelid, - contentsIds: [content.mMsgId], - }); - if (res.body.retval && res.body.posts.length > 0) { - Data.Posts[channelid][content.mMsgId] = { post: res.body.posts[0], isSearched: true }; - } else if (res.body.retval && res.body.comments.length > 0) { - if (Data.Comments[content.mThreadId] === undefined) { - Data.Comments[content.mThreadId] = {}; - } - Data.Comments[content.mThreadId][content.mMsgId] = { - comment: res.body.comments[0], - showReplies: false, - }; // Comments[post][comment] - const comm = res.body.comments[0]; - if (Data.TopComments[comm.mMeta.mThreadId] === undefined) { - Data.TopComments[comm.mMeta.mThreadId] = {}; - } - if (comm.mMeta.mThreadId === comm.mMeta.mParentId) { - // this is a check for the top level comments - Data.TopComments[comm.mMeta.mThreadId][comm.mMeta.mMsgId] = comm; - // pushing top comments respective to post - } else { - if (Data.ParentCommentMap[comm.mMeta.mParentId] === undefined) { - Data.ParentCommentMap[comm.mMeta.mParentId] = {}; - } - Data.ParentCommentMap[comm.mMeta.mParentId][comm.mMeta.mMsgId] = comm; - } - } else if (res.body.retval && res.body.votes.length > 0) { - const vote = res.body.votes[0]; +// getChannelContent takes a set of ids, so a whole channel is fetched in a few +// requests instead of one per item. Chunked rather than sent as a single call so +// that no request grows unbounded and so the UI can paint as batches land. +const CONTENT_BATCH_SIZE = 200; - if (Data.Votes[vote.mMeta.mThreadId] === undefined) { - Data.Votes[vote.mMeta.mThreadId] = {}; - } - if (Data.Votes[vote.mMeta.mThreadId][vote.mMeta.mParentId] === undefined) { - Data.Votes[vote.mMeta.mThreadId][vote.mMeta.mParentId] = { upvotes: 0, downvotes: 0 }; - } - if (vote.mVoteType === GXS_VOTE_UP) { - Data.Votes[vote.mMeta.mThreadId][vote.mMeta.mParentId].upvotes += 1; - } +function storePost(post, channelid) { + const msgId = post.mMeta && post.mMeta.mMsgId; + if (!msgId) { + return; + } + Data.Posts[channelid][msgId] = { post, isSearched: true }; +} - if (vote.mVoteType === GXS_VOTE_DOWN) { - Data.Votes[vote.mMeta.mThreadId][vote.mMeta.mParentId].downvotes += 1; +function storeComment(comm) { + const meta = comm.mMeta; + if (!meta) { + return; + } + if (Data.Comments[meta.mThreadId] === undefined) { + Data.Comments[meta.mThreadId] = {}; + } + Data.Comments[meta.mThreadId][meta.mMsgId] = { comment: comm, showReplies: false }; // Comments[post][comment] + if (Data.TopComments[meta.mThreadId] === undefined) { + Data.TopComments[meta.mThreadId] = {}; + } + if (meta.mThreadId === meta.mParentId) { + // this is a check for the top level comments + Data.TopComments[meta.mThreadId][meta.mMsgId] = comm; + // pushing top comments respective to post + } else { + if (Data.ParentCommentMap[meta.mParentId] === undefined) { + Data.ParentCommentMap[meta.mParentId] = {}; } + Data.ParentCommentMap[meta.mParentId][meta.mMsgId] = comm; } } +function storeVote(vote) { + const meta = vote.mMeta; + if (!meta) { + return; + } + if (Data.Votes[meta.mThreadId] === undefined) { + Data.Votes[meta.mThreadId] = {}; + } + if (Data.Votes[meta.mThreadId][meta.mParentId] === undefined) { + Data.Votes[meta.mThreadId][meta.mParentId] = { upvotes: 0, downvotes: 0 }; + } + if (vote.mVoteType === GXS_VOTE_UP) { + Data.Votes[meta.mThreadId][meta.mParentId].upvotes += 1; + } + + if (vote.mVoteType === GXS_VOTE_DOWN) { + Data.Votes[meta.mThreadId][meta.mParentId].downvotes += 1; + } +} + +async function updatecontent(contentIds, channelid) { + const ids = Array.isArray(contentIds) ? contentIds : [contentIds]; + if (ids.length === 0) { + return; + } + const res = await rs.rsJsonApiRequest('/rsgxschannels/getChannelContent', { + channelId: channelid, + contentsIds: ids, + }); + // rsJsonApiRequest resolves to undefined when the request never made it out + if (!res || !res.body || !res.body.retval) { + return; + } + // A batch mixes the three kinds, so all three lists have to be walked. The + // metadata of each item is used rather than the summary it was asked from. + (res.body.posts || []).forEach((post) => storePost(post, channelid)); + (res.body.comments || []).forEach(storeComment); + (res.body.votes || []).forEach(storeVote); +} + async function updatedisplaychannels(keyid, details) { const res1 = await rs.rsJsonApiRequest('/rsgxschannels/getChannelsInfo', { chanIds: [keyid], @@ -107,10 +136,16 @@ async function updatedisplaychannels(keyid, details) { channelId: keyid, }); - if (res2.body.retval) { - res2.body.summaries.map(async (content) => { - await updatecontent(content, keyid); - }); + if (!res2 || !res2.body || !res2.body.retval || !Array.isArray(res2.body.summaries)) { + return; + } + + const ids = res2.body.summaries.map((content) => content.mMsgId).filter(Boolean); + // Sequential on purpose: this runs once per channel of the list, so firing the + // batches concurrently would put the browser back where it started. + for (let i = 0; i < ids.length; i += CONTENT_BATCH_SIZE) { + await updatecontent(ids.slice(i, i + CONTENT_BATCH_SIZE), keyid); + m.redraw(); } } const DisplayChannelsFromList = () => { @@ -173,8 +208,8 @@ const FilesTable = () => { return { oninit: (v) => {}, view: (v) => - m('table.files', [ - m('tr', [m('th', 'File Name'), m('th', 'Size'), m('th', m('i.fas.fa-download'))]), + m('table.files.channel-files', [ + m('thead', m('tr', [m('th', 'File Name'), m('th', 'Size'), m('th', m('i.fas.fa-download'))])), v.children, ]), }; diff --git a/webui-src/app/channels/other_channels.js b/webui-src/app/channels/other_channels.js index cc8c5bf6..10a54fd4 100644 --- a/webui-src/app/channels/other_channels.js +++ b/webui-src/app/channels/other_channels.js @@ -12,13 +12,13 @@ const Layout = () => { v.attrs.list.map((channel) => m(util.ChannelSummary, { details: channel, - category: 'OtherChannels', + category: 'Other', }) ), v.attrs.list.map((channel) => m(util.DisplayChannelsFromList, { id: channel.mGroupId, - category: 'OtherChannels', + category: 'Other', }) ), ]) diff --git a/webui-src/app/channels/popular_channels.js b/webui-src/app/channels/popular_channels.js index db58754a..241778a6 100644 --- a/webui-src/app/channels/popular_channels.js +++ b/webui-src/app/channels/popular_channels.js @@ -12,13 +12,13 @@ const Layout = () => { v.attrs.list.map((channel) => m(util.ChannelSummary, { details: channel, - category: 'PopularChannels', + category: 'Popular', }) ), v.attrs.list.map((channel) => m(util.DisplayChannelsFromList, { id: channel.mGroupId, - category: 'PopularChannels', + category: 'Popular', }) ), ]) diff --git a/webui-src/app/channels/subscribed_channels.js b/webui-src/app/channels/subscribed_channels.js index 0a28f01c..dd1ae8f0 100644 --- a/webui-src/app/channels/subscribed_channels.js +++ b/webui-src/app/channels/subscribed_channels.js @@ -12,13 +12,13 @@ const Layout = () => { v.attrs.list.map((channel) => m(util.ChannelSummary, { details: channel, - category: 'SubscribedChannels', + category: 'Subscribed', }) ), v.attrs.list.map((channel) => m(util.DisplayChannelsFromList, { id: channel.mGroupId, - category: 'SubscribedChannels', + category: 'Subscribed', }) ), ]) diff --git a/webui-src/app/chat/chat.js b/webui-src/app/chat/chat.js index ff8e215c..cdd53501 100644 --- a/webui-src/app/chat/chat.js +++ b/webui-src/app/chat/chat.js @@ -7,11 +7,18 @@ const chatEmoji = require('chat/chat_emoji'); const HistoryBrowserModal = require('people/people_history'); const { + get64Num, + loadLobbyDetails, + loadDistantChatDetails, sortLobbies, + getNicknameColor, getStatusColor, getStatusTooltip, + renderTextWithEmoji, getSafeAvatar, + MobileState, ChatRoomsModel, + Message, ChatLobbyModel, ChatHubState, } = chatState; @@ -110,6 +117,54 @@ function scrollChatToBottom() { }, 50); } +function renderUserTooltip(gxsId, name) { + const details = ChatHubState.gxsDetails[gxsId]; + if (!details) return null; + + const avatar = getSafeAvatar(details); + const firstLetter = (name || '?').slice(0, 1).toUpperCase(); + const votes = details.mReputation + ? (details.mReputation.mFriendsPositiveVotes - details.mReputation.mFriendsNegativeVotes) + : 0; + + const rect = ChatHubState.hoveredUser ? ChatHubState.hoveredUser.rect : null; + const tooltipWidth = 280; + const tooltipGap = 10; + let left = rect ? rect.left - tooltipWidth - tooltipGap : window.innerWidth - tooltipWidth - tooltipGap; + if (left < tooltipGap && rect) left = rect.right + tooltipGap; + let top = rect ? rect.top : 100; + if (top + 160 > window.innerHeight) top = window.innerHeight - 170; + if (top < 10) top = 10; + + return m('.user-tooltip', { + style: { + position: 'fixed', + top: `${top}px`, + left: `${left}px`, + zIndex: 10000, + } + }, [ + m('.tooltip-avatar', m(peopleUtil.UserAvatar, { avatar, firstLetter, identityId: gxsId, size: 56, isSquare: true })), + m('.tooltip-details', [ + m('.tooltip-row', [m('span.tooltip-label', 'Identity name: '), m('span.tooltip-value', name)]), + m('.tooltip-row', [m('span.tooltip-label', 'Identity Id: '), m('span.tooltip-value.tooltip-id', gxsId)]), + details.mPgpId && details.mPgpId !== '0000000000000000' && m('.tooltip-row', [ + m('span.tooltip-label', 'Node: '), + m('span.tooltip-value', `${rs.userList.username(details.mPgpId) || name} [${details.mPgpId}]`) + ]), + m('.tooltip-row', [ + m('span.tooltip-label', 'Votes: '), + m('span.tooltip-value', { + style: { + color: votes >= 0 ? '#008000' : '#cc0000', + fontWeight: 'bold' + } + }, (votes >= 0 ? '+' : '') + votes) + ]) + ]) + ]); +} + function pollHashStatus(localpath) { rs.rsJsonApiRequest('/rsFiles/ExtraFileStatus', { localpath }, (data) => { if (data && data.retval && data.info && data.info.hash && data.info.hash !== '0000000000000000000000000000000000000000') { @@ -137,6 +192,75 @@ function pollHashStatus(localpath) { // ************************* views **************************** +const Lobby = () => { + return { + view: (vnode) => { + const { info, tagname, onclick, lobbytagname = 'mainname' } = vnode.attrs; + return m( + ChatLobbyModel.selected(info, '.selected-lobby', tagname), + { + key: rs.idToHex(info.lobby_id), + onclick, + }, + [ + m('h5', { class: lobbytagname }, info.lobby_name === '' ? '' : info.lobby_name), + m('.topic', info.lobby_topic), + ] + ); + }, + }; +}; + +const LobbyList = { + view(vnode) { + const tagname = vnode.attrs.tagname; + const lobbytagname = vnode.attrs.lobbytagname; + const onclick = vnode.attrs.onclick || (() => null); + return [ + vnode.attrs.rooms.map((info) => + m(Lobby, { + info, + tagname, + lobbytagname, + onclick: onclick(info), + }) + ), + ]; + }, +}; + +const SubscribedLobbies = { + view() { + return m('.widget', [ + m('.widget__heading', m('h3', 'Subscribed chat rooms')), + m('.widget__body', [ + m(LobbyList, { + rooms: sortLobbies(Object.values(ChatRoomsModel.subscribedRooms)), + tagname: '.lobby.subscribed', + onclick: ChatLobbyModel.switchToEvent, + }), + ]), + ]); + }, +}; + +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, + }), + ]), + ]); + }, +}; + +// ************************* Chat Hub Sub-Components **************************** + const ChatRoomHeader = () => { return { view: (vnode) => { @@ -335,7 +459,7 @@ const ChatConversationView = () => { }) ]), m('textarea.chat-hub-textarea', { - placeholder: canTalk ? 'Type a message... Press Enter to send (or paste image)' : 'Waiting for tunnel to be secured...', + placeholder: 'Type a message...', disabled: !canTalk, enterkeyhint: 'send', onpaste: (e) => { @@ -565,12 +689,7 @@ const ChatConversationView = () => { onmouseenter: (e) => { if (ChatHubState.activeMenu) return; const rect = e.currentTarget.getBoundingClientRect(); - const rightbar = document.querySelector('.chat-hub-rightbar'); - if (rightbar) { - const parentRect = rightbar.getBoundingClientRect(); - const top = rect.top - parentRect.top + rect.height / 2; - ChatHubState.hoveredUser = { gxsId, name, top }; - } + ChatHubState.hoveredUser = { gxsId, name, rect }; }, onmouseleave: () => { ChatHubState.hoveredUser = null; @@ -641,59 +760,44 @@ const ChatConversationView = () => { }); } return null; - })() + })(), ]); }); })()), - ChatHubState.hoveredUser && (() => { - const hUser = ChatHubState.hoveredUser; - const details = ChatHubState.gxsDetails[hUser.gxsId]; - if (!details) return null; - - const avatar = getSafeAvatar(details); - const firstLetter = (hUser.name || '?').slice(0, 1).toUpperCase(); - const votes = details.mReputation - ? (details.mReputation.mFriendsPositiveVotes - details.mReputation.mFriendsNegativeVotes) - : 0; - - return m('.user-tooltip', { - style: { - top: `${hUser.top}px`, - } - }, [ - m('.tooltip-avatar', m(peopleUtil.UserAvatar, { avatar, firstLetter, identityId: hUser.gxsId, size: 64 })), - m('.tooltip-details', [ - m('.tooltip-row', [m('span.tooltip-label', 'Identity name: '), m('span.tooltip-value', hUser.name)]), - m('.tooltip-row', [m('span.tooltip-label', 'Identity Id: '), m('span.tooltip-value.tooltip-id', hUser.gxsId)]), - details.mPgpId && details.mPgpId !== '0000000000000000' && m('.tooltip-row', [ - m('span.tooltip-label', 'Node: '), - m('span.tooltip-value', `${rs.userList.username(details.mPgpId) || hUser.name} [${details.mPgpId}]`) - ]), - m('.tooltip-row', [ - m('span.tooltip-label', 'Votes: '), - m('span.tooltip-value', { - style: { - color: votes >= 0 ? '#22c55e' : '#ef4444', - fontWeight: 'bold' - } - }, (votes >= 0 ? '+' : '') + votes) - ]) - ]) - ]); - })(), + ChatHubState.hoveredUser && renderUserTooltip(ChatHubState.hoveredUser.gxsId, ChatHubState.hoveredUser.name), ChatHubState.activeMenu && (() => { const menu = ChatHubState.activeMenu; const isOwn = menu.gxsId === rs.idToHex(ChatLobbyModel.currentLobby.gxs_id || ''); const isMuted = ChatHubState.mutedUsers && ChatHubState.mutedUsers.has(menu.gxsId); - return m('.rightbar-context-menu', { - style: { - top: `${menu.top}px`, - }, - onclick: (e) => { - e.stopPropagation(); - } - }, [ + return [ + m('.menu-backdrop', { + style: { + position: 'fixed', + inset: 0, + zIndex: 9998, + }, + onclick: (e) => { + e.preventDefault(); + e.stopPropagation(); + ChatHubState.activeMenu = null; + m.redraw(); + }, + oncontextmenu: (e) => { + e.preventDefault(); + e.stopPropagation(); + ChatHubState.activeMenu = null; + m.redraw(); + }, + }), + m('.rightbar-context-menu', { + style: { + top: `${menu.top}px`, + }, + onclick: (e) => { + e.stopPropagation(); + } + }, [ m('.menu-item', { onclick: () => { ChatHubState.userSortMethod = 'activity'; @@ -850,7 +954,7 @@ const ChatConversationView = () => { m('i.fas.fa-user', { style: 'color: #8b5cf6; margin-right: 0.5rem; width: 18px; text-align: center;' }), 'Show author in people tab' ]) - ]); + ])]; })() ]) ]); @@ -906,6 +1010,8 @@ const ChatRoomDetailView = () => { const room = ChatHubState.selectedRoom; if (!room) return null; + let participantCount = 0; + let participantNames = []; let participants = []; if (room.gxs_ids) { @@ -933,8 +1039,8 @@ const ChatRoomDetailView = () => { } } - const participantCount = participants.length; - const participantNames = participants.map((p) => p.name); + participantCount = participants.length; + participantNames = participants.map((p) => p.name); participantNames.sort((a, b) => a.localeCompare(b)); const lobbyHexId = rs.idToHex(room.lobby_id); @@ -1235,182 +1341,182 @@ const Layout = { m('p.no-rooms', 'No chat rooms found'), ]), ]), - ChatHubState.showCreateRoomModal && m('.attach-modal-overlay', [ - m('.attach-modal', [ - m('h4', 'Create New Chat Room'), - - m('.form-field', { style: 'display: flex; flex-direction: column; gap: 0.25rem;' }, [ - m('label', { style: 'font-weight: bold; font-size: 0.9rem; color: #475569;' }, 'Room Name:'), - m('input[type=text]', { - value: ChatHubState.newRoomName, - oninput: (e) => { ChatHubState.newRoomName = e.target.value; }, - placeholder: 'Enter room name', - style: 'padding: 0.5rem; border: 1px solid #cbd5e1; border-radius: 0.25rem; font-size: 0.9rem;' - }) - ]), - - m('.form-field', { style: 'display: flex; flex-direction: column; gap: 0.25rem; margin-top: 0.5rem;' }, [ - m('label', { style: 'font-weight: bold; font-size: 0.9rem; color: #475569;' }, 'Topic:'), - m('input[type=text]', { - value: ChatHubState.newRoomTopic, - oninput: (e) => { ChatHubState.newRoomTopic = e.target.value; }, - placeholder: 'Enter room topic', - style: 'padding: 0.5rem; border: 1px solid #cbd5e1; border-radius: 0.25rem; font-size: 0.9rem;' - }) - ]), - - m('.form-field', { style: 'display: flex; flex-direction: column; gap: 0.25rem; margin-top: 0.5rem;' }, [ - m('label', { style: 'font-weight: bold; font-size: 0.9rem; color: #475569;' }, 'Admin Identity:'), - m('select', { - value: ChatHubState.newRoomIdentity, - onchange: (e) => { ChatHubState.newRoomIdentity = e.target.value; }, - style: 'padding: 0.5rem; border: 1px solid #cbd5e1; border-radius: 0.25rem; font-size: 0.9rem; background-color: #ffffff;' - }, [ - ChatHubState.ownGxsIdentities && ChatHubState.ownGxsIdentities.map((id) => { - const details = ChatHubState.gxsDetails[id]; - const name = details ? (details.mNickname || details.mGroupName) : id; - return m('option', { value: id }, name); - }) - ]) - ]), + ]), + ChatHubState.showCreateRoomModal && m('.attach-modal-overlay', [ + m('.attach-modal', [ + m('h4', 'Create New Chat Room'), + + m('.form-field', { style: 'display: flex; flex-direction: column; gap: 0.25rem;' }, [ + m('label', { style: 'font-weight: bold; font-size: 0.9rem; color: #475569;' }, 'Room Name:'), + m('input[type=text]', { + value: ChatHubState.newRoomName, + oninput: (e) => { ChatHubState.newRoomName = e.target.value; }, + placeholder: 'Enter room name', + style: 'padding: 0.5rem; border: 1px solid #cbd5e1; border-radius: 0.25rem; font-size: 0.9rem;' + }) + ]), - m('.form-field', { style: 'display: flex; gap: 0.5rem; align-items: center; margin-top: 0.75rem;' }, [ - m('label', { style: 'display: inline-flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; color: #475569; cursor: pointer; user-select: none;' }, [ - m('input[type=checkbox]', { - checked: ChatHubState.newRoomPublic, - onclick: (e) => { ChatHubState.newRoomPublic = e.target.checked; } - }), - 'Public Room' - ]) - ]), + m('.form-field', { style: 'display: flex; flex-direction: column; gap: 0.25rem; margin-top: 0.5rem;' }, [ + m('label', { style: 'font-weight: bold; font-size: 0.9rem; color: #475569;' }, 'Topic:'), + m('input[type=text]', { + value: ChatHubState.newRoomTopic, + oninput: (e) => { ChatHubState.newRoomTopic = e.target.value; }, + placeholder: 'Enter room topic', + style: 'padding: 0.5rem; border: 1px solid #cbd5e1; border-radius: 0.25rem; font-size: 0.9rem;' + }) + ]), - m('.form-field', { style: 'display: flex; gap: 0.5rem; align-items: center; margin-top: 0.5rem;' }, [ - m('label', { style: 'display: inline-flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; color: #475569; cursor: pointer; user-select: none;' }, [ - m('input[type=checkbox]', { - checked: ChatHubState.newRoomSigned, - onclick: (e) => { ChatHubState.newRoomSigned = e.target.checked; } - }), - 'PGP signed identities' - ]) - ]), + m('.form-field', { style: 'display: flex; flex-direction: column; gap: 0.25rem; margin-top: 0.5rem;' }, [ + m('label', { style: 'font-weight: bold; font-size: 0.9rem; color: #475569;' }, 'Admin Identity:'), + m('select', { + value: ChatHubState.newRoomIdentity, + onchange: (e) => { ChatHubState.newRoomIdentity = e.target.value; }, + style: 'padding: 0.5rem; border: 1px solid #cbd5e1; border-radius: 0.25rem; font-size: 0.9rem; background-color: #ffffff;' + }, [ + ChatHubState.ownGxsIdentities && ChatHubState.ownGxsIdentities.map((id) => { + const details = ChatHubState.gxsDetails[id]; + const name = details ? (details.mNickname || details.mGroupName) : id; + return m('option', { value: id }, name); + }) + ]) + ]), - ChatHubState.createRoomError && m('p.error-text', { style: 'color: #ef4444; font-size: 0.85rem; margin: 0.5rem 0 0 0;' }, ChatHubState.createRoomError), + m('.form-field', { style: 'display: flex; gap: 0.5rem; align-items: center; margin-top: 0.75rem;' }, [ + m('label', { style: 'display: inline-flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; color: #475569; cursor: pointer; user-select: none;' }, [ + m('input[type=checkbox]', { + checked: ChatHubState.newRoomPublic, + onclick: (e) => { ChatHubState.newRoomPublic = e.target.checked; } + }), + 'Public Room' + ]) + ]), - m('.modal-buttons', { style: 'display: flex; justify-content: flex-end; gap: 0.75rem; margin-top: 1rem;' }, [ - m('button', { - disabled: !ChatHubState.newRoomName.trim() || !ChatHubState.newRoomIdentity, - onclick: () => { - const name = ChatHubState.newRoomName.trim(); - const topic = ChatHubState.newRoomTopic.trim(); - const identity = ChatHubState.newRoomIdentity; - const isPublic = ChatHubState.newRoomPublic; - const isSigned = ChatHubState.newRoomSigned; - let flags = 0; - if (isPublic) flags |= 4; - if (isSigned) flags |= 8; - - rs.rsJsonApiRequest('/rsChats/createChatLobby', { - lobby_name: name, - lobby_identity: identity, - lobby_topic: topic, - invited_friends: [], - lobby_privacy_type: flags - }, (data, success) => { - if (success) { - ChatHubState.showCreateRoomModal = false; - ChatHubState.newRoomName = ''; - ChatHubState.newRoomTopic = ''; - ChatHubState.newRoomSigned = false; - ChatHubState.createRoomError = ''; - ChatRoomsModel.loadSubscribedRooms(); - m.redraw(); - } else { - ChatHubState.createRoomError = 'Failed to create room. Check parameters.'; - m.redraw(); - } - }); - } - }, 'Create'), - m('button.red', { - onclick: () => { - ChatHubState.showCreateRoomModal = false; - ChatHubState.newRoomName = ''; - ChatHubState.newRoomTopic = ''; - ChatHubState.newRoomSigned = false; - ChatHubState.createRoomError = ''; - } - }, 'Cancel') + m('.form-field', { style: 'display: flex; gap: 0.5rem; align-items: center; margin-top: 0.5rem;' }, [ + m('label', { style: 'display: inline-flex; align-items: center; gap: 0.5rem; font-size: 0.9rem; color: #475569; cursor: pointer; user-select: none;' }, [ + m('input[type=checkbox]', { + checked: ChatHubState.newRoomSigned, + onclick: (e) => { ChatHubState.newRoomSigned = e.target.checked; } + }), + 'PGP signed identities' ]) + ]), + + ChatHubState.createRoomError && m('p.error-text', { style: 'color: #ef4444; font-size: 0.85rem; margin: 0.5rem 0 0 0;' }, ChatHubState.createRoomError), + + m('.modal-buttons', { style: 'display: flex; justify-content: flex-end; gap: 0.75rem; margin-top: 1rem;' }, [ + m('button', { + disabled: !ChatHubState.newRoomName.trim() || !ChatHubState.newRoomIdentity, + onclick: () => { + const name = ChatHubState.newRoomName.trim(); + const topic = ChatHubState.newRoomTopic.trim(); + const identity = ChatHubState.newRoomIdentity; + const isPublic = ChatHubState.newRoomPublic; + const isSigned = ChatHubState.newRoomSigned; + let flags = 0; + if (isPublic) flags |= 4; + if (isSigned) flags |= 8; + + rs.rsJsonApiRequest('/rsChats/createChatLobby', { + lobby_name: name, + lobby_identity: identity, + lobby_topic: topic, + invited_friends: [], + lobby_privacy_type: flags + }, (data, success) => { + if (success) { + ChatHubState.showCreateRoomModal = false; + ChatHubState.newRoomName = ''; + ChatHubState.newRoomTopic = ''; + ChatHubState.newRoomSigned = false; + ChatHubState.createRoomError = ''; + ChatRoomsModel.loadSubscribedRooms(); + m.redraw(); + } else { + ChatHubState.createRoomError = 'Failed to create room. Check parameters.'; + m.redraw(); + } + }); + } + }, 'Create'), + m('button.red', { + onclick: () => { + ChatHubState.showCreateRoomModal = false; + ChatHubState.newRoomName = ''; + ChatHubState.newRoomTopic = ''; + ChatHubState.newRoomSigned = false; + ChatHubState.createRoomError = ''; + } + }, 'Cancel') ]) - ]), - ChatHubState.showInviteModal && m('.attach-modal-overlay', [ - m('.attach-modal', { style: 'max-width: 450px;' }, [ - m('h4', 'Invite Friends to ' + (ChatHubState.selectedRoom ? ChatHubState.selectedRoom.lobby_name : '')), - m('.friends-invite-list', { style: 'max-height: 250px; overflow-y: auto; margin-top: 1rem; border: 1px solid #e2e8f0; border-radius: 0.375rem; padding: 0.5rem;' }, [ - ChatHubState.friendsList.length === 0 - ? m('p', { style: 'text-align: center; color: #64748b; font-style: italic; margin: 1rem 0;' }, 'No friends available') - : ChatHubState.friendsList.map((friend) => { - const isChecked = ChatHubState.selectedFriendsToInvite.has(friend.id); - return m('.friend-invite-item', { - style: 'display: flex; align-items: center; justify-content: space-between; padding: 0.5rem; border-bottom: 1px solid #f1f5f9; cursor: pointer;', - onclick: () => { - if (isChecked) { - ChatHubState.selectedFriendsToInvite.delete(friend.id); - } else { + ]) + ]), + ChatHubState.showInviteModal && m('.attach-modal-overlay', [ + m('.attach-modal', { style: 'max-width: 450px;' }, [ + m('h4', 'Invite Friends to ' + (ChatHubState.selectedRoom ? ChatHubState.selectedRoom.lobby_name : '')), + m('.friends-invite-list', { style: 'max-height: 250px; overflow-y: auto; margin-top: 1rem; border: 1px solid #e2e8f0; border-radius: 0.375rem; padding: 0.5rem;' }, [ + ChatHubState.friendsList.length === 0 + ? m('p', { style: 'text-align: center; color: #64748b; font-style: italic; margin: 1rem 0;' }, 'No friends available') + : ChatHubState.friendsList.map((friend) => { + const isChecked = ChatHubState.selectedFriendsToInvite.has(friend.id); + return m('.friend-invite-item', { + style: 'display: flex; align-items: center; justify-content: space-between; padding: 0.5rem; border-bottom: 1px solid #f1f5f9; cursor: pointer;', + onclick: () => { + if (isChecked) { + ChatHubState.selectedFriendsToInvite.delete(friend.id); + } else { + ChatHubState.selectedFriendsToInvite.add(friend.id); + } + } + }, [ + m('div', { style: 'display: flex; align-items: center; gap: 0.5rem;' }, [ + m('.status-bullet', { style: { backgroundColor: friend.online ? '#22c55e' : '#94a3b8', width: '8px', height: '8px', borderRadius: '50%', display: 'inline-block' } }), + m('span', { style: 'font-weight: 500;' }, friend.name) + ]), + m('input[type=checkbox]', { + checked: isChecked, + onclick: (e) => { + e.stopPropagation(); + if (e.target.checked) { ChatHubState.selectedFriendsToInvite.add(friend.id); + } else { + ChatHubState.selectedFriendsToInvite.delete(friend.id); } } - }, [ - m('div', { style: 'display: flex; align-items: center; gap: 0.5rem;' }, [ - m('.status-bullet', { style: { backgroundColor: friend.online ? '#22c55e' : '#94a3b8', width: '8px', height: '8px', borderRadius: '50%', display: 'inline-block' } }), - m('span', { style: 'font-weight: 500;' }, friend.name) - ]), - m('input[type=checkbox]', { - checked: isChecked, - onclick: (e) => { - e.stopPropagation(); - if (e.target.checked) { - ChatHubState.selectedFriendsToInvite.add(friend.id); - } else { - ChatHubState.selectedFriendsToInvite.delete(friend.id); - } - } - }) - ]); - }) - ]), - m('.modal-buttons', { style: 'display: flex; justify-content: flex-end; gap: 0.75rem; margin-top: 1.5rem;' }, [ - m('button', { - disabled: ChatHubState.selectedFriendsToInvite.size === 0, - onclick: () => { - const lobbyHexId = rs.idToHex(ChatHubState.selectedRoom.lobby_id); - const invitePromises = []; - ChatHubState.selectedFriendsToInvite.forEach((friendId) => { - invitePromises.push( - new Promise((resolve) => { - rs.rsJsonApiRequest('/rsChats/invitePeerToLobby', { - lobby_id: lobbyHexId, - peer_id: friendId - }, () => resolve()); - }) - ); - }); - Promise.all(invitePromises).then(() => { - ChatHubState.showInviteModal = false; - ChatHubState.selectedFriendsToInvite.clear(); - m.redraw(); - }); - } - }, 'Invite'), - m('button.red', { - onclick: () => { + }) + ]); + }) + ]), + m('.modal-buttons', { style: 'display: flex; justify-content: flex-end; gap: 0.75rem; margin-top: 1rem;' }, [ + m('button.blue', { + disabled: ChatHubState.selectedFriendsToInvite.size === 0, + onclick: () => { + const lobbyHexId = rs.idToHex(ChatHubState.selectedRoom.lobby_id); + const invitePromises = []; + ChatHubState.selectedFriendsToInvite.forEach((friendId) => { + invitePromises.push( + new Promise((resolve) => { + rs.rsJsonApiRequest('/rsChats/invitePeerToLobby', { + lobby_id: lobbyHexId, + peer_id: friendId + }, () => resolve()); + }) + ); + }); + Promise.all(invitePromises).then(() => { ChatHubState.showInviteModal = false; ChatHubState.selectedFriendsToInvite.clear(); - } - }, 'Cancel') - ]) + m.redraw(); + }); + } + }, 'Invite'), + m('button.red', { + onclick: () => { + ChatHubState.showInviteModal = false; + ChatHubState.selectedFriendsToInvite.clear(); + } + }, 'Cancel') ]) - ]), + ]) ]), m('.chat-hub-right-pane', [ @@ -1463,7 +1569,10 @@ const Layout = { ]), ]), ChatHubState.messageContextMenu.show && m('.chat-msg-context-menu', { - style: `position: fixed; top: ${ChatHubState.messageContextMenu.y}px; left: ${ChatHubState.messageContextMenu.x}px; background: #ffffff; border: 1px solid #cbd5e1; border-radius: 0.5rem; box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05); padding: 0.35rem 0; z-index: 3000; min-width: 160px;`, + style: { + top: `${Math.max(8, Math.min(ChatHubState.messageContextMenu.y, window.innerHeight - 132))}px`, + left: `${Math.max(8, Math.min(ChatHubState.messageContextMenu.x, window.innerWidth - 228))}px`, + }, onclick: (e) => e.stopPropagation(), }, [ m('.context-menu-item', { @@ -1523,6 +1632,74 @@ const Layout = { }, }; +const LayoutSingle = () => { + const onResize = () => { + const element = document.querySelector('.messages'); + if (element) element.scrollTop = element.scrollHeight; + }; + return { + 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; + const isRoom = chatType === 3; + 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() }), + m( + '.messages' + (isRoom ? '.compact-container' : ''), + { onclick: () => MobileState.closeAll() }, + ChatLobbyModel.messages + ), + 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') + ), + ] + ), + ] + ); + }, + }; +}; + /* /rsChats/initiateDistantChatConnexion * @param[in] to_pid RsGxsId to start the connection diff --git a/webui-src/app/chat/chat_state.js b/webui-src/app/chat/chat_state.js index 2760a633..f7422210 100644 --- a/webui-src/app/chat/chat_state.js +++ b/webui-src/app/chat/chat_state.js @@ -89,6 +89,27 @@ function getStatusTooltip(status) { } } +// Chat messages travel as HTML. Stripping the tags is not enough: the entities +// they leave behind are still raw text and end up displayed verbatim, the most +// visible one being the   that Qt emits for leading and repeated spaces. +// A textarea decodes them without ever parsing markup, since its content model +// is plain text and nothing in the string can become an element. +function decodeHtmlEntities(text) { + const el = document.createElement('textarea'); + el.innerHTML = text; + return el.value; +} + +// Turn the HTML payload of a chat message into the text we display. +function htmlToText(text) { + return decodeHtmlEntities( + text + .replaceAll('
', '\n') + .replaceAll('
', '\n') + .replace(new RegExp('|<[^>]*>', 'gm'), '') + ); +} + function renderChatMessage(rawText) { if (!rawText) return ''; @@ -103,10 +124,7 @@ function renderChatMessage(rawText) { while ((match = imgRegex.exec(rawText)) !== null) { if (match.index > lastIndex) { const precedingText = rawText.substring(lastIndex, match.index); - const cleanText = precedingText - .replaceAll('
', '\n') - .replaceAll('
', '\n') - .replace(new RegExp('|<[^>]*>', 'gm'), ''); + const cleanText = htmlToText(precedingText); if (cleanText) { parts.push(renderTextWithEmoji(cleanText)); } @@ -142,10 +160,7 @@ function renderChatMessage(rawText) { if (lastIndex < rawText.length) { const trailingText = rawText.substring(lastIndex); - const cleanText = trailingText - .replaceAll('
', '\n') - .replaceAll('
', '\n') - .replace(new RegExp('|<[^>]*>', 'gm'), ''); + const cleanText = htmlToText(trailingText); if (cleanText) { parts.push(renderFormattedMessageText(cleanText)); } @@ -179,12 +194,11 @@ function renderChatMessage(rawText) { } // 3. Normal text message - const cleanText = rawText - .replace(/]*>/gi, '\n> ') - .replace(/<\/blockquote>/gi, '\n') - .replaceAll('
', '\n') - .replaceAll('
', '\n') - .replace(new RegExp('|<[^>]*>', 'gm'), ''); + const cleanText = htmlToText( + rawText + .replace(/]*>/gi, '\n> ') + .replace(/<\/blockquote>/gi, '\n') + ); return renderFormattedMessageText(cleanText); } diff --git a/webui-src/app/config/config_resolver.js b/webui-src/app/config/config_resolver.js index dd9f82ed..99386fd9 100644 --- a/webui-src/app/config/config_resolver.js +++ b/webui-src/app/config/config_resolver.js @@ -16,6 +16,7 @@ const Layout = { m(widget.Sidebar, { tabs: Object.keys(sections), baseRoute: '/config/', + mobileDrawer: true, }), m('.node-panel', vnode.children), ], diff --git a/webui-src/app/feedreader/feedreader.js b/webui-src/app/feedreader/feedreader.js new file mode 100644 index 00000000..b1baa017 --- /dev/null +++ b/webui-src/app/feedreader/feedreader.js @@ -0,0 +1,140 @@ +const m = require('mithril'); +const rs = require('rswebui'); +const api = (method, body = {}) => rs.rsJsonApiRequest(`/rsFeedReader/${method}`, body); + +function safeHtml(html) { + const doc = new DOMParser().parseFromString(html || '', 'text/html'); + doc.querySelectorAll('script,iframe,object,embed,form,style').forEach((node) => node.remove()); + doc.querySelectorAll('*').forEach((node) => [...node.attributes].forEach((attr) => { + if (attr.name.toLowerCase().startsWith('on')) node.removeAttribute(attr.name); + if ((attr.name === 'href' || attr.name === 'src') && /^javascript:/i.test(attr.value)) node.removeAttribute(attr.name); + })); + return doc.body.innerHTML; +} + +module.exports = () => { + let tree = [], selectedFeed = null, selectedMessage = null, messages = []; + let loading = false, error = '', showAdd = false, addType = 'feed'; + let form = { name: '', url: '', parentId: 0 }; + + async function loadBranch(parentId = 0) { + const res = await api('getFeeds', { parentId }); + if (res.status !== 200) throw new Error(res.body.error || 'Unable to load feeds'); + return Promise.all(res.body.feeds.map(async (feed) => ({ + ...feed, children: feed.folder ? await loadBranch(feed.feedId) : [], + }))); + } + + async function loadTree() { + loading = true; error = ''; + try { tree = await loadBranch(); } catch (e) { error = e.message; } + loading = false; m.redraw(); + } + + async function selectFeed(feed) { + selectedFeed = feed; selectedMessage = null; messages = []; error = ''; + const res = await api('getMessages', { feedId: feed.feedId }); + if (res.status === 200) messages = res.body.messages.sort((a, b) => b.pubDate - a.pubDate); + else error = res.body.error || 'Unable to load articles'; + m.redraw(); + } + + async function openMessage(message) { + selectedMessage = message; + if (!message.read) { + await api('setMessageRead', { feedId: message.feedId, msgId: message.msgId, read: true }); + message.read = true; + } + m.redraw(); + } + + async function addItem() { + const res = await api(addType === 'folder' ? 'addFolder' : 'addFeed', form); + if (res.status === 200) { + showAdd = false; form = { name: '', url: '', parentId: 0 }; await loadTree(); + } else error = res.body.error || 'Unable to add item'; + } + + async function removeFeed(feed) { + if (!window.confirm(`Remove “${feed.name || feed.url}”?`)) return; + const res = await api('removeFeed', { feedId: feed.feedId }); + if (res.status === 200) { + if (selectedFeed && selectedFeed.feedId === feed.feedId) { + selectedFeed = null; selectedMessage = null; messages = []; + } + await loadTree(); + } + } + + function renderTree(items, depth = 0) { + return items.map((item) => [ + m('.feedreader-tree-item', { + class: selectedFeed && selectedFeed.feedId === item.feedId ? 'selected' : '', + style: { paddingLeft: `${12 + depth * 16}px` }, + onclick: () => !item.folder && selectFeed(item), + }, [ + m('i', { class: item.folder ? 'fas fa-folder' : 'fas fa-rss' }), + m('span', item.name || item.url || 'Untitled'), + m('button.feedreader-icon-button', { + title: 'Remove', onclick: (event) => { event.stopPropagation(); removeFeed(item); }, + }, m('i.fas.fa-trash')), + ]), + item.children && renderTree(item.children, depth + 1), + ]); + } + + return { + oninit: loadTree, + view: () => m('.feedreader-page', [ + m('.feedreader-toolbar', [ + m('h2', [m('i.fas.fa-rss'), ' FeedReader']), + m('button', { onclick: () => { addType = 'feed'; showAdd = true; } }, 'Add feed'), + m('button', { onclick: () => { addType = 'folder'; showAdd = true; } }, 'Add folder'), + m('button', { onclick: loadTree, title: 'Reload feed tree' }, m('i.fas.fa-sync-alt')), + ]), + error && m('.feedreader-error', error), + showAdd && m('.feedreader-add', [ + m('h3', addType === 'folder' ? 'Add folder' : 'Add feed'), + m('input', { placeholder: 'Name', value: form.name, oninput: (e) => (form.name = e.target.value) }), + addType === 'feed' && m('input', { placeholder: 'https://example.org/feed.xml', value: form.url, oninput: (e) => (form.url = e.target.value) }), + m('button', { onclick: addItem }, 'Save'), + m('button', { onclick: () => (showAdd = false) }, 'Cancel'), + ]), + m('.feedreader-columns', [ + m('aside.feedreader-tree', loading ? m('p', 'Loading…') : renderTree(tree)), + m('section.feedreader-messages', selectedFeed ? [ + m('.feedreader-section-title', [ + m('h3', selectedFeed.name || selectedFeed.url), + m('button', { onclick: async () => { + await api('refreshFeed', { feedId: selectedFeed.feedId }); await selectFeed(selectedFeed); + } }, [m('i.fas.fa-sync-alt'), ' Refresh']), + ]), + messages.length ? messages.map((message) => m('.feedreader-message-row', { + class: `${message.read ? 'read' : 'unread'} ${selectedMessage === message ? 'selected' : ''}`, + onclick: () => openMessage(message), + }, [ + m('strong', message.title || '(Untitled article)'), m('span', message.author), + m('time', message.pubDate ? new Date(message.pubDate * 1000).toLocaleString() : ''), + ])) : m('p.feedreader-placeholder', 'No articles in this feed.'), + ] : m('p.feedreader-placeholder', 'Select a feed to read its articles.')), + m('article.feedreader-reader', selectedMessage ? [ + m('h2', selectedMessage.title || '(Untitled article)'), + m('.feedreader-article-meta', [selectedMessage.author, selectedMessage.pubDate ? new Date(selectedMessage.pubDate * 1000).toLocaleString() : ''].filter(Boolean).join(' · ')), + selectedMessage.link && m('a', { href: selectedMessage.link, target: '_blank', rel: 'noopener noreferrer' }, 'Open original article'), + m('.feedreader-article-body', { innerHTML: safeHtml(selectedMessage.descriptionTransformed || selectedMessage.description) }), + m('.feedreader-article-actions', [ + m('button', { onclick: async () => { + selectedMessage.read = !selectedMessage.read; + await api('setMessageRead', { feedId: selectedMessage.feedId, msgId: selectedMessage.msgId, read: selectedMessage.read }); + } }, selectedMessage.read ? 'Mark unread' : 'Mark read'), + m('button', { onclick: async () => { + if (!window.confirm('Delete this article?')) return; + await api('removeMessage', { feedId: selectedMessage.feedId, msgId: selectedMessage.msgId }); + await selectFeed(selectedFeed); + } }, 'Delete'), + ]), + ] : m('p.feedreader-placeholder', 'Select an article to view it.')), + ]), + ]), + }; +}; diff --git a/webui-src/app/files/files_resolver.js b/webui-src/app/files/files_resolver.js index 2c985be7..a5486cec 100644 --- a/webui-src/app/files/files_resolver.js +++ b/webui-src/app/files/files_resolver.js @@ -35,6 +35,7 @@ const Layout = { m(widget.Sidebar, { tabs: Object.keys(sections), baseRoute: '/files/', + mobileDrawer: true, }), m('.node-panel', m('.widget', vnode.children)), ], diff --git a/webui-src/app/files/files_search.js b/webui-src/app/files/files_search.js index 9e62247c..5cb020ac 100644 --- a/webui-src/app/files/files_search.js +++ b/webui-src/app/files/files_search.js @@ -21,8 +21,13 @@ function handleSubmit() { const SearchBar = () => { return { view: () => - m('form.search-form', { onsubmit: handleSubmit }, [ - m('input[type=text][placeholder=search keyword]', { + m('form.search-form', { + onsubmit: (event) => { + event.preventDefault(); + handleSubmit(); + }, + }, [ + m('input[type=text][placeholder=Search files]', { value: matchString, oninput: (e) => (matchString = e.target.value), }), @@ -144,9 +149,16 @@ const Layout = () => { 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.name-col', { 'data-label': 'Name' }, [ + m(getFileIcon(item.fName)), + m('span', item.fName), + ]), + m( + '.results-cell.size-col', + { 'data-label': 'Size' }, + rs.formatBytes((item.fSize && (item.fSize.xint64 || item.fSize.xstr64)) || 0) + ), + m('.results-cell.hash-col', { 'data-label': 'Hash' }, item.fHash), m( '.results-cell.action-col', m( diff --git a/webui-src/app/files/friends_files.js b/webui-src/app/files/friends_files.js index 3eb9af25..c617b69a 100644 --- a/webui-src/app/files/friends_files.js +++ b/webui-src/app/files/friends_files.js @@ -25,7 +25,7 @@ function displayfiles() { haveFile = res.body.retval; } } - if (v.attrs.replyDepth === 1 && parStruct) { + if (v.attrs.replyDepth === 0 && parStruct) { isId = true; const res = await rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { sslId: parStruct.details.name, @@ -37,25 +37,29 @@ function displayfiles() { }, view: (v) => [ m('tr', [ - parStruct && Object.keys(parStruct.details.children).length + parStruct && parStruct.details.children && Object.keys(parStruct.details.children).length ? m( 'td', m('i.fas.fa-angle-right', { class: 'fa-rotate-' + (parStruct.showChild ? '90' : '0'), style: 'margin-top:12px', - onclick: () => { + onclick: async () => { if (!loaded) { - // if it is not already retrieved. - parStruct.details.children.map(async (child) => { - const res = await rs.rsJsonApiRequest('/rsfiles/requestDirDetails', { - handle: child.handle.xint64, - flags: util.RS_FILE_HINTS_REMOTE, - }); - childrenList.push(res.body.details); - loaded = true; - }); + // Retrieve the directory entries before displaying the nested rows. + const entries = await Promise.all( + parStruct.details.children.map(async (child) => { + const res = await rs.rsJsonApiRequest('/rsfiles/requestDirDetails', { + handle: child.handle.xint64, + flags: util.RS_FILE_HINTS_REMOTE, + }); + return res.body.details; + }) + ); + childrenList.push(...entries); + loaded = true; } parStruct.showChild = !parStruct.showChild; + m.redraw(); }, }) ) @@ -69,9 +73,25 @@ function displayfiles() { left: `calc(30px*${v.attrs.replyDepth})`, }, }, - isId - ? nameOfId + ' (' + parStruct.details.name.slice(0, 8) + '...)' - : parStruct.details.name + [ + m('i.fas', { + class: isId + ? 'fa-user-friends friends-files__friend-icon' + : !isFile + ? parStruct.showChild + ? 'fa-folder-open friends-files__folder-icon' + : 'fa-folder friends-files__folder-icon' + : 'fa-file friends-files__file-icon', + title: isId ? 'Friend' : isFile ? 'File' : 'Folder', + style: 'margin-right:0.45rem', + }), + isId + ? (nameOfId || parStruct.details.name) + + ' (' + + parStruct.details.name.slice(0, 8) + + '...)' + : parStruct.details.name, + ] ), m('td', rs.formatBytes(parStruct.details.size.xint64)), isFile && @@ -142,13 +162,31 @@ function displayfiles() { } const Layout = () => { - // let root_handle; - let parent; + let directories = []; return { - oninit: () => { - rs.rsJsonApiRequest('/rsfiles/requestDirDetails', { + oninit: async () => { + const res = await rs.rsJsonApiRequest('/rsfiles/requestDirDetails', { flags: util.RS_FILE_HINTS_REMOTE, - }).then((res) => (parent = res)); + }); + const root = res.body.details; + + // The remote API returns a synthetic "root" directory. It is not a + // friend and only adds an unnecessary level to this view, so begin at + // its children instead. + if (root && root.name === 'root' && root.children) { + directories = await Promise.all( + root.children.map(async (child) => { + const childRes = await rs.rsJsonApiRequest('/rsfiles/requestDirDetails', { + handle: child.handle.xint64, + flags: util.RS_FILE_HINTS_REMOTE, + }); + return childRes.body.details; + }) + ); + } else if (root) { + directories = [root]; + } + m.redraw(); }, view: () => [ m('.widget__heading', [m('h3', 'Friends Files')]), @@ -157,11 +195,12 @@ const Layout = () => { util.FriendsFilesTable, m( 'tbody', - parent && // root + directories.map((directory) => m(displayfiles, { - par_directory: { details: parent.body.details, showChild: false }, + par_directory: { details: directory, showChild: false }, replyDepth: 0, }) + ) ) ), ]), diff --git a/webui-src/app/files/my_files.js b/webui-src/app/files/my_files.js index 2cc332bb..ce139d8a 100644 --- a/webui-src/app/files/my_files.js +++ b/webui-src/app/files/my_files.js @@ -61,7 +61,16 @@ const DisplayFiles = () => { left: `calc(1.5rem*${v.attrs.replyDepth})`, }, }, - translateName(parStruct.details.name || '') + [ + parStruct.details.children !== undefined + ? m('i.fas', { + class: parStruct.showChild ? 'fa-folder-open' : 'fa-folder', + title: 'Folder', + style: 'margin-right: 0.45rem; color: #d69e2e;', + }) + : null, + translateName(parStruct.details.name || ''), + ] ), m('td', rs.formatBytes((parStruct.details.size && parStruct.details.size.xint64) || 0)), ]), @@ -108,7 +117,15 @@ const Layout = () => { view: () => [ m('.widget__heading', [ m('h3', 'My Files'), - m('button', { onclick: () => (showShareManager = true) }, 'Configure shared directories'), + m( + 'button.my-files__configure-shares', + { + onclick: () => (showShareManager = true), + title: 'Configure shared directories', + 'aria-label': 'Configure shared directories', + }, + [m('i.fas.fa-folder-plus'), m('span', 'Configure shared directories')] + ), ]), m('.widget__body', [ m( diff --git a/webui-src/app/forums/forums.js b/webui-src/app/forums/forums.js index 587692f5..cb2dcef5 100644 --- a/webui-src/app/forums/forums.js +++ b/webui-src/app/forums/forums.js @@ -7,15 +7,15 @@ const peopleUtil = require('people/people_util'); const getForums = { All: [], - PopularForums: [], - SubscribedForums: [], + Popular: [], + Subscribed: [], MyForums: [], async load() { const res = await rs.rsJsonApiRequest('/rsgxsforums/getForumsSummaries'); if (res && res.body && res.body.forums) { getForums.All = res.body.forums; - getForums.PopularForums = getForums.All; - getForums.SubscribedForums = getForums.All.filter( + getForums.Popular = getForums.All; + getForums.Subscribed = getForums.All.filter( (forum) => forum.mSubscribeFlags === util.GROUP_SUBSCRIBE_SUBSCRIBED || forum.mSubscribeFlags === util.GROUP_MY_FORUM @@ -28,9 +28,9 @@ const getForums = { }; const sections = { MyForums: require('forums/my_forums'), - SubscribedForums: require('forums/subscribed_forums'), - PopularForums: require('forums/popular_forums'), - OtherForums: require('forums/other_forums'), + Subscribed: require('forums/subscribed_forums'), + Popular: require('forums/popular_forums'), + Other: require('forums/other_forums'), }; const Layout = () => { @@ -94,6 +94,7 @@ module.exports = { m(widget.Sidebar, { tabs: Object.keys(sections), baseRoute: '/forums/', + mobileDrawer: true, }), m('.node-panel', m(Layout, { pathInfo: vnode.attrs })), ]; diff --git a/webui-src/app/forums/popular_forums.js b/webui-src/app/forums/popular_forums.js index f3cc5919..4ce0eb43 100644 --- a/webui-src/app/forums/popular_forums.js +++ b/webui-src/app/forums/popular_forums.js @@ -12,13 +12,13 @@ const Layout = () => { v.attrs.list.map((forum) => m(util.ForumSummary, { details: forum, - category: 'PopularForums', + category: 'Popular', }) ), v.attrs.list.map((forum) => m(util.DisplayForumsFromList, { id: forum.mGroupId, - category: 'PopularForums', + category: 'Popular', }) ), ]) diff --git a/webui-src/app/forums/subscribed_forums.js b/webui-src/app/forums/subscribed_forums.js index 94119249..c3660c3b 100644 --- a/webui-src/app/forums/subscribed_forums.js +++ b/webui-src/app/forums/subscribed_forums.js @@ -12,13 +12,13 @@ const Layout = () => { v.attrs.list.map((forum) => m(util.ForumSummary, { details: forum, - category: 'SubscribedForums', + category: 'Subscribed', }) ), v.attrs.list.map((forum) => m(util.DisplayForumsFromList, { id: forum.mGroupId, - category: 'SubscribedForums', + category: 'Subscribed', }) ), ]) diff --git a/webui-src/app/mail/mail_resolver.js b/webui-src/app/mail/mail_resolver.js index c06b5fca..5c1c9859 100644 --- a/webui-src/app/mail/mail_resolver.js +++ b/webui-src/app/mail/mail_resolver.js @@ -33,7 +33,11 @@ const Messages = { (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 + (msg) => + (msg.msgflags & util.RS_MSG_BOXMASK) === util.RS_MSG_DRAFTBOX || + (msg.msgflags & 0x05) === 0x05 || + (msg.msgflags & 0x04) !== 0 || + (msg.msgflags & 0x08) !== 0 ); Messages.trash = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_TRASH); Messages.starred = Messages.all.filter((msg) => msg.msgflags & util.RS_MSG_STAR); @@ -68,12 +72,12 @@ const sections = { drafts: require('mail/mail_draftbox'), sent: require('mail/mail_sentbox'), trash: require('mail/mail_trashbox'), -}; -const sectionsquickview = { starred: require('mail/mail_starred'), system: require('mail/mail_system'), spam: require('mail/mail_spam'), attachment: require('mail/mail_attachment'), +}; +const sectionsquickview = { important: require('mail/mail_important'), work: require('mail/mail_work'), todo: require('mail/mail_todo'), @@ -81,11 +85,18 @@ const sectionsquickview = { personal: require('mail/mail_personal'), }; const tagselect = { - showval: 'Tags', - opts: ['Tags', 'Important', 'Work', 'Personal'], + opts: [ + { label: '🏷️ Filter by Tag...', val: '' }, + { label: '🔴 Important', val: 'important' }, + { label: '🟠 Work', val: 'work' }, + { label: '🟢 Personal', val: 'personal' }, + { label: '🔵 Todo', val: 'todo' }, + { label: '🟣 Later', val: 'later' }, + ], }; const Layout = () => { let showCompose = false; + let mobileNavOpen = false; // setFunction like react to show/hide popup function setShowCompose(bool) { showCompose = bool; @@ -99,26 +110,41 @@ const Layout = () => { 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, + }; + const sectionsQuickviewSize = { important: (Messages.important || []).length, work: (Messages.work || []).length, todo: (Messages.todo || []).length, later: (Messages.later || []).length, personal: (Messages.personal || []).length, }; + const activeTab = m.route.param().tab; + const activeBox = tabConfig[activeTab]; + const activeBoxIcons = { + inbox: 'fa-inbox', outbox: 'fa-envelope-open-text', drafts: 'fa-edit', sent: 'fa-envelope-open', + trash: 'fa-trash-alt', starred: 'fa-star', system: 'fa-bell', spam: 'fa-fire', attachment: 'fa-paperclip', + important: 'fa-square', work: 'fa-square', todo: 'fa-square', later: 'fa-square', personal: 'fa-square', + }; return [ m('.side-bar', [ + m('button.mail-mobile-nav-toggle[type=button][aria-label=Open mail navigation]', { + 'aria-expanded': mobileNavOpen, + onclick: () => { mobileNavOpen = !mobileNavOpen; }, + }, m('i.fas.fa-bars')), + m('.mail-nav-drawer', { class: mobileNavOpen ? 'mail-nav-drawer--open' : '' }, [ m( 'button.mail-compose-btn', { style: 'display: flex; align-items: center; justify-content: center; gap: 0.5rem;', - onclick: () => setShowCompose(true), + onclick: () => { + mobileNavOpen = false; + setShowCompose(true); + }, }, [m('i.fas.fa-pen'), 'Compose'] ), @@ -126,12 +152,15 @@ const Layout = () => { tabs: Object.keys(sections), size: sectionsSize, baseRoute: '/mail/', + onNavigate: () => { mobileNavOpen = false; }, }), m(util.SidebarQuickView, { tabs: Object.keys(sectionsquickview), size: sectionsQuickviewSize, baseRoute: '/mail/', + onNavigate: () => { mobileNavOpen = false; }, }), + ]), ]), m( '.node-panel', @@ -141,16 +170,37 @@ const Layout = () => { m( 'select.mail-tag', { - value: tagselect.showval, - onchange: (e) => (tagselect.showval = tagselect.opts[e.target.selectedIndex]), + value: m.route.param().tab || '', + onchange: (e) => { + const selectedTag = e.target.value; + if (selectedTag) { + m.route.set('/mail/:tab', { tab: selectedTag }); + } + }, }, - [tagselect.opts.map((opt) => m('option', { value: opt }, opt.toLocaleString()))] + tagselect.opts.map((opt) => m('option', { value: opt.val }, opt.label)) ), m(util.SearchBar, { list: {} }), ]), - vnode.children, + activeBox + ? m('.mail-box-content', [ + m('.mail-mobile-box-title', [ + m('i.fas', { class: activeBoxIcons[activeTab] || 'fa-envelope' }), + m('span', activeBox.title), + ]), + vnode.children, + ]) + : vnode.children, ]) ), + m( + 'button.mobile-fab-compose', + { + title: 'Compose Mail', + onclick: () => setShowCompose(true), + }, + m('i.fas.fa-pen') + ), showCompose && m( '.composePopupOverlay#mailComposerPopup', m( @@ -173,6 +223,7 @@ const tabConfig = { starred: { title: 'Starred', category: 'starred' }, system: { title: 'System', category: 'system' }, spam: { title: 'Spam', category: 'spam' }, + attachment: { title: 'Attachments', category: 'attachment' }, important: { title: 'Important', category: 'important' }, work: { title: 'Work', category: 'work' }, todo: { title: 'Todo', category: 'todo' }, @@ -216,7 +267,7 @@ module.exports = { if (tab === 'attachment') { return m( Layout, - m(sectionsquickview.attachment, { + m(sections.attachment, { list: util.sortList(Messages[tab]), }) ); diff --git a/webui-src/app/mail/mail_util.js b/webui-src/app/mail/mail_util.js index f4acb680..493b4597 100644 --- a/webui-src/app/mail/mail_util.js +++ b/webui-src/app/mail/mail_util.js @@ -53,19 +53,19 @@ function renderMailUserTooltip() { ? ((details.mReputation.mFriendsPositiveVotes || 0) - (details.mReputation.mFriendsNegativeVotes || 0)) : 0; - const top = hUser.rect.top - 10; - const left = Math.min(Math.max(hUser.rect.left, 140), window.innerWidth - 280); + const rect = hUser.rect; + const top = rect ? Math.max(10, Math.min(rect.bottom + 4, window.innerHeight - 185)) : 100; + const left = rect ? Math.min(Math.max(rect.left, 20), window.innerWidth - 300) : 100; return m('.user-tooltip', { style: { position: 'fixed', top: `${top}px`, left: `${left}px`, - transform: 'translateY(-100%)', zIndex: 10000, } }, [ - m('.tooltip-avatar', m(peopleUtil.UserAvatar, { avatar, firstLetter, identityId: hUser.gxsId, size: 64 })), + m('.tooltip-avatar', m(peopleUtil.UserAvatar, { avatar, firstLetter, identityId: hUser.gxsId, size: 64, isSquare: true })), m('.tooltip-details', [ m('.tooltip-row', [m('span.tooltip-label', 'Identity name: '), m('span.tooltip-value', hUser.name)]), m('.tooltip-row', [m('span.tooltip-label', 'Identity Id: '), m('span.tooltip-value.tooltip-id', hUser.gxsId)]), @@ -77,7 +77,7 @@ function renderMailUserTooltip() { m('span.tooltip-label', 'Votes: '), m('span.tooltip-value', { style: { - color: votes >= 0 ? '#22c55e' : '#ef4444', + color: votes >= 0 ? '#008000' : '#cc0000', fontWeight: 'bold' } }, (votes >= 0 ? '+' : '') + votes) @@ -113,6 +113,22 @@ function loadTagTypes() { } loadTagTypes(); +function formatMailDate(ts) { + if (!ts) return ''; + const date = new Date(ts * 1000); + const now = new Date(); + const isToday = date.toDateString() === now.toDateString(); + if (isToday) { + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } + const isThisYear = date.getFullYear() === now.getFullYear(); + const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + if (isThisYear) { + return `${date.getDate()} ${months[date.getMonth()]}`; + } + return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear().toString().slice(2)}`; +} + // Utility functions const humanReadableSize = (fileSize) => { return fileSize / 1024 > 1024 @@ -179,7 +195,7 @@ const MessageSummary = () => { }, [ m( - 'td', + 'td.cell-star', m(`input.star-check[type=checkbox][id=msg-${v.attrs.details.msgId}]`, { checked: isStarred }), // Use label with [for] to manipulate hidden checkbox m( @@ -191,8 +207,8 @@ const MessageSummary = () => { m('i.fas.fa-star') ) ), - files && m('td', files.length), - m('td', { style: 'border-bottom: inherit;' }, [ + m('td.cell-attachment', files && files.length > 0 ? m('i.fas.fa-paperclip', { title: `${files.length} attachment(s)` }) : null), + m('td.cell-subject', [ m('div', { style: { display: 'flex', @@ -213,7 +229,7 @@ const MessageSummary = () => { ]) ]), m( - 'td', + 'td.cell-from', m( 'div', { @@ -257,7 +273,7 @@ const MessageSummary = () => { ] ) ), - m('td', new Date(details.ts * 1000).toLocaleString()), + m('td.cell-date', { title: new Date(details.ts * 1000).toLocaleString() }, formatMailDate(details.ts)), ] ), }; @@ -265,7 +281,8 @@ const MessageSummary = () => { const AttachmentSection = () => { function handleAttachmentDownload(item) { - const { fname: fileName, hash, size: xstr64 } = item; + const { fname: fileName, hash, size } = item; + const xstr64 = typeof size === 'object' ? size.xstr64 : String(size); const flags = util.RS_FILE_REQ_ANONYMOUS_ROUTING; rs.rsJsonApiRequest( '/rsFiles/FileRequest', @@ -279,26 +296,22 @@ const AttachmentSection = () => { } return { view: (v) => - m('table.attachment-container', [ - m('tr.attachment-header', [ - m('th', 'File Name'), - m('th', 'From'), - m('th', 'Size'), - m('th', 'Date'), - m('th', 'Download'), - ]), - m( - 'tbody', - v.attrs.files.map((file) => - m('tr.attachment', [ - m('td.attachment__name', [m('i.fas.fa-file'), m('span', file.fname)]), - m('td.attachment__from', rs.userList.userMap[file.from._addr_string] || '[Unknown]'), - m('td.attachment__size', humanReadableSize(file.size.xint64)), - m('td.attachment__date', new Date(file.ts * 1000).toLocaleString()), - m('td', m('button', { onclick: () => handleAttachmentDownload(file) }, 'Download')), - ]) - ) - ), + m('.attachments-wrapper', [ + v.attrs.files.map((file) => { + const fileSizeNum = file.size ? (typeof file.size === 'object' ? file.size.xint64 || parseInt(file.size.xstr64) || 0 : Number(file.size) || 0) : 0; + return m('.attachment-card', [ + m('.attachment-icon', m('i.fas.fa-paperclip')), + m('.attachment-info', [ + m('.attachment-name', file.fname), + m('.attachment-size', humanReadableSize(fileSizeNum)), + ]), + m( + 'button.btn-attachment-download', + { onclick: () => handleAttachmentDownload(file) }, + [m('i.fas.fa-download'), m('span.btn-text', ' Download')] + ), + ]); + }), ]), }; }; @@ -405,10 +418,10 @@ const MessageView = () => { m('i.fas.fa-arrow-left') ), m('.msg-view-nav__action', [ - m('button', { onclick: () => { composeType = 'reply'; setShowCompose(true); } }, 'Reply'), - m('button', { onclick: () => { composeType = 'replyAll'; setShowCompose(true); } }, 'Reply All'), - m('button', { onclick: () => { composeType = 'forward'; setShowCompose(true); } }, 'Forward'), - m('button', { onclick: confirmMailDelete }, 'Delete'), + m('button', { onclick: () => { composeType = 'reply'; setShowCompose(true); } }, [m('i.fas.fa-reply'), m('span.btn-text', ' Reply')]), + m('button', { onclick: () => { composeType = 'replyAll'; setShowCompose(true); } }, [m('i.fas.fa-reply-all'), m('span.btn-text', ' Reply All')]), + m('button', { onclick: () => { composeType = 'forward'; setShowCompose(true); } }, [m('i.fas.fa-forward'), m('span.btn-text', ' Forward')]), + m('button.red', { onclick: confirmMailDelete }, [m('i.fas.fa-trash'), m('span.btn-text', ' Delete')]), ]), ]), m('.msg-view__header', [ @@ -736,12 +749,11 @@ const sidebarIcons = { const Sidebar = () => { return { - view: ({ attrs: { tabs, baseRoute, size } }) => + view: ({ attrs: { tabs, baseRoute, size, onNavigate } }) => m( '.sidebar', tabs.map((panelName, index) => { const displayName = panelName.charAt(0).toUpperCase() + panelName.slice(1); - const labelText = size[panelName] > 0 ? `${displayName} (${size[panelName]})` : displayName; return m( m.route.Link, { @@ -750,12 +762,14 @@ const Sidebar = () => { onclick: () => { activeSideLink.sideactive = index; activeSideLink.quicksideactive = -1; + if (onNavigate) onNavigate(); }, href: baseRoute + panelName, }, [ sidebarIcons[panelName] || null, - labelText, + m('span.sidebar-link-text', displayName), + size[panelName] > 0 && m('span.sidebar-badge', size[panelName]), ] ); }) @@ -766,13 +780,12 @@ const Sidebar = () => { const SidebarQuickView = () => { // for the Mail tab, to be moved later. return { - view: ({ attrs: { tabs, baseRoute, size } }) => + view: ({ attrs: { tabs, baseRoute, size, onNavigate } }) => m( '.sidebarquickview', m('h6.bold', 'Quick View'), tabs.map((panelName, index) => { const displayName = panelName.charAt(0).toUpperCase() + panelName.slice(1); - const labelText = size[panelName] > 0 ? `${displayName} (${size[panelName]})` : displayName; return m( m.route.Link, { @@ -782,12 +795,14 @@ const SidebarQuickView = () => { onclick: () => { activeSideLink.quicksideactive = index; activeSideLink.sideactive = -1; + if (onNavigate) onNavigate(); }, href: baseRoute + panelName, }, [ sidebarIcons[panelName] || null, - labelText, + m('span.sidebar-link-text', displayName), + size[panelName] > 0 && m('span.sidebar-badge', size[panelName]), ] ); }) diff --git a/webui-src/app/main.js b/webui-src/app/main.js index 867acd3d..50a343bd 100644 --- a/webui-src/app/main.js +++ b/webui-src/app/main.js @@ -11,6 +11,7 @@ const files = require('files/files_resolver'); const channels = require('channels/channels'); const forums = require('forums/forums'); const boards = require('boards/boards'); +const feedreader = require('feedreader/feedreader'); const config = require('config/config_resolver'); const statusbar = require('statusbar'); @@ -24,6 +25,7 @@ const navIcon = { channels: m('i.fas.fa-tv.sidenav-icon'), forums: m('i.fas.fa-bullhorn.sidenav-icon'), boards: m('i.fas.fa-globe.sidenav-icon'), + feedreader: m('i.fas.fa-rss.sidenav-icon'), config: m('i.fas.fa-cogs.sidenav-icon'), }; @@ -114,7 +116,7 @@ const navbar = () => { ? 'Connected to RetroShare Core' : 'Connection Lost', }), - m('span.webui-version', { style: { fontSize: '0.7em' } }, 'v131'), + m('span.webui-version', { style: { fontSize: '0.7em' } }, 'v132'), m('i.fas.fa-sync-alt.refresh-icon', { style: { cursor: 'pointer', fontSize: '0.8em' }, onclick: () => window.location.reload(true), @@ -175,6 +177,7 @@ const Layout = () => { channels: '/channels/MyChannels', forums: '/forums/MyForums', boards: '/boards/MyBoards', + feedreader: '/feedreader', config: '/config/network', }, }), @@ -261,6 +264,9 @@ m.route(document.getElementById('main'), '/', { '/boards/:tab/:mGroupId/:mMsgId': { render: (v) => m(Layout, m(boards, v.attrs)), }, + '/feedreader': { + render: () => m(Layout, m(feedreader)), + }, '/config/:tab': { render: (v) => m(Layout, m(config, v.attrs)), }, diff --git a/webui-src/app/mithril.js b/webui-src/app/mithril.js index eca571df..6eed2eea 100644 --- a/webui-src/app/mithril.js +++ b/webui-src/app/mithril.js @@ -215,11 +215,12 @@ //takes advantage of the fact the current `vnode` is the first argument in //all lifecycle methods. function callHook(vnode3) { - var original = vnode3.state; + if (typeof this !== 'function') return; + var original = vnode3 ? vnode3.state : undefined; try { return this.apply(original, arguments); } finally { - checkState(vnode3, original); + if (vnode3) checkState(vnode3, original); } } // IE11 (at least) throws an UnspecifiedError when accessing document.activeElement when @@ -1109,12 +1110,13 @@ if (typeof source.onupdate === 'function') hooks.push(callHook.bind(source.onupdate, vnode3)); } function shouldNotUpdate(vnode3, old) { + if (!vnode3 || !old || !old.dom) return false; do { if (vnode3.attrs != null && typeof vnode3.attrs.onbeforeupdate === 'function') { var force = callHook.call(vnode3.attrs.onbeforeupdate, vnode3, old); if (force !== undefined && !force) break; } - if (typeof vnode3.tag !== 'string' && typeof vnode3.state.onbeforeupdate === 'function') { + if (typeof vnode3.tag !== 'string' && vnode3.state && typeof vnode3.state.onbeforeupdate === 'function') { var force = callHook.call(vnode3.state.onbeforeupdate, vnode3, old); if (force !== undefined && !force) break; } diff --git a/webui-src/app/network/network.js b/webui-src/app/network/network.js index 99d77521..be498411 100644 --- a/webui-src/app/network/network.js +++ b/webui-src/app/network/network.js @@ -9,6 +9,7 @@ const { fetchIdDetails, startDirectChat, getOnlineSslId, + preloadNetworkChatHistory, } = require('network/network_state'); const { OwnProfileCard, FriendsList } = require('network/network_friends_list'); const DetailsTab = require('network/network_details_tab'); @@ -17,7 +18,10 @@ const ChatTab = require('network/network_chat_tab'); const NetworkLayout = () => { return { oninit: () => { - Data.refreshGpgDetails().then(() => m.redraw()); + Data.refreshGpgDetails().then(() => { + preloadNetworkChatHistory(); + m.redraw(); + }); loadOwnProfile(); loadGxsIdentities(); }, diff --git a/webui-src/app/network/network_chat_tab.js b/webui-src/app/network/network_chat_tab.js index a10bf867..c99d4d5a 100644 --- a/webui-src/app/network/network_chat_tab.js +++ b/webui-src/app/network/network_chat_tab.js @@ -1,6 +1,76 @@ const m = require('mithril'); +const rs = require('rswebui'); const Data = require('network/network_data'); -const { State, startDirectChat, getOnlineSslId, sendDirectChatMessage } = require('network/network_state'); +const { + State, + startDirectChat, + getOnlineSslId, + sendDirectChatMessage, + loadAllDirectChatHistory, +} = require('network/network_state'); +const { renderChatMessage } = require('chat/chat_state'); +const chatEmoji = require('chat/chat_emoji'); +const HistoryBrowserModal = require('people/people_history'); + +// Direct peer-to-peer chat images do NOT require 200KB compression limit +function formatDirectChatImage(file, callback) { + if (!file) return; + const reader = new FileReader(); + reader.onload = (evt) => { + const img = new Image(); + img.onload = () => { + const maxWidth = 1920; + const maxHeight = 1080; + let width = img.width; + let height = img.height; + + if (width > maxWidth || height > maxHeight) { + const ratio = Math.min(maxWidth / width, maxHeight / height); + width = Math.round(width * ratio); + height = Math.round(height * ratio); + + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, width, height); + const dataUrl = canvas.toDataURL('image/jpeg', 0.92); + callback(``); + } else { + callback(``); + } + }; + img.onerror = () => { + if (evt.target.result) { + callback(``); + } else { + callback(null); + } + }; + img.src = evt.target.result; + }; + reader.readAsDataURL(file); +} + +function pollHashStatusForDirectChat(localpath) { + rs.rsJsonApiRequest('/rsFiles/ExtraFileStatus', { localpath }, (data) => { + if (data && data.retval && data.info && data.info.hash && data.info.hash !== '0000000000000000000000000000000000000000') { + const info = data.info; + const sizeNum = info.size.xint64 || parseInt(info.size.xstr64) || info.size; + const fileLink = `${info.name} (${rs.formatBytes(sizeNum)})`; + + State.chatInputMsg = State.chatInputMsg ? State.chatInputMsg + '\n' + fileLink : fileLink; + State.showAttachModal = false; + State.isHashing = false; + State.attachPath = ''; + m.redraw(); + } else { + if (State.isHashing) { + setTimeout(() => pollHashStatusForDirectChat(localpath), 1000); + } + } + }); +} const ChatTab = () => { return { @@ -70,38 +140,117 @@ const ChatTab = () => { }), m('span', { style: { color: locOnline ? '#10b981' : '#ef4444', fontWeight: '500' } }, locOnline ? 'Online' : 'Offline') ]) - ]) + ]), + m('button.blue.history-btn', { + title: 'View all direct chat history with this friend', + style: 'padding: 0.25rem 0.75rem; border-radius: 0.25rem; font-size: 0.85rem; display: flex; align-items: center; gap: 0.35rem; border: none; cursor: pointer; background-color: #3b82f6; color: #ffffff; font-weight: 600;', + onclick: () => { + State.showHistoryModal = true; + State.historySearchQuery = ''; + loadAllDirectChatHistory(); + }, + }, [m('i.fas.fa-history'), 'History']) ]); })(), m( '.chat-messages[id=chat-messages-container]', State.chatMessages.map((msg) => { - const isOwn = msg.own === true; + const isOwn = msg.own === true || msg.incoming === false; const senderName = isOwn ? (State.ownProfile.name || 'Me') : friend.name; - const time = new Date(msg.sendTime * 1000).toLocaleTimeString(); - const text = (msg.msg || '') - .replaceAll('
', '\n') - .replace(new RegExp('|<[^>]*>', 'gm'), ''); + const time = new Date((msg.sendTime || msg.recvTime || 0) * 1000).toLocaleTimeString(); + const text = msg.msg || msg.message || ''; return m( '.chat-bubble-container' + (isOwn ? '.outgoing' : '.incoming'), [ !isOwn && m('.chat-sender', senderName), - m('.chat-bubble', text), + m('.chat-bubble', renderChatMessage(text)), m('.chat-time', time), ] ); }) ), - m('.chat-input-area', [ + m(HistoryBrowserModal, { + state: State, + name: friend.name, + ownName: State.ownProfile.name || 'You', + }), + m('.chat-input-area', { style: 'display: flex; align-items: center; gap: 0.5rem; padding: 0.75rem; background: #ffffff; border-top: 1px solid #cbd5e1;' }, [ + m('button.chat-hub-action-btn', { + title: 'Attach file link', + onclick: () => { + State.showAttachModal = true; + State.attachPath = ''; + State.attachBrowseHint = false; + State.hashingError = ''; + m.redraw(); + } + }, m('i.fas.fa-paperclip')), + + m('.emoji-picker-wrapper', { style: 'position: relative;' }, [ + m('button.chat-hub-action-btn', { + title: 'Insert emoji', + onclick: (e) => { + e.stopPropagation(); + State.showEmojiPicker = !State.showEmojiPicker; + } + }, m('i.fas.fa-smile')), + State.showEmojiPicker && m(chatEmoji.EmojiPicker, { + onSelect: (emoji) => { + State.chatInputMsg = (State.chatInputMsg || '') + emoji; + State.showEmojiPicker = false; + m.redraw(); + } + }), + ]), + + m('label.chat-hub-action-btn', { + title: 'Send image', + style: 'cursor: pointer;', + }, [ + m('i.fas.fa-image'), + m('input[type=file][accept=image/*]', { + style: 'display: none;', + onchange: (e) => { + if (!e.target.files || !e.target.files[0]) return; + const file = e.target.files[0]; + formatDirectChatImage(file, (imgTag) => { + if (imgTag) { + State.chatInputMsg = (State.chatInputMsg || '') + imgTag; + m.redraw(); + } + }); + e.target.value = ''; + } + }) + ]), + m('textarea.chat-textarea', { - placeholder: 'Type your message... Press Enter to send', + placeholder: 'Type a message here...', value: State.chatInputMsg, + style: 'flex: 1; resize: none; border: 1px solid #cbd5e1; border-radius: 6px; padding: 0.5rem; font-family: inherit; font-size: 0.9rem; outline: none; min-height: 40px; max-height: 120px;', oninput: (e) => { State.chatInputMsg = e.target.value; }, + onpaste: (e) => { + const items = (e.clipboardData || (e.originalEvent && e.originalEvent.clipboardData))?.items; + if (!items) return; + for (let i = 0; i < items.length; i++) { + if (items[i].type.indexOf('image') !== -1) { + e.preventDefault(); + const blob = items[i].getAsFile(); + formatDirectChatImage(blob, (imgTag) => { + if (imgTag) { + State.chatInputMsg = (State.chatInputMsg || '') + imgTag; + m.redraw(); + } + }); + break; + } + } + }, onkeydown: (e) => { if (e.code === 'Enter' && !e.shiftKey) { e.preventDefault(); @@ -110,13 +259,124 @@ const ChatTab = () => { }, }), m( - 'button.send-btn', + 'button.send-btn.blue', { + style: 'height: 38px;', onclick: () => sendDirectChatMessage(), }, [m('i.fas.fa-paper-plane'), ' Send'] ), ]), + + State.showAttachModal && m('.attach-modal-overlay', { + onclick: (e) => { + if (e.target === e.currentTarget && !State.isHashing) { + State.showAttachModal = false; + State.attachPath = ''; + State.attachBrowseHint = false; + State.hashingError = ''; + } + } + }, [ + m('.attach-modal', [ + m('.attach-modal-header', [ + m('i.fas.fa-paperclip.attach-modal-icon'), + m('h4', 'Attach File to Direct Chat'), + ]), + m('p', 'Browse for a file or type the absolute path on your local system:'), + m('input#direct-attach-file-picker[type=file]', { + style: 'display:none', + onchange: (e) => { + const file = e.target.files && e.target.files[0]; + if (file) { + const fullPath = file.path; + const hasFullPath = fullPath && (fullPath.includes('/') || fullPath.includes('\\')) && fullPath !== file.name; + if (hasFullPath) { + State.attachPath = fullPath; + State.attachBrowseHint = false; + } else { + State.attachPath = file.name; + State.attachBrowseHint = true; + } + e.target.value = ''; + State.hashingError = ''; + m.redraw(); + } + }, + }), + m('.attach-path-row', [ + m('input[type=text]', { + placeholder: 'e.g. C:\\Downloads\\file.zip', + value: State.attachPath, + oninput: (e) => { + State.attachPath = e.target.value; + State.attachBrowseHint = false; + }, + disabled: State.isHashing, + }), + m('button.attach-browse-btn', { + type: 'button', + disabled: State.isHashing, + title: 'Browse for file', + onclick: () => { + const picker = document.getElementById('direct-attach-file-picker'); + if (picker) picker.click(); + }, + }, [m('i.fas.fa-folder-open'), m('span', ' Browse…')]), + ]), + State.attachBrowseHint && m('.attach-path-hint', [ + m('i.fas.fa-info-circle'), + m('span', [ + ' Your browser cannot expose the full file path. ', + m('strong', 'Edit the path above'), + ' and add your folder prefix — e.g. change ', + m('code', 'file.zip'), + ' to ', + m('code', 'C:\\Downloads\\file.zip'), + ' — then click Attach.', + ]), + ]), + State.isHashing && m('.hashing-spinner', [ + m('i.fas.fa-spinner.fa-spin'), + m('span', ' Hashing file... Please wait.') + ]), + !State.attachBrowseHint && State.hashingError && m('p.error-text', State.hashingError), + m('.modal-buttons', [ + m('button.btn.blue', { + disabled: State.isHashing || !State.attachPath.trim() || State.attachBrowseHint, + onclick: () => { + const path = State.attachPath.trim(); + State.isHashing = true; + State.hashingError = ''; + m.redraw(); + + rs.rsJsonApiRequest('/rsFiles/ExtraFileHash', { + localpath: path, + period: 86400 * 7, + flags: 0 + }, (data, success) => { + if (success && data.retval) { + pollHashStatusForDirectChat(path); + } else { + State.isHashing = false; + State.hashingError = 'Failed to initiate file hashing. Check the path and try again.'; + m.redraw(); + } + }); + } + }, [m('i.fas.fa-link'), m('span', ' Attach')]), + m('button.btn.red', { + disabled: State.isHashing, + onclick: () => { + State.showAttachModal = false; + State.attachPath = ''; + State.attachBrowseHint = false; + State.hashingError = ''; + } + }, 'Cancel') + ]) + ]) + ]), ]); }, }; diff --git a/webui-src/app/network/network_data.js b/webui-src/app/network/network_data.js index 427cbf3f..0a7626b5 100644 --- a/webui-src/app/network/network_data.js +++ b/webui-src/app/network/network_data.js @@ -20,6 +20,29 @@ async function loadSslDetails() { const Data = { gpgDetails: {}, }; + +function normalizeStatusValue(value, fallback) { + if (value && typeof value === 'object') value = value.value ?? value.status ?? value.xint32; + if (typeof value === 'number') return value; + if (typeof value === 'string') { + const numeric = Number(value); + if (Number.isFinite(numeric)) return numeric; + const names = { OFFLINE: 0, AWAY: 1, BUSY: 2, ONLINE: 3, INACTIVE: 4 }; + const match = Object.keys(names).find((name) => value.toUpperCase().includes(name)); + if (match) return names[match]; + } + return fallback; +} + +Data.getStatusPresentation = function (statusValue, isOnline = false) { + const value = normalizeStatusValue(statusValue, isOnline ? 3 : 0); + return { + value, + label: ['Offline', 'Away', 'Busy', 'Online', 'Inactive'][value] || (isOnline ? 'Online' : 'Offline'), + color: ['#94a3b8', '#eab308', '#ef4444', '#10b981', '#f59e0b'][value] || '#94a3b8', + }; +}; + Data.refreshGpgDetails = async function () { const details = {}; const sslDetails = await loadSslDetails(); @@ -34,6 +57,8 @@ Data.refreshGpgDetails = async function () { ) .then(() => { let customState = ''; + let statusValue = isOnline ? 3 : 0; + let statusTimestamp = 0; return rs .rsJsonApiRequest( '/rsChats/getCustomStateString', @@ -45,19 +70,19 @@ Data.refreshGpgDetails = async function () { } ) .catch(() => {}) + .then(() => rs.rsJsonApiRequest( + '/rsStatus/getStatus', + { id: data.id }, + (statusData) => { + if (statusData && statusData.retval && statusData.statusInfo) { + statusValue = normalizeStatusValue(statusData.statusInfo.status, statusValue); + statusTimestamp = statusData.statusInfo.time_stamp || 0; + } + } + ).catch(() => {})) .then(() => { - let avatar = ''; - return rs - .rsJsonApiRequest( - '/rsChats/getAvatar', - { pid: data.id }, - (avatarData) => { - if (avatarData && avatarData.retval && avatarData.avatar_base64_string) { - avatar = avatarData.avatar_base64_string; - } - } - ) - .catch(() => {}) + const avatar = ''; + return Promise.resolve() .then(() => { const gpgId = (data.gpg_id || '').toLowerCase(); const loc = { @@ -67,6 +92,8 @@ Data.refreshGpgDetails = async function () { isOnline, gpg_id: gpgId, customState, + statusValue, + statusTimestamp, avatar, }; @@ -77,6 +104,8 @@ Data.refreshGpgDetails = async function () { isOnline, locations: [loc], customState, + statusValue, + statusTimestamp, avatar: avatar || '', }; } else { @@ -87,6 +116,10 @@ Data.refreshGpgDetails = async function () { if (!details[gpgId].customState || (isOnline && customState)) { details[gpgId].customState = customState; } + if (isOnline || !details[gpgId].isOnline) { + details[gpgId].statusValue = statusValue; + details[gpgId].statusTimestamp = statusTimestamp; + } } details[gpgId].isOnline = details[gpgId].isOnline || isOnline; }); diff --git a/webui-src/app/network/network_details_tab.js b/webui-src/app/network/network_details_tab.js index 44793fce..a1d08adf 100644 --- a/webui-src/app/network/network_details_tab.js +++ b/webui-src/app/network/network_details_tab.js @@ -37,6 +37,7 @@ const DetailsTab = () => { if (!friend) return null; const friendGxsId = State.gpgToGxsIdMap[gpgId.toLowerCase()]; + const status = Data.getStatusPresentation(friend.statusValue, friend.isOnline); return m('.network-detail-view', [ m('.detail-header', [ @@ -52,30 +53,30 @@ const DetailsTab = () => { m('i.fas.fa-fingerprint'), m('span', 'GPG ID: ' + gpgId), ]), - ]), - m('.detail-actions', [ - m( - 'button', - { - onclick: () => { - const sslId = getOnlineSslId(gpgId); - if (sslId) { - State.activeTab = 'chat'; - startDirectChat(sslId); - } + m('.detail-actions', { style: 'margin-top: 0.75rem;' }, [ + m( + 'button', + { + onclick: () => { + const sslId = getOnlineSslId(gpgId); + if (sslId) { + State.activeTab = 'chat'; + startDirectChat(sslId); + } + }, }, - }, - [m('i.fas.fa-comments'), ' Start Chat'] - ), - m( - 'button', - { - onclick: () => { - State.showMailCompose = true; + [m('i.fas.fa-comments'), m('span.btn-text', ' Start Chat')] + ), + m( + 'button', + { + onclick: () => { + State.showMailCompose = true; + }, }, - }, - [m('i.fas.fa-envelope'), ' Send Mail'] - ), + [m('i.fas.fa-envelope'), m('span.btn-text', ' Send Mail')] + ), + ]), ]), ]), @@ -85,8 +86,8 @@ const DetailsTab = () => { m('.info-label', 'Status'), m( '.info-value', - { style: friend.isOnline ? 'color: #10b981; font-weight: 600;' : '' }, - friend.isOnline ? 'Online' : 'Offline' + { style: `color: ${status.color}; font-weight: 600;` }, + status.label ), m('.info-label', 'Custom Status'), m( @@ -110,13 +111,15 @@ const DetailsTab = () => { friend.locations .slice() .sort((a, b) => (a.isOnline === b.isOnline ? 0 : a.isOnline ? -1 : 1)) - .map((loc) => - m('.location-card', { key: loc.id }, [ + .map((loc) => { + const locStatus = Data.getStatusPresentation(loc.statusValue, loc.isOnline); + return m('.location-card', { key: loc.id }, [ m('.loc-header', [ m('.loc-name', loc.name), m( - '.loc-status' + (loc.isOnline ? '.online' : '.offline'), - loc.isOnline ? 'Online' : 'Offline' + '.loc-status', + { style: { color: locStatus.color } }, + locStatus.label ), ]), m('.loc-body', [ @@ -139,8 +142,8 @@ const DetailsTab = () => { 'Remove Location' ), ]), - ]) - ) + ]); + }) ), ]), ]); diff --git a/webui-src/app/network/network_friends_list.js b/webui-src/app/network/network_friends_list.js index cee53289..4ff18289 100644 --- a/webui-src/app/network/network_friends_list.js +++ b/webui-src/app/network/network_friends_list.js @@ -1,29 +1,91 @@ const m = require('mithril'); const Data = require('network/network_data'); const peopleUtil = require('people/people_util'); -const { State, startDirectChat, getOnlineSslId } = require('network/network_state'); +const { State, startDirectChat, getOnlineSslId, setOwnCustomStateString } = require('network/network_state'); + +function formatRelativeTime(ts) { + if (!ts) return ''; + const now = Math.floor(Date.now() / 1000); + const diff = now - ts; + if (diff < 30) return 'Just Now'; + if (diff < 3600) return `${Math.floor(diff / 60)} min${Math.floor(diff / 60) > 1 ? 's' : ''}`; + if (diff < 86400) return `${Math.floor(diff / 3600)} hr${Math.floor(diff / 3600) > 1 ? 's' : ''}`; + return `${Math.floor(diff / 86400)} d`; +} const OwnProfileCard = () => { + let isEditing = false; + let statusInputText = ''; + return { view: () => { const avatar = State.ownProfile.avatar ? { mData: { base64: State.ownProfile.avatar } } : undefined; const firstLetter = (State.ownProfile.name || 'U').slice(0, 1).toUpperCase(); + const displayName = State.ownProfile.location + ? `${State.ownProfile.name || 'Unknown'} (${State.ownProfile.location})` + : State.ownProfile.name || 'Loading...'; + const status = Data.getStatusPresentation(State.ownProfile.statusValue, true); return m('.own-profile-card', [ m('.profile-header', [ - m(peopleUtil.UserAvatar, { avatar, firstLetter, seed: State.ownProfile.name }), + m('.profile-avatar-wrapper', [ + m(peopleUtil.UserAvatar, { avatar, firstLetter, seed: State.ownProfile.name }), + m('.status-dot', { + style: { backgroundColor: status.color }, + title: status.label, + }), + ]), m('.profile-info', [ - m('.profile-name', State.ownProfile.name || 'Loading...'), - m('.profile-status', 'Online'), - State.ownProfile.customState && - m( - '.profile-custom-status', - { - style: 'font-size: 0.8rem; color: #94a3b8; font-style: italic; margin-top: 2px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 150px;', - title: State.ownProfile.customState, - }, - State.ownProfile.customState - ), + m('.profile-name', { title: displayName }, displayName), + isEditing + ? m('.profile-custom-status-edit', { + style: 'display: flex; align-items: center; gap: 4px; margin-top: 3px;' + }, [ + m('input[type=text]', { + value: statusInputText, + placeholder: 'Set custom status...', + style: 'font-size: 0.8rem; padding: 2px 6px; border: 1px solid #3ba4d7; border-radius: 4px; width: 125px; outline: none; background: #ffffff;', + oninput: (e) => { statusInputText = e.target.value; }, + onkeydown: (e) => { + if (e.key === 'Enter') { + setOwnCustomStateString(statusInputText); + isEditing = false; + } else if (e.key === 'Escape') { + isEditing = false; + } + }, + oncreate: (vnode) => vnode.dom.focus(), + }), + m('i.fas.fa-check', { + style: 'cursor: pointer; color: #10b981; font-size: 0.85rem; padding: 2px;', + title: 'Save status', + onclick: () => { + setOwnCustomStateString(statusInputText); + isEditing = false; + }, + }), + m('i.fas.fa-times', { + style: 'cursor: pointer; color: #ef4444; font-size: 0.85rem; padding: 2px;', + title: 'Cancel', + onclick: () => { + isEditing = false; + }, + }), + ]) + : m( + '.profile-custom-status', + { + style: State.ownProfile.customState + ? 'font-size: 0.825rem; color: #64748b; font-style: italic; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 180px; cursor: pointer; margin-top: 2px;' + : 'font-size: 0.825rem; color: #94a3b8; font-style: italic; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 180px; cursor: pointer; margin-top: 2px;', + title: 'Edit status message', + onclick: () => { + statusInputText = State.ownProfile.customState || ''; + isEditing = true; + }, + }, + State.ownProfile.customState || 'Set custom status...' + ), ]), ]), ]); @@ -35,66 +97,182 @@ const FriendsList = () => { return { view: () => { const search = State.searchString.toLowerCase(); - const filteredFriends = Object.entries(Data.gpgDetails).filter( - ([gpgId, friend]) => (friend.name || '').toLowerCase().includes(search) - ); + const allGpgEntries = Object.entries(Data.gpgDetails || {}); + + // Compute active chats count + let activeChatsCount = 0; + allGpgEntries.forEach(([gpgId]) => { + const hist = State.chatHistoryMap && State.chatHistoryMap[gpgId]; + if (hist && hist.lastMsg) { + activeChatsCount++; + } + }); + + let displayFriends = []; + + if (State.mainTab === 'network') { + displayFriends = allGpgEntries.filter(([gpgId, friend]) => + (friend.name || '').toLowerCase().includes(search) + ); + displayFriends.sort((a, b) => + a[1].isOnline === b[1].isOnline ? 0 : a[1].isOnline ? -1 : 1 + ); + } else { + // Chats Tab: filter friends with chat history + displayFriends = allGpgEntries.filter(([gpgId, friend]) => { + const hist = State.chatHistoryMap && State.chatHistoryMap[gpgId]; + if (!hist || !hist.lastMsg) return false; + return (friend.name || '').toLowerCase().includes(search); + }); + + displayFriends.sort((a, b) => { + const histA = State.chatHistoryMap[a[0]]; + const histB = State.chatHistoryMap[b[0]]; + const timeA = histA ? histA.lastTime : 0; + const timeB = histB ? histB.lastTime : 0; + return timeB - timeA; + }); + } return m('.friends-list-container', [ - m('.searchbar-container', [ - m('input.searchbar', { - type: 'text', - placeholder: 'Search friends...', - value: State.searchString, - oninput: (e) => { - State.searchString = e.target.value; - }, - }), + m('.people-sidebar-header', [ + m('.searchbar-wrapper', [ + m('i.fas.fa-search'), + m('input.searchbar-input', { + type: 'text', + placeholder: State.mainTab === 'network' ? 'Search friends...' : 'Search chats...', + value: State.searchString, + oninput: (e) => { + State.searchString = e.target.value; + }, + }), + ]), + m('.segmented-control', [ + m( + 'button.segment-tab' + (State.mainTab === 'network' ? '.active' : ''), + { + onclick: () => { + State.mainTab = 'network'; + }, + }, + [m('i.fas.fa-users'), ' Network'] + ), + m( + 'button.segment-tab' + (State.mainTab === 'chats' ? '.active' : ''), + { + onclick: () => { + State.mainTab = 'chats'; + }, + }, + [ + m('i.fas.fa-comments'), + ' Chats', + activeChatsCount > 0 && m('span.segment-badge', activeChatsCount), + ] + ), + ]), ]), m('.friends-scroll', [ - filteredFriends.length === 0 - ? m('p', { style: 'padding: 1rem; color: #94a3b8; text-align: center;' }, 'No friends found') - : filteredFriends - .sort((a, b) => (a[1].isOnline === b[1].isOnline ? 0 : a[1].isOnline ? -1 : 1)) - .map(([gpgId, friend]) => { - const avatar = friend.avatar ? { mData: { base64: friend.avatar } } : undefined; - const firstLetter = (friend.name || '?').slice(0, 1).toUpperCase(); - const isSelected = State.selectedFriendGpgId === gpgId; + displayFriends.length === 0 + ? m( + 'p', + { style: 'padding: 1rem; color: #94a3b8; text-align: center;' }, + State.mainTab === 'network' ? 'No friends found' : 'No active chats found' + ) + : displayFriends.map(([gpgId, friend]) => { + const avatar = friend.avatar ? { mData: { base64: friend.avatar } } : undefined; + const firstLetter = (friend.name || '?').slice(0, 1).toUpperCase(); + const isSelected = State.selectedFriendGpgId === gpgId; + const hist = State.chatHistoryMap && State.chatHistoryMap[gpgId]; + const status = Data.getStatusPresentation(friend.statusValue, friend.isOnline); + + const isOnlineOrActive = friend.isOnline || (status && status.value > 0); + if (State.mainTab === 'chats') { + // Render Chat List Item return m( - `.friend-list-item${isSelected ? '.selected' : ''}`, + `.chat-item${isSelected ? '.selected' : ''}`, { key: gpgId, onclick: () => { State.selectedFriendGpgId = gpgId; - State.currentChatPeerId = null; - State.chatMessages = []; - if (State.activeTab === 'chat') { - const sslId = getOnlineSslId(gpgId); - if (sslId) startDirectChat(sslId); - } + State.activeTab = 'chat'; + const sslId = getOnlineSslId(gpgId); + if (sslId) startDirectChat(sslId); }, }, [ - m('.friend-avatar', m(peopleUtil.UserAvatar, { avatar, firstLetter, seed: gpgId })), - m('.friend-meta', [ - m('.friend-name', friend.name), + m('.chat-avatar-wrapper', [ + m(peopleUtil.UserAvatar, { avatar, firstLetter, seed: gpgId }), + m('.status-dot', { + style: { + backgroundColor: status.color, + }, + title: status.label, + }), + ]), + m('.chat-info', [ m( - `.friend-status${friend.isOnline ? '.online' : ''}`, - friend.isOnline ? 'Online' : 'Offline' + '.chat-name', + { + style: isOnlineOrActive ? { color: status.color, fontWeight: '700' } : {}, + }, + friend.name ), - friend.customState && - m( - '.friend-custom-status', - { - style: 'font-size: 0.85rem; color: #64748b; margin-top: 2px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 160px;', - title: friend.customState, - }, - friend.customState - ), + m('.chat-last-msg', hist ? hist.lastMsg : ''), + ]), + m('.chat-meta', [ + hist && hist.lastTime && m('.chat-time', formatRelativeTime(hist.lastTime)), ]), ] ); - }), + } + + // Render Network Friend List Item + return m( + `.friend-list-item${isSelected ? '.selected' : ''}`, + { + key: gpgId, + onclick: () => { + State.selectedFriendGpgId = gpgId; + State.currentChatPeerId = null; + State.chatMessages = []; + if (State.activeTab === 'chat') { + const sslId = getOnlineSslId(gpgId); + if (sslId) startDirectChat(sslId); + } + }, + }, + [ + m('.friend-avatar', [ + m(peopleUtil.UserAvatar, { avatar, firstLetter, seed: gpgId }), + m('.status-dot', { + style: { backgroundColor: status.color }, + title: status.label, + }), + ]), + m('.friend-meta', [ + m( + '.friend-name', + { + style: isOnlineOrActive ? { color: status.color, fontWeight: '700' } : {}, + }, + friend.name + ), + friend.customState && + m( + '.friend-custom-status', + { + style: + 'font-size: 0.85rem; color: #64748b; margin-top: 2px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; max-width: 180px;', + title: friend.customState, + }, + friend.customState + ), + ]), + ] + ); + }), ]), ]); }, diff --git a/webui-src/app/network/network_state.js b/webui-src/app/network/network_state.js index bea212fd..be10010e 100644 --- a/webui-src/app/network/network_state.js +++ b/webui-src/app/network/network_state.js @@ -6,53 +6,123 @@ const peopleUtil = require('people/people_util'); const State = { ownProfile: { name: 'Loading...', + location: '', ssl_id: '', gpg_id: '', customState: '', + statusValue: 3, + statusTimestamp: 0, avatar: '', }, ownGxsIds: [], selectedOwnGxsId: '', selectedOwnGxsDetails: null, selectedFriendGpgId: null, + mainTab: 'network', // 'network' | 'chats' activeTab: 'details', // 'details' | 'chat' searchString: '', gpgToGxsIdMap: {}, gxsIdToDetailsMap: {}, gxsIdentities: [], + chatHistoryMap: {}, // gpgId -> { lastMsg, lastTime } currentChatPeerId: null, chatMessages: [], chatInputMsg: '', showMailCompose: false, + showAttachModal: false, + attachPath: '', + attachBrowseHint: false, + isHashing: false, + hashingError: '', + showEmojiPicker: false, + showHistoryModal: false, + historySearchQuery: '', + fullHistoryMessages: [], + isHistoryLoading: false, }; function loadOwnProfile() { + rs.rsJsonApiRequest('/rsStatus/getOwnStatus', {}, (statusData) => { + if (statusData && statusData.retval && statusData.statusInfo) { + State.ownProfile.statusValue = statusData.statusInfo.status; + State.ownProfile.statusTimestamp = statusData.statusInfo.time_stamp || 0; + m.redraw(); + } + }).catch(() => {}); + + const fetchOwnCustomState = () => { + rs.rsJsonApiRequest('/rsChats/getOwnCustomStateString', {}, (statusData) => { + if (statusData) { + let customState = ''; + if (typeof statusData.retval === 'string') { + customState = statusData.retval; + } else if (typeof statusData === 'string') { + customState = statusData; + } else if (statusData.retval && typeof statusData.retval === 'object') { + customState = + statusData.retval.status || + statusData.retval.customState || + statusData.retval.custom_state || + statusData.retval.status_string || + ''; + } else { + customState = + statusData.customState || + statusData.custom_state || + statusData.status || + statusData.status_string || + statusData.ownCustomStateString || + ''; + } + State.ownProfile.customState = customState; + m.redraw(); + } + }).catch(() => { + if (State.ownProfile.ssl_id) { + rs.rsJsonApiRequest( + '/rsChats/getCustomStateString', + { peer_id: State.ownProfile.ssl_id }, + (statusData) => { + if (statusData) { + const customState = + typeof statusData.retval === 'string' + ? statusData.retval + : statusData.customState || statusData.custom_state || statusData.status || ''; + State.ownProfile.customState = customState; + m.redraw(); + } + } + ).catch(() => {}); + } + }); + }; + + fetchOwnCustomState(); + rs.rsJsonApiRequest('/rsConfig/getConfigNetStatus', {}, (data) => { if (data && data.status) { State.ownProfile.name = data.status.ownName || 'Unknown'; State.ownProfile.ssl_id = data.status.ownId || ''; if (State.ownProfile.ssl_id) { - rs.rsJsonApiRequest('/rsChats/getCustomStateString', { peer_id: State.ownProfile.ssl_id }, (statusData) => { - if (statusData && statusData.retval) { - State.ownProfile.customState = statusData.retval; - m.redraw(); - } - }); + fetchOwnCustomState(); rs.rsJsonApiRequest('/rsPeers/getPeerDetails', { sslId: State.ownProfile.ssl_id }, (detData) => { - if (detData && detData.det && detData.det.gpg_id) { - State.ownProfile.gpg_id = detData.det.gpg_id; + if (detData && detData.det) { + State.ownProfile.gpg_id = detData.det.gpg_id || ''; + State.ownProfile.location = detData.det.location || ''; m.redraw(); } }); + /* Disabled getAvatar API call to avoid 404 network errors rs.rsJsonApiRequest('/rsChats/getAvatar', { pid: State.ownProfile.ssl_id }, (avatarData) => { if (avatarData && avatarData.retval && avatarData.avatar_base64_string) { State.ownProfile.avatar = avatarData.avatar_base64_string; m.redraw(); } }); + */ } m.redraw(); } @@ -116,6 +186,7 @@ function startDirectChat(sslId) { State.currentChatPeerId = sslId; State.chatMessages = []; loadDirectChatMessages(); + loadRecentDirectChatHistory(); } function getOnlineSslId(gpgId) { @@ -125,20 +196,136 @@ function getOnlineSslId(gpgId) { return onlineLoc ? onlineLoc.id : friend.locations[0].id; } +function isSystemMsg(msg) { + if (!msg) return false; + const str = String(msg); + return ( + str.includes('Distant chat requested') || + str.includes('Distant chat established') || + str.includes('Distant chat closed') || + str.includes('Distant chat status') + ); +} + +function preloadNetworkChatHistory() { + const gpgIds = Object.keys(Data.gpgDetails || {}); + gpgIds.forEach((gpgId) => { + if (!gpgId || gpgId === '0000000000000000') return; + + const privatePeerId = { + broadcast_status_peer_id: '00000000000000000000000000000000', + type: 1, // PRIVATE + peer_id: gpgId, + distant_chat_id: '00000000000000000000000000000000', + lobby_id: { xstr64: '0' }, + }; + + rs.rsJsonApiRequest( + '/rsHistory/getMessages', + { + chatPeerId: privatePeerId, + loadCount: 20, + }, + (msgData, success) => { + if (success && msgData && msgData.msgs) { + const userMsgs = msgData.msgs.filter( + (m) => !m.isSystem && !isSystemMsg(m.message || m.msg) + ); + if (userMsgs.length > 0) { + const last = userMsgs[userMsgs.length - 1]; + State.chatHistoryMap[gpgId] = { + lastMsg: last.message || last.msg || '', + lastTime: last.sendTime || last.recvTime || Math.floor(Date.now() / 1000), + }; + m.redraw(); + } + } + } + ); + }); +} + function loadDirectChatMessages() { rs.events[15].notify = (chatMessage) => { + const messagePeerId = chatMessage.chat_id && chatMessage.chat_id.peer_id + ? rs.idToHex(chatMessage.chat_id.peer_id) + : ''; if ( chatMessage.chat_id && - (chatMessage.chat_id.type === 1 || chatMessage.chat_id.type === 2) && - rs.idToHex(chatMessage.chat_id) === State.currentChatPeerId + chatMessage.chat_id.type === 1 && + messagePeerId === State.currentChatPeerId ) { State.chatMessages.push(chatMessage); + if (State.selectedFriendGpgId) { + State.chatHistoryMap[State.selectedFriendGpgId] = { + lastMsg: chatMessage.msg || chatMessage.message || '', + lastTime: chatMessage.sendTime || chatMessage.recvTime || Math.floor(Date.now() / 1000), + }; + } m.redraw(); scrollChatToBottom(); } }; } +function directChatId(peerId) { + return { + broadcast_status_peer_id: '00000000000000000000000000000000', + type: 1, + peer_id: peerId, + distant_chat_id: '00000000000000000000000000000000', + lobby_id: { xstr64: '0' }, + }; +} + +function mergeDirectChatMessages(messages) { + const unique = new Map(); + messages.forEach((message) => { + const text = message.msg || message.message || ''; + const time = message.sendTime || message.recvTime || 0; + const incoming = message.incoming === true; + unique.set(`${time}_${incoming}_${text}`, message); + }); + return Array.from(unique.values()).sort( + (a, b) => (a.sendTime || a.recvTime || 0) - (b.sendTime || b.recvTime || 0) + ); +} + +function loadRecentDirectChatHistory() { + const peerId = State.currentChatPeerId; + if (!peerId) return; + rs.rsJsonApiRequest('/rsHistory/getMessages', { + chatPeerId: directChatId(peerId), + loadCount: 20, + }, (data, success) => { + if (peerId !== State.currentChatPeerId) return; + if (success && data && Array.isArray(data.msgs)) { + State.chatMessages = mergeDirectChatMessages(data.msgs.concat(State.chatMessages)); + m.redraw(); + scrollChatToBottom(); + } + }); +} + +function loadAllDirectChatHistory() { + const peerId = State.currentChatPeerId; + if (!peerId) return; + State.isHistoryLoading = true; + State.fullHistoryMessages = []; + m.redraw(); + rs.rsJsonApiRequest('/rsHistory/getMessages', { + chatPeerId: directChatId(peerId), + loadCount: 0, + }, (data, success) => { + if (peerId !== State.currentChatPeerId) return; + State.fullHistoryMessages = success && data && Array.isArray(data.msgs) + ? mergeDirectChatMessages(data.msgs) + : []; + State.isHistoryLoading = false; + m.redraw(); + }); +} + function sendDirectChatMessage() { if (!State.chatInputMsg.trim() || !State.currentChatPeerId) return; @@ -153,13 +340,21 @@ function sendDirectChatMessage() { }, (data, success) => { if (success) { + const nowSec = Math.floor(Date.now() / 1000); State.chatMessages.push({ chat_id: { type: 1, peer_id: State.currentChatPeerId }, msg, - sendTime: Date.now() / 1000, + sendTime: nowSec, incoming: false, own: true, }); + + if (State.selectedFriendGpgId) { + State.chatHistoryMap[State.selectedFriendGpgId] = { + lastMsg: msg, + lastTime: nowSec, + }; + } m.redraw(); scrollChatToBottom(); } else { @@ -176,15 +371,30 @@ function scrollChatToBottom() { }, 100); } +function setOwnCustomStateString(statusString) { + const str = (statusString || '').trim(); + rs.rsJsonApiRequest('/rsChats/setCustomStateString', { status_string: str }, () => { + State.ownProfile.customState = str; + m.redraw(); + }).catch(() => { + State.ownProfile.customState = str; + m.redraw(); + }); +} + module.exports = { State, loadOwnProfile, + setOwnCustomStateString, loadSelectedOwnGxsDetails, fetchIdDetails, loadGxsIdentities, startDirectChat, getOnlineSslId, + preloadNetworkChatHistory, loadDirectChatMessages, + loadRecentDirectChatHistory, + loadAllDirectChatHistory, sendDirectChatMessage, scrollChatToBottom, }; diff --git a/webui-src/app/people/people_chat_tab.js b/webui-src/app/people/people_chat_tab.js index aba1ae9d..2f279626 100644 --- a/webui-src/app/people/people_chat_tab.js +++ b/webui-src/app/people/people_chat_tab.js @@ -105,7 +105,7 @@ const ChatTab = () => { style: 'padding: 0.5rem 1rem; background-color: #ffffff; border-bottom: 1px solid #cbd5e1; display: flex; align-items: center; justify-content: space-between; font-size: 0.85rem;', }, [ m('.chat-tunnel-status', { style: 'display: flex; align-items: center; gap: 0.5rem;' }, [ - m('span', { style: 'color: #64748b; font-weight: 500;' }, 'Distant Chat Tunnel'), + m('span.tunnel-label', { style: 'color: #64748b; font-weight: 500;' }, 'Distant Chat Tunnel'), m('i.fas.fa-circle', { style: { color: getStatusColor(State.distantChatStatus ? State.distantChatStatus.status : 0), @@ -117,7 +117,7 @@ const ChatTab = () => { ]), m('.chat-actions', { style: 'display: flex; align-items: center; gap: 0.75rem;' }, [ m('.select-own-profile', { style: 'display: flex; align-items: center; gap: 0.5rem;' }, [ - m('span', { style: 'color: #64748b;' }, 'Chatting as:'), + m('span.chatting-as-label', { style: 'color: #64748b;' }, 'Chatting as:'), (() => { const ownId = State.selectedOwnGxsIdForChat; if (ownId) fetchIdDetails(ownId); @@ -281,7 +281,7 @@ const ChatTab = () => { ]), m('textarea.chat-textarea', { - placeholder: canTalk ? 'Type your encrypted message here... (or paste image)' : 'Waiting for tunnel to be secured...', + placeholder: canTalk ? 'Type a message here...' : 'Waiting for tunnel to be secured...', disabled: !canTalk, value: State.chatInputMsg, style: 'flex: 1; resize: none; border: 1px solid #cbd5e1; border-radius: 6px; padding: 0.5rem; font-family: inherit; font-size: 0.9rem; outline: none; min-height: 40px; max-height: 120px;', diff --git a/webui-src/app/people/people_details_tab.js b/webui-src/app/people/people_details_tab.js index 8dd16570..4d560202 100644 --- a/webui-src/app/people/people_details_tab.js +++ b/webui-src/app/people/people_details_tab.js @@ -100,7 +100,7 @@ const DetailsTab = () => { }) ), }, - [m('i.fas.fa-edit'), ' Edit'] + [m('i.fas.fa-edit'), m('span.btn-text', ' Edit')] ), m( 'button.btn.red', @@ -113,7 +113,7 @@ const DetailsTab = () => { }) ), }, - [m('i.fas.fa-trash-alt'), ' Delete'] + [m('i.fas.fa-trash-alt'), m('span.btn-text', ' Delete')] ), ] : [ @@ -125,7 +125,7 @@ const DetailsTab = () => { initializeDistantChat(); }, }, - [m('i.fas.fa-comment-alt'), ' Start Chat'] + [m('i.fas.fa-comment-alt'), m('span.btn-text', ' Start Chat')] ), m( 'button.btn.blue', @@ -134,7 +134,7 @@ const DetailsTab = () => { State.showMailCompose = true; }, }, - [m('i.fas.fa-envelope'), ' Send Mail'] + [m('i.fas.fa-envelope'), m('span.btn-text', ' Send Mail')] ), m( 'button.btn' + (isContact ? '.red' : '.blue'), @@ -151,8 +151,8 @@ const DetailsTab = () => { }, }, isContact - ? [m('i.fas.fa-user-minus'), ' Remove Contact'] - : [m('i.fas.fa-user-plus'), ' Add Contact'] + ? [m('i.fas.fa-user-minus'), m('span.btn-text', ' Remove Contact')] + : [m('i.fas.fa-user-plus'), m('span.btn-text', ' Add Contact')] ), ], ]), diff --git a/webui-src/app/people/people_history.js b/webui-src/app/people/people_history.js index a5ba3b08..7ec9dfe8 100644 --- a/webui-src/app/people/people_history.js +++ b/webui-src/app/people/people_history.js @@ -5,6 +5,10 @@ const peopleState = require('people/people_state'); const HistoryBrowserModal = () => { return { oninit: (vnode) => { + if (vnode.attrs && vnode.attrs.state) { + vnode.attrs.state.historySearchQuery = ''; + return; + } const chatState = require('chat/chat_state'); const isRoom = vnode.attrs && vnode.attrs.isRoom; if (isRoom) { @@ -21,15 +25,16 @@ const HistoryBrowserModal = () => { view: (vnode) => { const chatState = require('chat/chat_state'); const isRoom = vnode.attrs && vnode.attrs.isRoom; - const stateObj = isRoom ? chatState.ChatHubState : peopleState.State; + const externalState = vnode.attrs && vnode.attrs.state; + const stateObj = externalState || (isRoom ? chatState.ChatHubState : peopleState.State); if (!stateObj.showHistoryModal) return null; - let name = 'Chat History'; - if (isRoom) { + let name = (vnode.attrs && vnode.attrs.name) || 'Chat History'; + if (!externalState && isRoom) { const lobby = chatState.ChatLobbyModel.currentLobby; name = lobby ? lobby.lobby_name : 'Chat Room'; - } else { + } else if (!externalState) { const details = peopleState.State.selectedId ? peopleState.State.gxsIdToDetailsMap[peopleState.State.selectedId] : null; name = details ? (details.mNickname || details.mGroupName || 'Contact') : 'Contact'; } @@ -99,7 +104,9 @@ const HistoryBrowserModal = () => { : filteredHistory.map((msg) => { const isIncoming = msg.incoming; let senderName = msg.peerName || (isIncoming ? name : 'You'); - if (!isIncoming) { + if (!isIncoming && externalState) { + senderName = (vnode.attrs && vnode.attrs.ownName) || 'You'; + } else if (!isIncoming) { const ownId = isRoom ? (chatState.ChatLobbyModel.currentLobby ? chatState.ChatLobbyModel.currentLobby.gxs_id : '') : peopleState.State.selectedOwnGxsIdForChat; senderName = rs.userList.username(ownId) || 'You'; } diff --git a/webui-src/app/people/people_sidebar.js b/webui-src/app/people/people_sidebar.js index e9de2db4..4f4ac841 100644 --- a/webui-src/app/people/people_sidebar.js +++ b/webui-src/app/people/people_sidebar.js @@ -327,66 +327,87 @@ const PeopleSidebar = () => { const menu = State.activeMenu; const isOwn = State.ownGxsIds.includes(menu.gxsId); - return m('.people-context-menu', { - style: { - top: `${menu.top}px`, - left: menu.left !== undefined ? `${menu.left}px` : '10px', - position: 'absolute', - zIndex: 1000, - }, - onclick: (e) => { - e.stopPropagation(); - }, - }, [ - !isOwn && m('.menu-item', { - onclick: () => { + return [ + m('.menu-backdrop', { + style: { + position: 'fixed', + inset: 0, + zIndex: 9998, + }, + onclick: (e) => { + e.preventDefault(); + e.stopPropagation(); State.activeMenu = null; - State.selectedId = menu.gxsId; - State.activeTab = 'chat'; - State.chatPid = null; - State.chatMessages = []; - initializeDistantChat(); m.redraw(); }, - }, [ - m('i.fas.fa-comments', { style: 'color: #3b82f6; margin-right: 0.5rem;' }), - 'Start chat', - ]), - !isOwn && m('.menu-item', { - onclick: () => { + oncontextmenu: (e) => { + e.preventDefault(); + e.stopPropagation(); State.activeMenu = null; - State.selectedId = menu.gxsId; - State.activeTab = 'details'; - State.showMailCompose = true; m.redraw(); }, - }, [ - m('i.fas.fa-envelope', { style: 'color: #10b981; margin-right: 0.5rem;' }), - 'Send mail', - ]), - !isOwn && m('.menu-item', { - onclick: () => { - State.activeMenu = null; - rs.rsJsonApiRequest( - '/rsIdentity/setAsRegularContact', - { id: menu.gxsId, isContact: !menu.isContact }, - (data, success) => { - if (success) { - loadGxsIdentities(); - } - } - ); + }), + m('.people-context-menu', { + style: { + top: `${menu.top}px`, + left: menu.left !== undefined ? `${menu.left}px` : '10px', + position: 'absolute', + zIndex: 9999, + }, + onclick: (e) => { + e.stopPropagation(); }, }, [ - m('i.fas' + (menu.isContact ? '.fa-user-minus' : '.fa-user-plus'), { - style: { - color: menu.isContact ? '#ef4444' : '#3b82f6', - marginRight: '0.5rem', + !isOwn && m('.menu-item', { + onclick: () => { + State.activeMenu = null; + State.selectedId = menu.gxsId; + State.activeTab = 'chat'; + State.chatPid = null; + State.chatMessages = []; + initializeDistantChat(); + m.redraw(); }, - }), - menu.isContact ? 'Remove from Contacts' : 'Add to Contacts', + }, [ + m('i.fas.fa-comments', { style: 'color: #3b82f6; margin-right: 0.5rem;' }), + 'Start chat', + ]), + !isOwn && m('.menu-item', { + onclick: () => { + State.activeMenu = null; + State.selectedId = menu.gxsId; + State.activeTab = 'details'; + State.showMailCompose = true; + m.redraw(); + }, + }, [ + m('i.fas.fa-envelope', { style: 'color: #10b981; margin-right: 0.5rem;' }), + 'Send mail', + ]), + !isOwn && m('.menu-item', { + onclick: () => { + State.activeMenu = null; + rs.rsJsonApiRequest( + '/rsIdentity/setAsRegularContact', + { id: menu.gxsId, isContact: !menu.isContact }, + (data, success) => { + if (success) { + loadGxsIdentities(); + } + } + ); + }, + }, [ + m('i.fas' + (menu.isContact ? '.fa-user-minus' : '.fa-user-plus'), { + style: { + color: menu.isContact ? '#ef4444' : '#3b82f6', + marginRight: '0.5rem', + }, + }), + menu.isContact ? 'Remove from Contacts' : 'Add to Contacts', + ]), ]), - ]); + ]; })(), ]), ]); diff --git a/webui-src/app/people/people_util.js b/webui-src/app/people/people_util.js index 46036cc9..c529a3f8 100644 --- a/webui-src/app/people/people_util.js +++ b/webui-src/app/people/people_util.js @@ -32,7 +32,12 @@ const UserAvatar = () => ({ style: { width: sizeStr, height: sizeStr, - borderRadius: isSquare ? '0' : '', + minWidth: sizeStr, + minHeight: sizeStr, + flexShrink: '0', + aspectRatio: '1', + objectFit: 'cover', + borderRadius: isSquare ? '0' : '50%', } }); } @@ -41,9 +46,15 @@ const UserAvatar = () => ({ const svgString = jdenticon.toSvg(identityId, pxSize); return m('div.jdenticon-avatar', { style: { - display: 'inline-block', + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', width: sizeStr, height: sizeStr, + minWidth: sizeStr, + minHeight: sizeStr, + flexShrink: '0', + aspectRatio: '1', borderRadius: isSquare ? '0' : '50%', overflow: 'hidden', verticalAlign: 'middle', @@ -67,8 +78,15 @@ const UserAvatar = () => ({ 'div.defaultAvatar', { style: { + display: 'inline-flex', + alignItems: 'center', + justifyContent: 'center', width: sizeStr, height: sizeStr, + minWidth: sizeStr, + minHeight: sizeStr, + flexShrink: '0', + aspectRatio: '1', borderRadius: isSquare ? '0' : '50%', backgroundColor, } @@ -112,22 +130,67 @@ function sortIds(list) { if (list !== undefined) { const result = [...list]; - result.sort((a, b) => rs.userList.username(a).localeCompare(rs.userList.username(b))); + result.sort((a, b) => { + const nameA = rs.userList.username(a) || String(a); + const nameB = rs.userList.username(b) || String(b); + return nameA.localeCompare(nameB); + }); return result; } return list; } +const OWN_IDS_CACHE_MS = 30000; +const ownIdsCache = { + all: { ids: null, loadedAt: 0, promise: null }, + signed: { ids: null, loadedAt: 0, promise: null }, +}; + +async function loadOwnIds(onlySigned) { + if (onlySigned) { + const response = await rs.rsJsonApiRequest('/rsIdentity/getOwnSignedIds', {}); + return (response && response.body && response.body.ids) || []; + } + + // The complete list is these two calls put together. /rsIdentity/getOwnIds + // is not an alternative to them: it is the deprecated one, it carries no + // @jsonapi annotation, and the core answers 404. + const [signedResponse, pseudonymousResponse] = await Promise.all([ + rs.rsJsonApiRequest('/rsIdentity/getOwnSignedIds', {}), + rs.rsJsonApiRequest('/rsIdentity/getOwnPseudonimousIds', {}), + ]); + const signedIds = (signedResponse && signedResponse.body && signedResponse.body.ids) || []; + const pseudonymousIds = (pseudonymousResponse && pseudonymousResponse.body && pseudonymousResponse.body.ids) || []; + return pseudonymousIds.concat(signedIds); +} + async function ownIds(consumer = () => { }, onlySigned = false) { - await rs.rsJsonApiRequest('/rsIdentity/getOwnSignedIds', {}, (owns) => { - if (onlySigned) { - consumer(sortIds(owns.ids)); - } else { - rs.rsJsonApiRequest('/rsIdentity/getOwnPseudonimousIds', {}, (pseudo) => { - if (pseudo.ids) consumer(sortIds(pseudo.ids.concat(owns.ids))); - }); + const cache = onlySigned ? ownIdsCache.signed : ownIdsCache.all; + try { + if (cache.ids && Date.now() - cache.loadedAt < OWN_IDS_CACHE_MS) { + const cachedIds = [...cache.ids]; + consumer(cachedIds); + return cachedIds; } - }); + + if (!cache.promise) { + cache.promise = loadOwnIds(onlySigned) + .then((ids) => { + cache.ids = sortIds(Array.from(new Set(ids || []))); + cache.loadedAt = Date.now(); + return cache.ids; + }) + .finally(() => { cache.promise = null; }); + } + + const ids = [...await cache.promise]; + consumer(ids); + return ids; + } catch (error) { + console.warn('Unable to load own identities', error); + consumer([]); + return []; + } } const SearchBar = () => { let searchString = ''; diff --git a/webui-src/app/scss/components/_buttons.scss b/webui-src/app/scss/components/_buttons.scss index 19a68cda..8ff25a77 100644 --- a/webui-src/app/scss/components/_buttons.scss +++ b/webui-src/app/scss/components/_buttons.scss @@ -5,8 +5,12 @@ button { @include button($primary-color); + white-space: nowrap !important; + flex-shrink: 0 !important; } button.red { @include button($red-color); + white-space: nowrap !important; + flex-shrink: 0 !important; } diff --git a/webui-src/app/scss/components/_media.scss b/webui-src/app/scss/components/_media.scss index a4ca5934..c9d4194f 100644 --- a/webui-src/app/scss/components/_media.scss +++ b/webui-src/app/scss/components/_media.scss @@ -21,4 +21,15 @@ &__desc { flex-basis: 60%; } + + @media (max-width: 768px) { + &__desc { + display: none !important; + } + + &__details { + flex-basis: 100% !important; + width: 100% !important; + } + } } \ No newline at end of file diff --git a/webui-src/app/scss/components/_navbar.scss b/webui-src/app/scss/components/_navbar.scss index ac388212..3a21eab7 100644 --- a/webui-src/app/scss/components/_navbar.scss +++ b/webui-src/app/scss/components/_navbar.scss @@ -145,6 +145,10 @@ } } +.sidebar-mobile-toggle, +.sidebar-drawer__title, +.sidebar-drawer__backdrop { display: none !important; } + .sidebarquickview { &>h6 { padding: 0.5rem; @@ -225,4 +229,98 @@ .sidebarquickview>h6 { display: none !important; } + + /* Boards, Channels and Forums use a drawer instead of the horizontal tabs. */ + .sidebar-drawer { + display: block; + position: relative; + width: 100%; + height: 44px; + z-index: 1000; + } + + .sidebar-mobile-toggle { + display: inline-flex !important; + width: 40px; + height: 40px; + align-items: center; + justify-content: center; + border: 0; + border-radius: 6px; + background: #ffffff; + color: #0f172a; + cursor: pointer; + font-size: 1.15rem; + } + + .sidebar-mobile-toggle:hover { background: #f1f5f9; } + + .sidebar-drawer .sidebar { + position: fixed !important; + top: 0; + left: 0; + display: flex !important; + flex-direction: column !important; + width: min(82vw, 300px) !important; + height: 100dvh !important; + padding: 1rem 0 !important; + overflow-y: auto !important; + overflow-x: hidden !important; + transform: translateX(-105%); + transition: transform 180ms ease; + border: 0 !important; + border-right: 1px solid #e2e8f0 !important; + background: #ffffff !important; + opacity: 1 !important; + pointer-events: auto !important; + box-shadow: 8px 0 24px rgba(15, 23, 42, 0.16); + white-space: normal !important; + z-index: 1002; + } + + .sidebar-drawer .sidebar.sidebar--mobile-open { transform: translateX(0); } + .sidebar-drawer .sidebar a { + display: block !important; + padding: .8rem 1.25rem !important; + border: 0 !important; + border-left: 4px solid transparent !important; + color: #334155 !important; + font-size: .95rem; + } + + .sidebar-drawer .sidebar .selected-sidebar-link { + border-left-color: #3ba4d7 !important; + border-bottom: 0 !important; + background: #f0f9ff; + color: #0f172a !important; + } + + .sidebar-drawer__title { + display: block !important; + margin: 0 1.25rem .65rem; + padding-bottom: .75rem; + border-bottom: 1px solid #e2e8f0; + color: #64748b; + font-size: .75rem; + font-weight: 700; + letter-spacing: .06em; + text-transform: uppercase; + } + + .sidebar-drawer__backdrop { + display: none !important; + position: fixed; + inset: 0; + background: rgba(15, 23, 42, .35); + /* The drawer itself owns navigation input; this layer is visual only. */ + pointer-events: none; + z-index: 1001; + } +} + +@media (min-width: 701px) { + .sidebar-drawer { display: contents; } + .sidebar-mobile-toggle, + .sidebar-drawer__title, + .sidebar-drawer__backdrop { display: none !important; } } diff --git a/webui-src/app/scss/components/_statusbar.scss b/webui-src/app/scss/components/_statusbar.scss index 0f88078c..d3e927cc 100644 --- a/webui-src/app/scss/components/_statusbar.scss +++ b/webui-src/app/scss/components/_statusbar.scss @@ -15,15 +15,15 @@ flex-shrink: 0; &-left { - @include flex($align: center); + @include flex($align: center, $gap: 0.75rem); } &-right { - @include flex($align: center, $gap: 1.5rem); + @include flex($align: center, $gap: 0.75rem); } &-item { - @include flex($align: center); + @include flex($align: center, $gap: 0.3rem); } &-divider { @@ -40,3 +40,27 @@ display: inline-block; box-shadow: 0 0 4px rgba(0, 0, 0, 0.5); } + +@media (max-width: 768px) { + .statusbar { + height: 22px !important; + padding: 0 0.4rem !important; + font-size: 0.72rem !important; + + .statusbar-left, + .statusbar-right { + gap: 0.35rem !important; + } + + .statusbar-label, + .statusbar-total-bytes, + .statusbar-extra-info { + display: none !important; + } + + .statusbar-divider { + height: 10px !important; + margin: 0 0.1rem !important; + } + } +} diff --git a/webui-src/app/scss/pages/_board.scss b/webui-src/app/scss/pages/_board.scss index 70c305d7..3b4b21e9 100644 --- a/webui-src/app/scss/pages/_board.scss +++ b/webui-src/app/scss/pages/_board.scss @@ -1,48 +1,1118 @@ -/* subject */ -table.boards th:nth-child(1) { - width: 50%; - text-align: start; -} -/* subject */ -table.boards td:nth-child(1) { - text-align: start; - /* Truncate text with '...' */ - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -table.boards tr:hover { - background-color: #eef3f6; +/* ========================================================= + Global Modal & Lightbox Overlay (#popupmessage) + ========================================================= */ + +#popupmessage { + position: fixed !important; + top: 0 !important; + left: 0 !important; + right: 0 !important; + bottom: 0 !important; + width: 100vw !important; + height: 100vh !important; + background-color: rgba(15, 23, 42, 0.75) !important; + backdrop-filter: blur(4px) !important; + z-index: 999999 !important; + display: none; + align-items: center !important; + justify-content: center !important; + box-sizing: border-box !important; +} + +.popup { + position: fixed !important; + top: 50% !important; + left: 50% !important; + transform: translate(-50%, -50%) !important; + z-index: 1000000 !important; + max-width: 90vw !important; + max-height: 90vh !important; + display: flex !important; + flex-direction: column !important; + box-sizing: border-box !important; +} + +.popup-content { + position: relative !important; + background: #ffffff !important; + border-radius: 12px !important; + padding: 1rem !important; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.4), 0 10px 10px -5px rgba(0, 0, 0, 0.2) !important; + max-width: 90vw !important; + max-height: 90vh !important; + overflow: auto !important; + box-sizing: border-box !important; +} + +.popup-content span.close { + position: absolute !important; + top: 0.5rem !important; + right: 0.75rem !important; + font-size: 1.75rem !important; + font-weight: 700 !important; + color: #64748b !important; + cursor: pointer !important; + line-height: 1 !important; + z-index: 10 !important; + transition: color 0.15s ease !important; +} + +.popup-content span.close:hover { + color: #ef4444 !important; +} + +.board-view-container { + display: flex !important; + flex-direction: column !important; + gap: 1rem !important; + width: 100% !important; + max-width: 100% !important; + overflow-x: hidden !important; + padding: 0.5rem 0 !important; + box-sizing: border-box !important; +} + +.board-table { + width: 100% !important; + border-collapse: collapse !important; +} + +.board-table th, +.board-table td { + text-align: left !important; + padding: 0.65rem 0.85rem !important; +} + +/* Toolbar Header Controls */ +.board-toolbar { + display: flex !important; + flex-direction: row !important; + align-items: center !important; + justify-content: space-between !important; + gap: 1rem !important; + width: 100% !important; + padding: 0.5rem 0.85rem !important; + background-color: #ffffff !important; + border: 1px solid #cbd5e1 !important; + border-radius: 8px !important; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05) !important; + box-sizing: border-box !important; +} + +.board-toolbar__left { + display: flex !important; + flex-direction: row !important; + align-items: center !important; + gap: 0.75rem !important; + flex: 1 !important; + min-width: 0 !important; +} + +.board-toolbar__right { + display: flex !important; + flex-direction: row !important; + align-items: center !important; + gap: 0.65rem !important; + flex-shrink: 0 !important; +} + +.board-toolbar__count-badge { + display: inline-flex !important; + align-items: center !important; + gap: 0.4rem !important; + padding: 0.3rem 0.65rem !important; + background-color: #f1f5f9 !important; + color: #475569 !important; + font-size: 0.825rem !important; + font-weight: 600 !important; + border-radius: 16px !important; + border: 1px solid #e2e8f0 !important; + white-space: nowrap !important; +} + +.board-toolbar__count-badge i { + color: #007bff !important; +} + +.board-toolbar__search { + position: relative !important; + display: flex !important; + align-items: center !important; + flex: 1 !important; + max-width: 380px !important; + min-width: 160px !important; +} + +.board-toolbar__search-icon { + position: absolute !important; + left: 0.75rem !important; + color: #94a3b8 !important; + font-size: 0.85rem !important; + pointer-events: none !important; +} + +.board-toolbar__search-input { + width: 100% !important; + padding: 0.35rem 0.65rem 0.35rem 2.1rem !important; + border: 1px solid #cbd5e1 !important; + border-radius: 6px !important; + font-size: 0.85rem !important; + background-color: #ffffff !important; + color: #1e293b !important; +} + +.board-toolbar__search-input:focus { + outline: none !important; + border-color: #007bff !important; + box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.15) !important; +} + +.board-toolbar__voter { + display: inline-flex !important; + align-items: center !important; + gap: 0.4rem !important; + color: #475569 !important; + font-size: 0.8rem !important; + font-weight: 600 !important; + white-space: nowrap !important; +} + +.board-toolbar__voter select { + max-width: 180px !important; + min-width: 110px !important; + padding: 0.3rem 0.45rem !important; + border: 1px solid #cbd5e1 !important; + border-radius: 6px !important; + background: #fff !important; + color: #1e293b !important; + font-size: 0.8rem !important; +} + +.board-toolbar__voter select:focus { + outline: none !important; + border-color: #007bff !important; + box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.15) !important; +} + +.board-toolbar__view-toggle { + display: inline-flex !important; + flex-direction: row !important; + align-items: center !important; + background-color: #f1f5f9 !important; + padding: 2px !important; + border-radius: 6px !important; + border: 1px solid #cbd5e1 !important; +} + +.board-toolbar__toggle-btn { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + gap: 0.35rem !important; + padding: 0.3rem 0.65rem !important; + border: none !important; + background: transparent !important; + color: #64748b !important; + font-size: 0.825rem !important; + font-weight: 500 !important; + border-radius: 4px !important; + cursor: pointer !important; + white-space: nowrap !important; + transition: all 0.2s ease !important; +} + +.board-toolbar__toggle-btn i { + font-size: 0.85rem !important; +} + +.board-toolbar__toggle-btn:hover { + color: #1e293b !important; + background-color: rgba(255, 255, 255, 0.6) !important; +} + +.board-toolbar__toggle-btn--active { + background-color: #007bff !important; + color: #ffffff !important; + font-weight: 600 !important; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) !important; +} + +.board-toolbar__toggle-btn--active i { + color: #ffffff !important; +} + +@media (max-width: 768px) { + .board-toolbar { + flex-wrap: wrap !important; + } + .board-toolbar__left { + flex-basis: 100% !important; + } + .board-toolbar__toggle-btn span { + display: none !important; + } + .board-toolbar__toggle-btn { + padding: 0.35rem 0.55rem !important; + } +} + +/* YouTube-style discussion thread on a board post. */ +.board-comments { + margin-top: 1.5rem; + max-width: 900px; + color: #0f172a; +} + +.board-comments__heading { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1.25rem; +} + +.board-comments__heading h3 { margin: 0; font-size: 1.15rem; } +.board-comments__heading span { color: #64748b; font-size: .8rem; font-weight: 600; } +.board-comments__heading i { margin-right: .35rem; } +.board-comments__voter { display: inline-flex; align-items: center; gap: .4rem; margin-left: auto; color: #64748b; font-size: .75rem; font-weight: 600; white-space: nowrap; } +.board-comments__voter select { max-width: 180px; padding: .25rem .4rem; font-size: .78rem; } + +.board-post-voting { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin: .85rem 0; + padding: .65rem .75rem; + background: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: .5rem; +} +.board-post-voting__identity { display: flex; align-items: center; gap: .5rem; color: #64748b; font-size: .8rem; font-weight: 600; } +.board-post-voting__identity select { max-width: 220px; padding: .3rem .45rem; font-size: .8rem; } +.board-post-voting__buttons { display: flex; align-items: center; gap: .35rem; } +.board-post-voting__buttons button { min-width: 52px; padding: .3rem .55rem; box-shadow: none; } +.board-post-voting__score { min-width: 2rem; color: #334155; font-weight: 700; text-align: center; } + +.board-comment-composer, +.board-comment { + display: flex; + gap: .8rem; +} + +.board-comment-avatar { + display: flex; + flex: 0 0 38px; + width: 38px; + height: 38px; + align-items: center; + justify-content: center; + border-radius: 50%; + background: linear-gradient(135deg, #2563eb, #7c3aed); + color: #fff; + font-size: .78rem; + font-weight: 700; +} + +.board-comment-composer__body, +.board-comment__content { min-width: 0; flex: 1; } +.board-comment-composer__identity { max-width: 240px; margin-bottom: .45rem; font-size: .8rem; } + +.board-comment-composer__input { + width: 100%; + min-height: 36px; + padding: .45rem 0; + resize: vertical; + border: 0; + border-bottom: 1px solid #94a3b8; + border-radius: 0; + background: transparent; + color: #0f172a; + font: inherit; + line-height: 1.4; + box-sizing: border-box; +} + +.board-comment-composer__input:focus { outline: 0; border-bottom: 2px solid #2563eb; } +.board-comment-composer__input:disabled { cursor: not-allowed; opacity: .6; } + +.board-comment-composer__actions, +.board-comment__actions { + display: flex; + align-items: center; + gap: .45rem; + margin-top: .55rem; +} + +.board-comment-composer__actions { justify-content: flex-end; } +.board-comment-composer__actions button, +.board-comment__actions button, +.board-comment__like, +.board-comment-composer__replying button { + border: 0; + background: transparent; + color: #475569; cursor: pointer; + font-size: .78rem; + font-weight: 700; } -table.boards tr.hidden { - display: none; +.board-comment-composer__submit { + padding: .45rem .85rem !important; + border-radius: 999px !important; + background: #2563eb !important; + color: #fff !important; } +.board-comment-composer__submit:disabled { background: #dbe3ef !important; color: #94a3b8 !important; cursor: not-allowed; } +.board-comment-composer__cancel:hover, +.board-comment__actions button:hover { color: #2563eb; } + +.board-comment-composer__replying { display: flex; align-items: center; gap: .25rem; margin-bottom: .35rem; color: #64748b; font-size: .8rem; } +.board-comment-composer__replying button { margin-left: .3rem; } +.board-comment-composer__hint, +.board-comment-composer__error { margin: .4rem 0 0; font-size: .78rem; } +.board-comment-composer__hint { color: #64748b; } +.board-comment-composer__error { color: #dc2626; } -#toggleunsub { +.board-comments__list { margin-top: 1.8rem; } +.board-comment { margin-top: 1.35rem; } +.board-comment__header { display: flex; align-items: center; justify-content: space-between; min-height: 18px; } +.board-comment__meta { display: flex; align-items: baseline; gap: .55rem; font-size: .8rem; } +.board-comment__meta b { color: #1e293b; } +.board-comment__meta span { color: #64748b; font-size: .75rem; } +.board-comment__menu { width: 28px; height: 28px; padding: 0; border: 0; border-radius: 50%; background: transparent; color: #0f172a; cursor: pointer; opacity: 0; } +.board-comment:hover .board-comment__menu, .board-comment__menu:focus { opacity: 1; } +.board-comment__menu:hover { background: #f1f5f9; } +.board-comment__text { margin: .2rem 0 0; white-space: pre-wrap; overflow-wrap: anywhere; line-height: 1.45; } +.board-comment__actions { gap: .65rem; margin-top: .35rem; } +.board-comment__actions button { padding: .25rem .2rem; color: #0f172a; } +.board-comment__like { padding: .25rem .35rem; color: #94a3b8; } +.board-comment__actions i { margin-right: .2rem; } +.board-comment__replies-toggle { position: relative; - background: gray; + margin-top: .3rem; + padding: .25rem .35rem; + border: 0; + background: transparent; + color: #2563eb; + cursor: pointer; + font-size: .78rem; + font-weight: 700; +} +.board-comment__replies-toggle:hover { background: #eff6ff; border-radius: 4px; } +.board-comment__replies-toggle i { margin-left: .15rem; } +.board-comment__replies-toggle::before { content: ''; position: absolute; left: -3.15rem; bottom: .8rem; width: 1.5rem; height: 2.2rem; border-left: 1px solid #e2e8f0; border-bottom: 1px solid #e2e8f0; border-radius: 0 0 0 .75rem; pointer-events: none; } +.board-comment__replies { margin-top: .2rem; padding-left: 1rem; border-left: 2px solid #e2e8f0; } +.board-comments__status, +.board-comments__empty { margin: 2rem 0; color: #64748b; text-align: center; } +.board-comments__empty i { font-size: 1.5rem; } + +@media (max-width: 560px) { + .board-comments__heading { flex-wrap: wrap; justify-content: space-between; gap: .5rem; } + .board-comments__voter { width: 100%; margin-left: 0; } + .board-comments__voter select { flex: 1; max-width: none; } + .board-post-voting { align-items: stretch; flex-direction: column; } + .board-post-voting__identity select { flex: 1; min-width: 0; max-width: none; } + .board-post-voting__buttons { justify-content: center; } + .board-comment-composer, .board-comment { gap: .6rem; } + .board-comment-avatar { flex-basis: 32px; width: 32px; height: 32px; font-size: .68rem; } + .board-comment__replies { padding-left: .6rem; } + .board-comment__replies-toggle::before { left: -2.65rem; width: 1.2rem; } + .board-card__notes-btn span { display: none; } + .board-card__notes-btn { width: 30px; height: 30px; justify-content: center; padding: 0 !important; } + .board-card__notes-btn i { margin: 0 !important; } +} + +/* Pagination Controls (< 1 - 25 >) on exact same level right next to toggle */ +.board-pagination { + display: inline-flex !important; + flex-direction: row !important; + align-items: center !important; + gap: 0.25rem !important; + background-color: #ffffff !important; + padding: 2px 4px !important; + border-radius: 6px !important; + border: 1px solid #cbd5e1 !important; +} + +.board-pagination__btn { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + width: 26px !important; + height: 26px !important; + min-width: 26px !important; + min-height: 26px !important; + padding: 0 !important; + margin: 0 !important; + border: 1px solid #cbd5e1 !important; + border-radius: 4px !important; + background: #ffffff !important; + color: #007bff !important; + font-size: 0.825rem !important; + cursor: pointer !important; + transition: all 0.15s ease !important; +} + +.board-pagination__btn i { + font-size: 0.825rem !important; + color: #007bff !important; +} + +.board-pagination__btn:hover:not(:disabled) { + background-color: #007bff !important; + color: #ffffff !important; + border-color: #007bff !important; +} + +.board-pagination__btn:hover:not(:disabled) i { + color: #ffffff !important; +} + +.board-pagination__btn:disabled { + opacity: 0.4 !important; + cursor: not-allowed !important; + color: #94a3b8 !important; + border-color: #e2e8f0 !important; + background-color: #f1f5f9 !important; +} + +.board-pagination__btn:disabled i { + color: #94a3b8 !important; +} + +.board-pagination__label { + font-size: 0.825rem !important; + font-weight: 700 !important; + color: #334155 !important; + padding: 0 0.35rem !important; + white-space: nowrap !important; + user-select: none !important; +} + +/* Bottom Pagination Container */ +.board-view-footer { + display: flex !important; + justify-content: center !important; + align-items: center !important; + padding: 1rem 0 0.5rem 0 !important; + width: 100% !important; +} + +/* Board Grid Layout Modes */ +.board-grid { + display: flex !important; + flex-direction: column !important; + width: 100% !important; + box-sizing: border-box !important; +} + +.board-grid--compact { + display: flex !important; + flex-direction: column !important; + gap: 0.5rem !important; + width: 100% !important; +} + +.board-grid--card { + display: grid !important; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)) !important; + gap: 1.25rem !important; + width: 100% !important; +} + +.board-grid__empty { + display: flex !important; + flex-direction: column !important; + align-items: center !important; + justify-content: center !important; + padding: 4rem 2rem !important; + text-align: center !important; + background: #ffffff !important; + border: 2px dashed #cbd5e1 !important; + border-radius: 12px !important; + width: 100% !important; + box-sizing: border-box !important; +} + +.board-grid__empty-icon { + font-size: 3rem !important; + color: #cbd5e1 !important; + margin-bottom: 1rem !important; +} + +.board-grid__empty-title { + font-size: 1.15rem !important; + font-weight: 600 !important; + color: #475569 !important; + margin: 0 0 0.5rem 0 !important; +} + +.board-grid__empty-desc { + font-size: 0.9rem !important; + color: #94a3b8 !important; + margin: 0 !important; +} + +/* =================================================== + COMPACT VIEW MODE: FULL-WIDTH HORIZONTAL ROW BANNER + (Identical to Retroshare C++ Qt GUI Posted design) + =================================================== */ +.board-card { + box-sizing: border-box !important; +} + +.board-card--compact { + display: flex !important; + flex-direction: row !important; + align-items: center !important; + width: 100% !important; + min-height: 70px !important; + padding: 0.45rem 0.75rem !important; + background-color: #eef2f5 !important; + border: 1px solid #d1d5db !important; + border-radius: 4px !important; + gap: 0.75rem !important; + box-sizing: border-box !important; + margin-bottom: 0.35rem !important; +} + +.board-card--compact:hover { + background-color: #e2e8f0 !important; + border-color: #9ca3af !important; +} + +/* Vote Pill (Matching Comments Button style with normal-sized icons) */ +.board-card__vote-pill { + display: inline-flex !important; + align-items: center !important; + gap: 0.4rem !important; + padding: 0.2rem 0.55rem !important; + background-color: #f1f5f9 !important; + border: 1px solid #cbd5e1 !important; + border-radius: 20px !important; + box-sizing: border-box !important; +} + +button.board-card__vote-btn, +.board-card__vote-pill .board-card__vote-btn { + background: transparent !important; + border: none !important; + box-shadow: none !important; + padding: 0.1rem 0.2rem !important; + margin: 0 !important; + cursor: pointer !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + line-height: 1 !important; + border-radius: 4px !important; + transition: background-color 0.15s ease !important; + outline: none !important; +} + +.board-card__vote-pill .board-card__vote-btn:hover { + background-color: #e2e8f0 !important; + box-shadow: none !important; +} + +.board-card__vote-pill .board-card__vote-btn--up i { + color: #16a34a !important; + font-size: 1.15rem !important; +} + +.board-card__vote-pill .board-card__vote-btn--down i { + color: #dc2626 !important; + font-size: 1.15rem !important; +} + +.board-card__vote-pill .board-card__vote-score { + font-size: 0.9rem !important; + font-weight: 700 !important; + color: #1e293b !important; + padding: 0 0.15rem !important; + line-height: 1 !important; + min-width: 1rem !important; + text-align: center !important; +} + +/* Thumbnail Image Container */ +.board-card--compact .board-card__image-container { + width: 110px !important; + height: 62px !important; + flex-shrink: 0 !important; + border-radius: 4px !important; + overflow: hidden !important; + background-color: #cbd5e1 !important; +} + +.board-card--compact .board-card__image { + width: 100% !important; + height: 100% !important; + object-fit: cover !important; + display: block !important; +} + +.board-card--compact .board-card__placeholder-wrapper { + width: 100% !important; + height: 100% !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + background: linear-gradient(135deg, #e2e8f0 0%, #cbd5e1 100%) !important; +} + +.board-card__placeholder-content { + display: flex; + flex-direction: column; + align-items: center; + gap: .3rem; + color: #64748b; + font-size: .72rem; + font-weight: 600; +} +.board-card__placeholder-content i { font-size: 1.35rem; } + +.board-card--compact .board-card__placeholder-img { + width: 24px !important; + height: 24px !important; + color: #64748b !important; +} + +/* Main Details Section */ +.board-card--compact .board-card__content { + display: flex !important; + flex-direction: column !important; + justify-content: center !important; + flex: 1 !important; + min-width: 0 !important; + padding: 0 !important; +} + +/* Title: Blue bold underlined italicized link (Matches Qt GUI) */ +.board-card--compact .board-card__title { + font-size: 1.05rem !important; + font-weight: 700 !important; + color: #2255aa !important; + text-decoration: underline !important; + font-style: italic !important; + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + margin: 0 0 0.15rem 0 !important; + cursor: pointer !important; +} + +.board-card--compact .board-card__title:hover { + color: #1d4ed8 !important; +} + +/* Meta line: Posted by */ +.board-card--compact .board-card__meta { + font-size: 0.8rem !important; + color: #475569 !important; + margin-bottom: 0.2rem !important; +} + +.board-card--compact .board-card__meta b { + color: #1e293b !important; +} + +/* Footer: Comment button matching Qt GUI */ +.board-card--compact .board-card__footer { + display: flex !important; + align-items: center !important; + gap: 0.5rem !important; + padding: 0 !important; + border: none !important; + margin: 0 !important; +} + +button.board-card__comments-btn, +.board-card__comments-btn, +.board-card--compact .board-card__comments-btn { + display: inline-flex !important; + align-items: center !important; + gap: 0.35rem !important; + padding: 0.2rem 0.5rem !important; + background: transparent !important; + border: none !important; + box-shadow: none !important; + color: #64748b !important; + font-size: 0.85rem !important; + font-weight: 500 !important; + cursor: pointer !important; + outline: none !important; +} + +button.board-card__comments-btn:hover, +.board-card__comments-btn:hover, +.board-card--compact .board-card__comments-btn:hover { + color: #007bff !important; + text-decoration: underline !important; + box-shadow: none !important; +} + +button.board-card__notes-btn, +.board-card__notes-btn { + display: inline-flex !important; + align-items: center !important; + gap: 0.35rem !important; + padding: 0.2rem 0.5rem !important; + background: transparent !important; + border: none !important; + box-shadow: none !important; + color: #64748b !important; + font-size: 0.85rem !important; + font-weight: 500 !important; + cursor: pointer !important; +} + +.board-card__notes-btn:hover { color: #007bff !important; text-decoration: underline !important; } + +/* =================================================== + CARD VIEW MODE: ELEVATED GRID CARD + =================================================== */ +.board-card--card { + display: flex !important; + flex-direction: column !important; + background-color: #ffffff !important; + border: 1px solid #e2e8f0 !important; + border-radius: 10px !important; + overflow: hidden !important; + transition: transform 0.2s ease, box-shadow 0.2s ease !important; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04) !important; +} + +.board-card--card:hover { + transform: translateY(-3px) !important; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.08) !important; + border-color: #cbd5e1 !important; +} + +.board-card--card .board-card__vote-col { + display: none !important; +} + +.board-card--card .board-card__image-container { + width: 100% !important; + height: 170px !important; + overflow: hidden !important; + background-color: #f1f5f9 !important; +} + +.board-card--card .board-card__image { + width: 100% !important; + height: 100% !important; + object-fit: cover !important; + display: block !important; +} + +.board-card--card .board-card__placeholder-wrapper { + width: 100% !important; + height: 100% !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%) !important; +} + +.board-card--card .board-card__placeholder-img { + width: 36px !important; + height: 36px !important; + color: #94a3b8 !important; +} + +.board-card--card .board-card__content { + display: flex !important; + flex-direction: column !important; + flex: 1 !important; + padding: 1rem !important; +} + +.board-card--card .board-card__title { + font-size: 1.05rem !important; + font-weight: 700 !important; + color: #0f172a !important; + margin: 0 0 0.4rem 0 !important; + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + cursor: pointer !important; +} + +.board-card--card .board-card__title:hover { + color: #007bff !important; +} + +.board-card--card .board-card__meta { + font-size: 0.8rem !important; + color: #64748b !important; + margin-bottom: 0.5rem !important; +} + +.board-card--card .board-card__notes-wrapper { + display: flex !important; + flex-direction: column !important; + gap: 0.35rem !important; + margin-bottom: 0.75rem !important; +} + +.board-card--card .board-card__notes { + font-size: 0.85rem !important; + line-height: 1.45 !important; + color: #475569 !important; + background-color: #f8fafc !important; + border-left: 3px solid #cbd5e1 !important; + padding: 0.4rem 0.6rem !important; + word-break: break-word !important; +} + +.board-card--card .board-card__notes--clamped { + display: -webkit-box !important; + -webkit-line-clamp: 3 !important; + -webkit-box-orient: vertical !important; + overflow: hidden !important; + white-space: pre-line !important; +} + +.board-card--card .board-card__notes--expanded { + display: block !important; + white-space: pre-line !important; +} + +.board-card--card .board-card__notes-toggle { + align-self: flex-start !important; + background: none !important; + border: none !important; + padding: 0.1rem 0.3rem !important; + color: #007bff !important; + font-size: 0.775rem !important; + font-weight: 600 !important; + cursor: pointer !important; +} + +.board-card--card .board-card__notes-toggle:hover { + text-decoration: underline !important; +} + +.board-card--card .board-card__footer { + display: flex !important; + align-items: center !important; + justify-content: flex-end !important; + margin-top: auto !important; + padding-top: 0.65rem !important; + border-top: 1px solid #f1f5f9 !important; +} + +.board-card--card .board-card__comments-btn { + display: inline-flex !important; + align-items: center !important; + gap: 0.4rem !important; + padding: 0.3rem 0.6rem !important; + background-color: #f1f5f9 !important; + color: #475569 !important; + border: 1px solid #e2e8f0 !important; + border-radius: 6px !important; + font-size: 0.8rem !important; + font-weight: 500 !important; + cursor: pointer !important; +} + +.board-card--card .board-card__comments-btn:hover { + background-color: #007bff !important; + color: #ffffff !important; + border-color: #007bff !important; +} + +.board-card--card .board-card__comments-btn:hover i { + color: #ffffff !important; +} + +.board-card--card .board-card__notes-btn { + display: inline-flex !important; + align-items: center !important; + gap: 0.4rem !important; + padding: 0.3rem 0.6rem !important; + background-color: #f8fafc !important; + color: #475569 !important; + border: 1px solid #e2e8f0 !important; + border-radius: 6px !important; + font-size: 0.8rem !important; + font-weight: 500 !important; +} + +.board-card--card .board-card__notes-btn:hover { background-color: #e0f2fe !important; color: #0369a1 !important; border-color: #7dd3fc !important; text-decoration: none !important; } + +.board-notes-dialog { min-width: min(560px, 75vw); max-width: 75vw; } +.board-notes-dialog h3 { margin: 0 2rem .8rem 0; color: #0f172a; } +.board-notes-dialog__label { margin: 0 0 .35rem; color: #64748b; font-size: .78rem; font-weight: 700; text-transform: uppercase; } +.board-notes-dialog__content { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; color: #1e293b; line-height: 1.55; } + +/* ========================================================= + PhotoView Lightbox Modal Styles (Matching Qt GUI PhotoView) + ========================================================= */ + +/* Fullscreen overlay element appended to body by openPhotoModal() */ +#photo-view-overlay { + display: none; + position: fixed; + inset: 0; + z-index: 999999; + background-color: rgba(0, 0, 0, 0.85); + align-items: center; + justify-content: center; +} + +.photo-view-dialog { + background-color: #f8fafc !important; + border-radius: 8px !important; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5) !important; + display: flex !important; + flex-direction: column !important; + max-width: 90vw !important; + max-height: 90vh !important; + width: 820px !important; + overflow: hidden !important; + position: relative !important; + z-index: 1 !important; +} + +.photo-view-header { + display: flex !important; + align-items: center !important; + justify-content: space-between !important; + padding: 0.75rem 1rem !important; + background-color: #ffffff !important; + border-bottom: 1px solid #e2e8f0 !important; +} + +.photo-view-title { + font-size: 1.05rem !important; + font-style: italic !important; + font-weight: 700 !important; + color: #1e293b !important; + margin: 0 !important; + flex: 1 !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; +} + +.photo-view-close-btn { + background: none !important; + border: none !important; + font-size: 1.6rem !important; + line-height: 1 !important; + color: #64748b !important; + cursor: pointer !important; + padding: 0 0.4rem !important; +} + +.photo-view-close-btn:hover { + color: #ef4444 !important; +} + +.photo-view-body { + display: flex !important; + flex-direction: row !important; + align-items: stretch !important; + background-color: #0f172a !important; + flex: 1 !important; + min-height: 380px !important; + max-height: 68vh !important; + overflow: hidden !important; +} + +/* Left/right nav columns (fixed 56px wide, vertically center the button) */ +.photo-view-nav-col { + width: 56px !important; + flex-shrink: 0 !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + background-color: rgba(0, 0, 0, 0.25) !important; +} + +/* Image wrapper fills remaining space */ +.photo-view-img-wrap { + flex: 1 !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + min-width: 0 !important; + padding: 0.75rem !important; +} + +.photo-view-img { + max-width: 100% !important; + max-height: 65vh !important; + object-fit: contain !important; + display: block !important; + border-radius: 4px !important; +} + +.photo-view-no-img { + color: #94a3b8 !important; + font-size: 0.95rem !important; +} + +/* Nav button — static in flex column, no absolute positioning */ +.photo-view-nav-btn { + width: 38px !important; + height: 38px !important; + background-color: #ffffff !important; + border: 1px solid #cbd5e1 !important; + border-radius: 6px !important; + color: #007bff !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + cursor: pointer !important; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3) !important; + transition: all 0.15s ease !important; + flex-shrink: 0 !important; +} + +.photo-view-nav-btn i { + font-size: 1rem !important; + color: #007bff !important; +} + +.photo-view-nav-btn:hover { + background-color: #007bff !important; + color: #ffffff !important; + border-color: #007bff !important; +} + +.photo-view-nav-btn:hover i { + color: #ffffff !important; +} + +.photo-view-footer { + display: flex !important; + align-items: center !important; + justify-content: space-between !important; + padding: 0.75rem 1rem !important; + background-color: #ffffff !important; + border-top: 1px solid #e2e8f0 !important; } -#options { - width: 100px; - text-align: center; - font-size: medium; - margin-left: 20px; - height: 40px; +.photo-view-meta { + font-size: 0.875rem !important; + color: #475569 !important; } -#composepopup { - height: 80%; - width: 70%; +.photo-view-meta b { + color: #0f172a !important; } -#mtags { - width: 160px; - text-align: center; - font-size: medium; - margin-left: 10px; - height: 40px; +/* Responsive: Hide board header description on mobile screens */ +@media (max-width: 768px) { + .media-item__desc { + display: none !important; + } + .media-item__details { + flex-basis: 100% !important; + width: 100% !important; + } } diff --git a/webui-src/app/scss/pages/_channel.scss b/webui-src/app/scss/pages/_channel.scss index 962f99db..0179a898 100644 --- a/webui-src/app/scss/pages/_channel.scss +++ b/webui-src/app/scss/pages/_channel.scss @@ -78,6 +78,116 @@ table { } } } + +.posts-container-card .channel-post__placeholder { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: .35rem; + color: #64748b; + background: linear-gradient(135deg, #f8fafc, #dbe5f1); +} +.channel-post__placeholder i { font-size: 1.35rem; color: #64748b; } +.channel-post__placeholder span { font-size: 2rem; font-weight: 700; color: #2563eb; } +.channel-post__placeholder small { font-size: .72rem; font-weight: 600; } + +/* Compact YouTube-style post description with an opt-in expansion control. */ +.post-description { + margin: 1rem 0; + padding: .85rem 1rem; + border-radius: 10px; + background: #f1f5f9; + color: #1e293b; +} +.post-description__text { overflow-wrap: anywhere; line-height: 1.5; } +.post-description__text > :first-child { margin-top: 0; } +.post-description__text > :last-child { margin-bottom: 0; } +.post-description__text--collapsed { + display: -webkit-box; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 3; +} +.post-description__toggle { + margin-top: .35rem; + padding: 0 !important; + border: 0 !important; + box-shadow: none !important; + background: transparent !important; + color: #0f172a !important; + font-size: .85rem !important; + font-weight: 700 !important; +} +.post-description__toggle:hover { color: #2563eb !important; text-decoration: underline; } + +/* Keep image and generated-thumbnail cards the same size instead of stretching + * a sparse grid row to the full height of the Posts panel. */ +.posts-container { align-content: start; grid-auto-rows: 240px; } +.posts-container-card { height: 240px; min-height: 0; overflow: hidden; } +.posts-container-card > img, +.posts-container-card > .channel-post__placeholder { min-height: 0; } + +@media (max-width: 700px) { + .posts-container { grid-auto-rows: 210px; gap: 1rem; } + .posts-container-card { height: 210px; } +} + +/* Channel attachment table -> compact cards on phones. */ +@media (max-width: 700px) { + .file-section { margin-top: 1.25rem; } + + table.channel-files, + table.channel-files tbody, + table.channel-files tr, + table.channel-files td { + display: block; + width: 100% !important; + box-sizing: border-box; + } + + table.channel-files { padding: 0; table-layout: auto; } + table.channel-files thead { display: none; } + + table.channel-files tr { + margin: 0 0 .7rem; + padding: .7rem; + border: 1px solid #dbe3ef; + border-radius: 8px; + background: #fff; + } + + table.channel-files td { + min-width: 0; + padding: .25rem 0; + border: 0; + text-align: left; + } + + table.channel-files td::before { + display: block; + margin-bottom: .1rem; + color: #64748b; + content: attr(data-label); + font-size: .72rem; + font-weight: 700; + text-transform: uppercase; + } + + table.channel-files .channel-file__name { + overflow-wrap: anywhere; + color: #0f172a; + font-weight: 600; + line-height: 1.35; + } + + table.channel-files .channel-file__size { color: #475569; } + table.channel-files .channel-file__action { padding-top: .5rem; } + table.channel-files .channel-file__action > button { min-width: 116px; } + table.channel-files .channel-file__action .file-view { margin-top: .6rem; } +} /* #options{ width: 100px; text-align: center; diff --git a/webui-src/app/scss/pages/_chat.scss b/webui-src/app/scss/pages/_chat.scss index 6ac65bd9..94a2fc22 100644 --- a/webui-src/app/scss/pages/_chat.scss +++ b/webui-src/app/scss/pages/_chat.scss @@ -1,1617 +1,2038 @@ -@use '../abstracts' as *; - -.lobby { - margin: 10px; - border: 1px solid #aaa; - border-radius: 20px; -} - -.lobby .mainname { - margin: 20px; - font-weight: 100; - font-size: 1.2em; -} - -.topic { - color: #666; -} - -.lobby>.topic { - font-size: 0.95em; - margin-left: 25px; - margin-bottom: 5px; -} - -.lefttitle { - margin-top: 15px; - margin-bottom: 0; - font-weight: 100; - font-size: 1.2em; -} - -.leftname { - margin-top: 5px; - margin-bottom: 5px; - padding: 5px; - font-weight: 100; - font-size: 1em; -} - -.leftlobby>.topic { - font-size: 0.75em; - margin-left: 15px; - margin-bottom: 5px; -} - -.subscribed, -.public { - cursor: pointer; -} - -.leftlobby { - border: 1px solid #aaa; - border-radius: 10px; - margin-top: 5px; - background-color: white; -} - -.leftlobby.selected-lobby, -.selectedidentity { - color: white; - background-color: #3ba4d7; -} - -.rightbar { - position: absolute; - width: 185px; - background-color: white; - overflow: auto; - top: 130px; - bottom: 15px; - right: 15px; -} - -.user { - padding: 5px; -} - -.lobbyName { - padding: 15px; - margin-top: 2rem; -} - -.lobbies { - position: absolute; - width: 185px; - left: 165px; - bottom: 15px; - top: 130px; - overflow: auto; -} - -.messages, -.setup { - position: absolute; - background-color: white; - top: 130px; - left: 360px; - right: 215px; - overflow: auto; -} - -.messages { - bottom: 115px; -} - -.messagetext { - white-space: break-spaces; - margin-right: 5px; -} - -.message>* { - margin-left: 5px; -} - -.username { - color: darkgreen; - font-weight: bolder; -} - -.chatMessage { - position: absolute; - background-color: white; - height: 85px; - bottom: 15px; - right: 215px; - left: 360px; -} - -textarea.chatMsg { - height: 100%; - width: 100%; -} - -.chatatchar { - margin-left: 0.2em; - margin-right: 0.2em; - color: silver; -} - -.setupicon { - margin-left: 1em; - cursor: pointer; -} - -.leaveicon { - margin-left: 1em; - cursor: pointer; - color: #d40000; -} - -.selectidentity { - margin: 15px; - font-size: 1.2em; -} - -.setup>.identity { - cursor: pointer; -} - -.setup { - bottom: 15px; -} - -.createDistantChat { - margin-top: 1em; -} - -.no-lobbies { - - .messages, - .chatMessage, - .setup { - left: 165px; - } -} - -/* CHAT ROOM (Single Chat) - Desktop Grid Layout */ -@media (min-width: 900px) { - .node-panel.chat-room { - display: grid !important; - grid-template-columns: 250px 1fr 200px !important; - /* Lobbies, Chat, Users */ - grid-template-rows: auto 1fr auto !important; - /* Header, Messages, Input */ - grid-template-areas: - "lobbies header rightbar" - "lobbies messages rightbar" - "lobbies input rightbar" !important; - padding: 0 !important; - height: 100% !important; - } - - .node-panel.chat-room .lobbyName { - grid-area: header; - padding: 10px; - border-bottom: 1px solid #eee; - margin: 0; - z-index: 10; - background: white; - } - - .node-panel.chat-room .lobbies { - grid-area: lobbies; - position: static !important; - width: auto !important; - height: auto !important; - border-right: 1px solid #ccc; - overflow-y: auto; - display: block !important; - top: auto !important; - bottom: auto !important; - left: auto !important; - } - - .node-panel.chat-room .messages { - grid-area: messages; - position: static !important; - width: auto !important; - height: auto !important; - overflow-y: auto; - padding: 10px; - left: auto !important; - right: auto !important; - top: auto !important; - bottom: auto !important; - margin: 0 !important; - } - - .node-panel.chat-room .rightbar { - grid-area: rightbar; - position: static !important; - width: auto !important; - border-left: 1px solid #ccc; - overflow-y: auto; - display: block !important; - } - - .node-panel.chat-room .chatMessage { - grid-area: input; - position: static !important; - width: auto !important; - height: auto !important; - border-top: 1px solid #eee; - left: auto !important; - right: auto !important; - bottom: auto !important; - flex: 0 0 auto; - padding: 10px !important; - background: white; - z-index: 10; - } -} - -/* Mobile Overrides - Ensure Flex Column */ -@media (max-width: 899px) { - .node-panel.chat-room { - display: flex !important; - flex-direction: column !important; - height: 100% !important; - position: relative !important; - } - - .node-panel.chat-room .lobbyName { - flex: 0 0 auto; - } - - .node-panel.chat-room .messages { - flex: 1 !important; - overflow-y: auto !important; - position: relative !important; - top: 0 !important; - bottom: 0 !important; - left: 0 !important; - right: 0 !important; - width: 100% !important; - height: auto !important; - margin: 0 !important; - } - - .node-panel.chat-room .chatMessage { - flex: 0 0 auto !important; - position: relative !important; - bottom: 0 !important; - left: 0 !important; - right: 0 !important; - width: 100% !important; - height: auto !important; - z-index: 100; - } - - .node-panel.chat-room .rightbar, - .node-panel.chat-room .lobbies { - display: none !important; - position: fixed !important; - top: 60px !important; - bottom: 0 !important; - width: 80% !important; - background: white !important; - z-index: 200 !important; - box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2) !important; - } - - .node-panel.chat-room.show-lobbies .lobbies { - display: block !important; - left: 0 !important; - } - - .node-panel.chat-room.show-users .rightbar { - display: block !important; - right: 0 !important; - } - - .chat-overlay { - display: none; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.4); - z-index: 150; - } - - .show-lobbies .chat-overlay, - .show-users .chat-overlay { - display: block; - } - - /* Mobile Icons in Header */ - .mobile-menu-icons { - display: flex; - gap: 15px; - font-size: 1.2rem; - } - - .mobile-menu-icons i { - cursor: pointer; - padding: 5px; - } -} - -@media (min-width: 900px) { - .mobile-menu-icons { - display: none; - } -} - -/* ===================================================== - CHAT HUB - Two-Pane Layout (matching Network page) - ===================================================== */ - -.chat-hub-container { - display: flex; - height: 100%; - width: 100%; - overflow: hidden; - background-color: #f1f5f9; -} - -.chat-hub-left-pane { - width: 320px; - min-width: 300px; - max-width: 350px; - border-right: 1px solid #cbd5e1; - display: flex; - flex-direction: column; - background: #ffffff; - box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05); -} - -.chat-own-profile-card { - padding: 1.25rem; - border-bottom: 1px solid #e2e8f0; - background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); - position: relative; // anchors the absolutely-positioned .chat-create-lobby-btn - - .profile-header { - display: flex; - align-items: center; - gap: 1rem; - } - - .profile-info { - display: flex; - flex-direction: column; - flex: 1; - overflow: hidden; - - .profile-name { - font-weight: 700; - color: #1e293b; - font-size: 1.1rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .profile-status { - font-size: 0.85rem; - color: #10b981; - font-weight: 500; - display: flex; - align-items: center; - gap: 0.35rem; - - &::before { - content: ''; - display: inline-block; - width: 8px; - height: 8px; - background-color: #10b981; - border-radius: 50%; - } - } - } -} - -.chat-rooms-list-container { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - - .searchbar-container { - padding: 0.75rem 1rem; - border-bottom: 1px solid #e2e8f0; - - input.searchbar { - width: 100%; - padding: 0.5rem 0.75rem; - font-size: 0.9rem; - border: 1px solid #cbd5e1; - border-radius: 0.375rem; - background-color: #f8fafc; - outline: none; - transition: all 0.2s; - - &:focus { - background-color: #ffffff; - border-color: #3ba4d7; - box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); - } - } - } - - .rooms-scroll { - flex: 1; - overflow-y: auto; - padding: 0.5rem 0; - } -} - -.rooms-section-title { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1rem 0.375rem; - font-size: 0.75rem; - font-weight: 700; - color: #64748b; - text-transform: uppercase; - letter-spacing: 0.05em; - - i { - font-size: 0.7rem; - color: #94a3b8; - } -} - -.chat-room-list-item { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem 1rem; - margin: 0.125rem 0.5rem; - border-radius: 0.5rem; - cursor: pointer; - transition: all 0.2s; - - &:hover { - background-color: #f1f5f9; - } - - &.selected { - background-color: #e0f2fe; - - .room-name { - color: #0369a1; - font-weight: 600; - } - } - - .room-icon { - flex-shrink: 0; - width: 36px; - height: 36px; - border-radius: 0.5rem; - background: linear-gradient(135deg, #3ba4d7, #0ea5e9); - display: flex; - align-items: center; - justify-content: center; - color: #ffffff; - font-size: 1.35rem; - } - - &.public-room .room-icon { - background: linear-gradient(135deg, #10b981, #059669); - } - - .room-meta { - flex: 1; - min-width: 0; - - .room-name { - font-size: 0.95rem; - color: #334155; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - transition: color 0.2s; - } - - .room-topic { - font-size: 0.8rem; - color: #94a3b8; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } - - .room-badge { - flex-shrink: 0; - min-width: 24px; - height: 24px; - border-radius: 12px; - background-color: #e2e8f0; - color: #475569; - font-size: 0.75rem; - font-weight: 700; - display: flex; - align-items: center; - justify-content: center; - padding: 0 0.375rem; - } -} - -.chat-hub-right-pane { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - background-color: #f8fafc; -} - -.chat-pane-placeholder { - flex: 1; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - color: #94a3b8; - gap: 1rem; - padding: 2rem; - text-align: center; - - i { - font-size: 4rem; - color: #cbd5e1; - } - - p { - font-size: 1.1rem; - max-width: 400px; - } -} - -.chat-hub-tab-content { - flex: 1; - overflow-y: auto; - padding: 1.5rem; -} - -.chat-room-detail-view { - display: flex; - flex-direction: column; - gap: 1.5rem; - - .detail-header { - display: flex; - align-items: flex-start; - gap: 1.5rem; - padding-bottom: 1.5rem; - border-bottom: 1px solid #e2e8f0; - flex-wrap: wrap; - - .detail-title { - flex: 1; - min-width: 200px; - - h2 { - font-size: 1.75rem; - font-weight: 800; - color: #1e293b; - margin-bottom: 0.25rem; - } - - .detail-subtitle { - font-size: 0.9rem; - color: #64748b; - display: flex; - align-items: center; - gap: 0.5rem; - } - } - - .detail-actions { - display: flex; - gap: 0.75rem; - flex-wrap: wrap; - - button { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - font-size: 0.9rem; - } - } - } - - .detail-section { - background-color: #ffffff; - border-radius: 0.5rem; - border: 1px solid #e2e8f0; - padding: 1.25rem; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); - - h3 { - font-size: 1.1rem; - font-weight: 700; - color: #334155; - margin-bottom: 1rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid #f1f5f9; - } - - .info-grid { - display: grid; - grid-template-columns: 130px 1fr; - row-gap: 0.75rem; - font-size: 0.9rem; - - .info-label { - font-weight: 600; - color: #64748b; - } - - .info-value { - color: #1e293b; - word-break: break-all; - } - } - } -} - -.participants-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); - gap: 0.5rem; -} - -.participant-card { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 0.75rem; - background-color: #f8fafc; - border: 1px solid #e2e8f0; - border-radius: 0.375rem; - - .participant-name { - font-size: 0.875rem; - color: #334155; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } -} - -.no-participants { - color: #94a3b8; - font-size: 0.9rem; - font-style: italic; -} - -.detail-actions-footer { - display: flex; - gap: 0.75rem; - - button { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.5rem 1rem; - font-size: 0.9rem; - } -} - -.join-description { - color: #64748b; - font-size: 0.9rem; - margin-bottom: 1rem; -} - -.identities-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 0.75rem; -} - -.identity-card { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.75rem 1rem; - background-color: #f8fafc; - border: 1px solid #e2e8f0; - border-radius: 0.5rem; - cursor: pointer; - transition: all 0.2s; - - &:hover { - background-color: #e0f2fe; - border-color: #3ba4d7; - } - - .identity-name { - font-size: 0.95rem; - font-weight: 600; - color: #334155; - } - - i { - color: #3ba4d7; - font-size: 0.9rem; - } -} - -.no-rooms { - padding: 1rem; - color: #94a3b8; - text-align: center; - font-style: italic; -} - -/* Chat Hub Responsive - Mobile */ -@media (max-width: 899px) { - .chat-hub-container { - flex-direction: column; - } - - .chat-hub-left-pane { - width: 100%; - min-width: 0; - max-width: none; - max-height: 45%; - border-right: none; - border-bottom: 1px solid #cbd5e1; - } - - .chat-hub-right-pane { - flex: 1; - min-height: 0; - } -} - -/* ===================================================== - CHAT HUB - Right Pane Conversation & Tabs Styling - ===================================================== */ - -.chat-hub-header-bar { - padding: 0.75rem 1.5rem; - background-color: #ffffff; - border-bottom: 1px solid #e2e8f0; - display: flex; - align-items: center; - justify-content: space-between; - height: 65px; - flex-shrink: 0; -} - -.chat-hub-header-bar .chat-header-info { - display: flex; - flex-direction: column; - overflow: hidden; -} - -.chat-hub-header-bar .chat-header-info .chat-header-name { - font-size: 1.15rem; - font-weight: 800; - color: #1e293b; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.chat-hub-header-bar .chat-header-info .chat-header-topic { - font-size: 0.85rem; - color: #64748b; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - margin-top: 0.125rem; -} - -.chat-hub-header-bar .chat-header-actions { - display: flex; - gap: 0.5rem; -} - -.chat-hub-header-bar .chat-header-actions button { - display: flex; - align-items: center; - gap: 0.35rem; - padding: 0.375rem 0.75rem; - font-size: 0.85rem; -} - -.chat-hub-tabs-container { - background-color: #ffffff; - border-bottom: 1px solid #cbd5e1; - padding: 0.5rem 1.5rem 0; -} - -.chat-hub-tabs { - display: flex; - gap: 0.5rem; -} - -.chat-hub-tabs .tab-btn { - padding: 0.625rem 1.25rem; - font-size: 0.95rem; - font-weight: 600; - color: #64748b; - background: transparent; - border: none; - border-radius: 0.375rem 0.375rem 0 0; - border-bottom: 3px solid transparent; - cursor: pointer; - box-shadow: none; - transition: all 0.2s; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.chat-hub-tabs .tab-btn:hover { - color: #334155; - background-color: #f1f5f9; -} - -.chat-hub-tabs .tab-btn.active { - color: #3ba4d7; - border-bottom-color: #3ba4d7; - background-color: transparent; -} - -.chat-hub-tab-content { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - background-color: #f8fafc; -} - -.chat-hub-conversation-layout { - display: flex; - flex-direction: row; - height: 100%; - width: 100%; - overflow: hidden; -} - -.chat-hub-conversation-main { - display: flex; - flex-direction: column; - flex: 1; - height: 100%; - overflow: hidden; -} - -.chat-hub-rightbar { - width: 200px; - border-left: 1px solid #cbd5e1; - background-color: #ffffff; - display: flex; - flex-direction: column; - flex-shrink: 0; - position: relative; // anchors the hovered-participant .user-tooltip (top offset is measured against this) -} - -.chat-hub-rightbar .rightbar-title { - padding: 0.75rem 1rem; - font-size: 0.85rem; - font-weight: 700; - color: #64748b; - text-transform: uppercase; - letter-spacing: 0.05em; - border-bottom: 1px solid #e2e8f0; -} - -.chat-hub-rightbar .rightbar-users-list { - flex: 1; - overflow-y: auto; - padding: 0.5rem; -} - -.chat-hub-rightbar .user { - padding: 0.5rem 0.75rem; - font-size: 0.9rem; - color: #334155; - border-radius: 0.375rem; - transition: all 0.2s; - display: flex; - align-items: center; - gap: 0.5rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - position: relative; -} - -.chat-hub-rightbar .user:hover { - background-color: #f1f5f9; - color: #0f172a; -} - -.chat-hub-rightbar .user .defaultAvatar { - width: 2rem; - height: 2rem; - font-size: 0.9rem; - flex-shrink: 0; -} - -.chat-hub-rightbar .user img.avatar { - width: 2rem; - height: 2rem; - flex-shrink: 0; -} - -@media (max-width: 899px) { - .chat-hub-rightbar { - display: none; - } -} - -.chat-hub-messages { - flex: 1; - overflow-y: auto; - padding: 1.25rem 1.5rem; - display: flex; - flex-direction: column; - gap: 1rem; -} - -/* Chat bubble overrides for two-pane layout */ -.chat-hub-messages .message { - display: flex; - flex-direction: column; - max-width: 70%; - padding: 0.625rem 0.875rem; - border-radius: 0.75rem; - font-size: 0.925rem; - line-height: 1.4; - word-break: break-word; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.chat-hub-messages .message.incoming { - align-self: flex-start; - align-items: flex-start; - background-color: #ffffff; - color: #1e293b; - border: 1px solid #e2e8f0; - border-bottom-left-radius: 0.125rem; -} - -.chat-hub-messages .message.outgoing { - align-self: flex-end; - align-items: flex-end; - background-color: #3ba4d7; - color: #ffffff; - border-bottom-right-radius: 0.125rem; -} - -.chat-hub-messages .message .username { - font-size: 0.75rem; - margin-bottom: 0.25rem; - padding: 0 0.125rem; - font-weight: 700; -} - -.chat-hub-messages .message.incoming .username { - color: #0369a1; -} - -.chat-hub-messages .message.outgoing .username { - color: #e0f2fe; -} - -.chat-hub-messages .message .messagetext { - white-space: break-spaces; - margin: 0; -} - -.chat-hub-messages .message .datetime { - font-size: 0.7rem; - margin-top: 0.25rem; - padding: 0 0.125rem; - opacity: 0.8; -} - -.chat-hub-messages .message.incoming .datetime { - color: #64748b; -} - -.chat-hub-messages .message.outgoing .datetime { - color: #f1f5f9; -} - -.chat-hub-input-area { - padding: 0.75rem 1.5rem; - background-color: #ffffff; - border-top: 1px solid #cbd5e1; - display: flex; - gap: 0.75rem; - align-items: flex-end; - flex-shrink: 0; -} - -.chat-hub-input-area textarea.chat-hub-textarea { - flex: 1; - resize: vertical; - min-height: 40px; - max-height: 250px; - height: 40px; - padding: 0.5rem 0.75rem; - border: 1px solid #cbd5e1; - border-radius: 0.375rem; - font-size: 0.9rem; - outline: none; - transition: border-color 0.2s, box-shadow 0.2s; - background-color: #f8fafc; -} - -.chat-hub-input-area textarea.chat-hub-textarea:focus { - background-color: #ffffff; - border-color: #3ba4d7; - box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); -} - -.chat-hub-input-area button.chat-hub-send-btn { - padding: 0.5rem 1.25rem; - font-size: 0.9rem; - height: 40px; - display: flex; - align-items: center; - gap: 0.5rem; - border-radius: 0.375rem; -} - -/* Compact Room Chat Style (No bubbles, unique nickname colors, IRC-style) */ -.chat-hub-messages.compact-container, -.messages.compact-container { - gap: 0 !important; - padding: 0.75rem 1rem !important; - background-color: #ffffff !important; - display: flex !important; - flex-direction: column !important; - - .message.compact { - display: block !important; - max-width: 100% !important; - padding: 0.1rem 0 !important; - border-radius: 0 !important; - background-color: transparent !important; - border: none !important; - box-shadow: none !important; - align-self: flex-start !important; - font-size: 0.875rem !important; - line-height: 1.45 !important; - margin: 0 !important; - white-space: nowrap !important; - // overflow: hidden !important; - // text-overflow: ellipsis !important; - - &:hover { - background-color: #f8fafc !important; - overflow: visible !important; - white-space: normal !important; - } - - .datetime { - color: #a0a0a0 !important; - margin-right: 0.4rem !important; - font-size: 0.78rem !important; - font-family: monospace !important; - opacity: 1 !important; - display: inline !important; - } - - .username { - font-weight: bold !important; - margin-right: 0.2rem !important; - font-size: 0.875rem !important; - display: inline !important; - } - - .messagetext { - color: #1e293b !important; - white-space: normal !important; - word-break: break-word !important; - display: inline !important; - margin: 0 !important; - } - } -} - - - - - -// Chat-hub extras: user tooltip, right-bar context menu, attach-file modal, -// emoji picker, create-lobby button (chat.js). Recovered from compiled styles.css. - -.chat-create-lobby-btn { - position: absolute; - bottom: 0.5rem; - right: 1.25rem; - background-color: #0084ff; - color: #ffffff; - border: none; - border-radius: 0.375rem; - padding: 0.35rem 0.75rem; - font-size: 0.85rem; - font-weight: 600; - cursor: pointer; - box-shadow: 0 4px 6px -1px rgba(0, 132, 255, 0.2), 0 2px 4px -1px rgba(0, 132, 255, 0.1); - transition: background-color 0.2s, transform 0.2s; - display: flex; - align-items: center; - gap: 0.25rem; -} - -.chat-create-lobby-btn:hover { - background-color: #0073e6; - transform: translateY(-1px); -} - -.chat-create-lobby-btn:active { - transform: translateY(0); -} - -.chat-hub-rightbar .user .user-name { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1; -} - -.user-tooltip { - position: absolute; - width: 260px; - background-color: #ffffe1; - border: 1px solid #7f7f7f; - box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.25); - padding: 0.5rem; - border-radius: 0.25rem; - z-index: 10000; - white-space: normal; - display: flex; - gap: 0.5rem; - align-items: flex-start; -} - -.chat-hub-rightbar .user-tooltip { - left: -275px; - transform: translateY(-50%); - z-index: 1000; -} - -.user-tooltip .tooltip-avatar { - flex-shrink: 0; -} - -.user-tooltip .tooltip-details { - display: flex; - flex-direction: column; - gap: 0.25rem; - font-size: 0.8rem; - color: #000000; - text-align: left; -} - -.user-tooltip .tooltip-row { - line-height: 1.2; -} - -.user-tooltip .tooltip-label { - font-weight: bold; -} - -.user-tooltip .tooltip-value { - font-weight: normal; - word-break: break-all; -} - -.user-tooltip .tooltip-value.tooltip-id { - font-family: monospace; -} - -.chat-hub-rightbar .rightbar-context-menu { - position: absolute; - right: 1rem; - width: 210px; - background-color: #ffffff; - border: 1px solid #e2e8f0; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); - border-radius: 0.375rem; - z-index: 1010; - padding: 0.25rem 0; - display: flex; - flex-direction: column; -} - -.chat-hub-rightbar .rightbar-context-menu .menu-item { - padding: 0.5rem 1rem; - font-size: 0.85rem; - color: #334155; - cursor: pointer; - display: flex; - align-items: center; - transition: background-color 0.2s; -} - -.chat-hub-rightbar .rightbar-context-menu .menu-item:hover { - background-color: #f1f5f9; - color: #0f172a; -} - -.chat-emoji { - font-size: 1.45em; - line-height: 1; - vertical-align: -0.15em; - display: inline-block; -} - -.chat-hub-attach-btn, -.chat-hub-action-btn { - background-color: transparent !important; - border: none !important; - font-size: 1.15rem !important; - color: #64748b !important; - cursor: pointer !important; - padding: 0.4rem 0.5rem !important; - border-radius: 0.375rem !important; - flex-shrink: 0 !important; - display: inline-flex !important; - align-items: center !important; - justify-content: center !important; - transition: all 0.2s !important; - box-shadow: none !important; - margin: 0 !important; - line-height: 1 !important; - height: 36px !important; - width: 36px !important; -} - -.chat-hub-attach-btn:hover, -.chat-hub-action-btn:hover { - background-color: #f1f5f9 !important; - color: #3b82f6 !important; - transform: none !important; -} - -.attach-modal-overlay { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background-color: rgba(15, 23, 42, 0.4); - backdrop-filter: blur(4px); - display: flex; - align-items: center; - justify-content: center; - z-index: 2000; -} - -.attach-modal { - background-color: #ffffff; - border-radius: 0.5rem; - width: 450px; - max-width: 90%; - padding: 1.5rem; - box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); - display: flex; - flex-direction: column; - gap: 1rem; -} - -.attach-modal .attach-modal-header { - display: flex; - align-items: center; - gap: 0.6rem; - margin-bottom: 0.25rem; -} - -.attach-modal .attach-modal-icon { - font-size: 1.2rem; - color: #3b82f6; -} - -.attach-modal h4 { - margin: 0; - font-size: 1.2rem; - color: #0f172a; -} - -.attach-modal p { - margin: 0; - font-size: 0.9rem; - color: #475569; -} - -.attach-modal .attach-path-row { - display: flex; - gap: 0.5rem; - align-items: center; -} - -.attach-modal .attach-path-row input[type="text"] { - flex: 1; - padding: 0.75rem; - border: 1px solid #cbd5e1; - border-radius: 0.375rem; - font-size: 0.9rem; - outline: none; - transition: border-color 0.2s; - min-width: 0; -} - -.attach-modal .attach-path-row input[type="text"]:focus { - border-color: #3b82f6; - box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); -} - -.attach-browse-btn { - flex-shrink: 0; - display: flex; - align-items: center; - gap: 0.35rem; - padding: 0.625rem 0.9rem; - font-size: 0.875rem; - background-color: #f1f5f9; - color: #334155; - border: 1px solid #cbd5e1; - border-radius: 0.375rem; - cursor: pointer; - box-shadow: none; - transition: background-color 0.2s, border-color 0.2s; - white-space: nowrap; -} - -.attach-browse-btn:hover { - background-color: #e2e8f0; - border-color: #94a3b8; -} - -.attach-path-hint { - display: flex; - align-items: flex-start; - gap: 0.5rem; - padding: 0.6rem 0.75rem; - background-color: #fffbeb; - border: 1px solid #fcd34d; - border-left: 3px solid #f59e0b; - border-radius: 0.375rem; - font-size: 0.825rem; - color: #92400e; - line-height: 1.45; -} - -.attach-path-hint i { - color: #f59e0b; - margin-top: 0.1rem; - flex-shrink: 0; -} - -.attach-path-hint code { - font-family: monospace; - background-color: rgba(245, 158, 11, 0.15); - padding: 0.05rem 0.25rem; - border-radius: 0.2rem; -} - -.attach-modal .hashing-spinner { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.9rem; - color: #3b82f6; -} - -.attach-modal .error-text { - color: #ef4444; - font-size: 0.85rem; - margin: 0; -} - -.attach-modal .modal-buttons { - display: flex; - justify-content: flex-end; - gap: 0.75rem; - margin-top: 0.5rem; -} - -.attach-modal .modal-buttons button { - padding: 0.5rem 1rem; - font-size: 0.9rem; - border-radius: 0.25rem; - border: none; - cursor: pointer; - transition: opacity 0.2s; -} - -.attach-modal .modal-buttons button:hover { - opacity: 0.9; -} - -.chat-hub-emoji-btn { - background-color: transparent; - border: none; - font-size: 1.3rem; - cursor: pointer; - padding: 0.35rem 0.4rem; - margin-right: 0.25rem; - flex-shrink: 0; - display: flex; - align-items: center; - justify-content: center; - border-radius: 0.375rem; - line-height: 1; - transition: background-color 0.15s, transform 0.15s; - box-shadow: none; -} - -.chat-hub-emoji-btn:hover { - background-color: #f1f5f9; - transform: scale(1.1); -} - -.emoji-picker-wrapper { - position: relative; - flex-shrink: 0; - display: flex; - align-items: center; -} - -.emoji-picker { - position: absolute; - bottom: calc(100% + 0.5rem); - left: 0; - width: 320px; - background-color: #ffffff; - border: 1px solid #e2e8f0; - border-radius: 0.625rem; - box-shadow: 0 8px 30px -4px rgba(0, 0, 0, 0.18), 0 4px 12px -2px rgba(0, 0, 0, 0.1); - z-index: 3000; - display: flex; - flex-direction: column; - overflow: hidden; - animation: emoji-pop 0.15s ease-out; -} - -.emoji-search-row { - display: flex; - align-items: center; - gap: 0.4rem; - padding: 0.6rem 0.75rem 0.4rem; - border-bottom: 1px solid #f1f5f9; -} - -.emoji-search-icon { - color: #94a3b8; - font-size: 0.8rem; - flex-shrink: 0; -} - -.emoji-search-input { - flex: 1; - border: 1px solid #e2e8f0; - border-radius: 0.375rem; - padding: 0.3rem 0.5rem; - font-size: 0.85rem; - outline: none; - background-color: #f8fafc; - transition: border-color 0.15s; -} - -.emoji-search-input:focus { - border-color: #3ba4d7; - background-color: #fff; -} - -.emoji-search-clear { - background: none; - border: none; - cursor: pointer; - color: #94a3b8; - padding: 0.2rem; - font-size: 0.8rem; - box-shadow: none; - display: flex; - align-items: center; -} - -.emoji-search-clear:hover { - color: #475569; -} - -.emoji-categories { - display: flex; - gap: 0.1rem; - padding: 0.35rem 0.5rem; - border-bottom: 1px solid #f1f5f9; - overflow-x: auto; - scrollbar-width: none; -} - -.emoji-categories::-webkit-scrollbar { - display: none; -} - -.emoji-cat-btn { - background: none; - border: none; - cursor: pointer; - font-size: 1.2rem; - padding: 0.3rem 0.35rem; - border-radius: 0.375rem; - line-height: 1; - box-shadow: none; - transition: background-color 0.1s; - flex-shrink: 0; -} - -.emoji-cat-btn:hover { - background-color: #f1f5f9; -} - -.emoji-cat-btn.active { - background-color: #e0f2fe; - box-shadow: inset 0 -2px 0 #3ba4d7; -} - -.emoji-grid { - display: grid; - grid-template-columns: repeat(7, 1fr); - gap: 0; - padding: 0.4rem 0.35rem; - max-height: 220px; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: #cbd5e1 transparent; -} - -.emoji-grid::-webkit-scrollbar { - width: 4px; -} - -.emoji-grid::-webkit-scrollbar-track { - background: transparent; -} - -.emoji-grid::-webkit-scrollbar-thumb { - background-color: #cbd5e1; - border-radius: 4px; -} - -.emoji-btn { - background: none; - border: none; - cursor: pointer; - font-size: 1.7rem; - padding: 0.25rem; - border-radius: 0.3rem; - line-height: 1; - box-shadow: none; - text-align: center; - transition: background-color 0.1s, transform 0.1s; - display: flex; - align-items: center; - justify-content: center; - aspect-ratio: 1; -} - -.emoji-btn:hover { - background-color: #f1f5f9; - transform: scale(1.2); -} - -@keyframes emoji-pop { - from { opacity: 0; transform: scale(0.92) translateY(6px); } - to { opacity: 1; transform: scale(1) translateY(0); } -} +@use '../abstracts' as *; + +.lobby { + margin: 10px; + border: 1px solid #aaa; + border-radius: 20px; +} + +.lobby .mainname { + margin: 20px; + font-weight: 100; + font-size: 1.2em; +} + +.topic { + color: #666; +} + +.lobby>.topic { + font-size: 0.95em; + margin-left: 25px; + margin-bottom: 5px; +} + +.lefttitle { + margin-top: 15px; + margin-bottom: 0; + font-weight: 100; + font-size: 1.2em; +} + +.leftname { + margin-top: 5px; + margin-bottom: 5px; + padding: 5px; + font-weight: 100; + font-size: 1em; +} + +.leftlobby>.topic { + font-size: 0.75em; + margin-left: 15px; + margin-bottom: 5px; +} + +.subscribed, +.public { + cursor: pointer; +} + +.leftlobby { + border: 1px solid #aaa; + border-radius: 10px; + margin-top: 5px; + background-color: white; +} + +.leftlobby.selected-lobby, +.selectedidentity { + color: white; + background-color: #3ba4d7; +} + +.rightbar { + position: absolute; + width: 185px; + background-color: white; + overflow: auto; + top: 130px; + bottom: 15px; + right: 15px; +} + +.user { + padding: 5px; +} + +.lobbyName { + padding: 15px; + margin-top: 2rem; +} + +.lobbies { + position: absolute; + width: 185px; + left: 165px; + bottom: 15px; + top: 130px; + overflow: auto; +} + +.messages, +.setup { + position: absolute; + background-color: white; + top: 130px; + left: 360px; + right: 215px; + overflow: auto; +} + +.messages { + bottom: 115px; +} + +.messagetext { + white-space: break-spaces; + margin-right: 5px; +} + +.message>* { + margin-left: 5px; +} + +.username { + color: darkgreen; + font-weight: bolder; +} + +.chatMessage { + position: absolute; + background-color: white; + height: 85px; + bottom: 15px; + right: 215px; + left: 360px; +} + +textarea.chatMsg { + height: 100%; + width: 100%; +} + +.chatatchar { + margin-left: 0.2em; + margin-right: 0.2em; + color: silver; +} + +.setupicon { + margin-left: 1em; + cursor: pointer; +} + +.leaveicon { + margin-left: 1em; + cursor: pointer; + color: #d40000; +} + +.selectidentity { + margin: 15px; + font-size: 1.2em; +} + +.setup>.identity { + cursor: pointer; +} + +.setup { + bottom: 15px; +} + +.createDistantChat { + margin-top: 1em; +} + +.no-lobbies { + + .messages, + .chatMessage, + .setup { + left: 165px; + } +} + +/* CHAT ROOM (Single Chat) - Desktop Grid Layout */ +@media (min-width: 900px) { + .node-panel.chat-room { + display: grid !important; + grid-template-columns: 250px 1fr 200px !important; + /* Lobbies, Chat, Users */ + grid-template-rows: auto 1fr auto !important; + /* Header, Messages, Input */ + grid-template-areas: + "lobbies header rightbar" + "lobbies messages rightbar" + "lobbies input rightbar" !important; + padding: 0 !important; + height: 100% !important; + } + + .node-panel.chat-room .lobbyName { + grid-area: header; + padding: 10px; + border-bottom: 1px solid #eee; + margin: 0; + z-index: 10; + background: white; + } + + .node-panel.chat-room .lobbies { + grid-area: lobbies; + position: static !important; + width: auto !important; + height: auto !important; + border-right: 1px solid #ccc; + overflow-y: auto; + display: block !important; + top: auto !important; + bottom: auto !important; + left: auto !important; + } + + .node-panel.chat-room .messages { + grid-area: messages; + position: static !important; + width: auto !important; + height: auto !important; + overflow-y: auto; + padding: 10px; + left: auto !important; + right: auto !important; + top: auto !important; + bottom: auto !important; + margin: 0 !important; + } + + .node-panel.chat-room .rightbar { + grid-area: rightbar; + position: static !important; + width: auto !important; + border-left: 1px solid #ccc; + overflow-y: auto; + display: block !important; + } + + .node-panel.chat-room .chatMessage { + grid-area: input; + position: static !important; + width: auto !important; + height: auto !important; + border-top: 1px solid #eee; + left: auto !important; + right: auto !important; + bottom: auto !important; + flex: 0 0 auto; + padding: 10px !important; + background: white; + z-index: 10; + } +} + +/* Mobile Overrides - Ensure Flex Column */ +@media (max-width: 899px) { + .node-panel.chat-room { + display: flex !important; + flex-direction: column !important; + height: 100% !important; + position: relative !important; + } + + .node-panel.chat-room .lobbyName { + flex: 0 0 auto; + } + + .node-panel.chat-room .messages { + flex: 1 !important; + overflow-y: auto !important; + position: relative !important; + top: 0 !important; + bottom: 0 !important; + left: 0 !important; + right: 0 !important; + width: 100% !important; + height: auto !important; + margin: 0 !important; + } + + .node-panel.chat-room .chatMessage { + flex: 0 0 auto !important; + position: relative !important; + bottom: 0 !important; + left: 0 !important; + right: 0 !important; + width: 100% !important; + height: auto !important; + z-index: 100; + } + + .node-panel.chat-room .rightbar, + .node-panel.chat-room .lobbies { + display: none !important; + position: fixed !important; + top: 60px !important; + bottom: 0 !important; + width: 80% !important; + background: white !important; + z-index: 200 !important; + box-shadow: 2px 0 10px rgba(0, 0, 0, 0.2) !important; + } + + .node-panel.chat-room.show-lobbies .lobbies { + display: block !important; + left: 0 !important; + } + + .node-panel.chat-room.show-users .rightbar { + display: block !important; + right: 0 !important; + } + + .chat-overlay { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.4); + z-index: 150; + } + + .show-lobbies .chat-overlay, + .show-users .chat-overlay { + display: block; + } + + /* Mobile Icons in Header */ + .mobile-menu-icons { + display: flex; + gap: 15px; + font-size: 1.2rem; + } + + .mobile-menu-icons i { + cursor: pointer; + padding: 5px; + } +} + +@media (min-width: 900px) { + .mobile-menu-icons { + display: none; + } +} + +/* ===================================================== + CHAT HUB - Two-Pane Layout (matching Network page) + ===================================================== */ + +.chat-hub-container { + display: flex; + height: 100%; + width: 100%; + overflow: hidden; + background-color: #f1f5f9; +} + +.chat-hub-left-pane { + width: 320px; + min-width: 300px; + max-width: 350px; + border-right: 1px solid #cbd5e1; + display: flex; + flex-direction: column; + background: #ffffff; + box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05); +} + +.chat-own-profile-card { + padding: 0.85rem 1.25rem !important; + border-bottom: 1px solid #e2e8f0 !important; + background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%) !important; + display: flex !important; + align-items: center !important; + justify-content: space-between !important; + gap: 0.75rem !important; + // Kept from master: .chat-create-lobby-btn is still declared `position: + // absolute; bottom: .5rem; right: 1.25rem` further down this file, so the + // card has to stay its containing block. Without this the button escapes to + // the initial containing block and lands at the bottom right of the page. + position: relative; + + .profile-header { + display: flex !important; + align-items: center !important; + gap: 0.75rem !important; + flex: 1 !important; + min-width: 0 !important; + } + + .chat-create-lobby-btn { + display: flex !important; + align-items: center !important; + gap: 0.35rem !important; + padding: 0.4rem 0.85rem !important; + font-size: 0.85rem !important; + font-weight: 600 !important; + border-radius: 0.375rem !important; + cursor: pointer !important; + flex-shrink: 0 !important; + white-space: nowrap !important; + } + + .profile-info { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; + + .profile-name { + font-weight: 700; + color: #1e293b; + font-size: 1.1rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .profile-status { + font-size: 0.85rem; + color: #10b981; + font-weight: 500; + display: flex; + align-items: center; + gap: 0.35rem; + + &::before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + background-color: #10b981; + border-radius: 50%; + } + } + } +} + +.chat-rooms-list-container { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + + .searchbar-container { + padding: 0.75rem 1rem; + border-bottom: 1px solid #e2e8f0; + + input.searchbar { + width: 100%; + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + background-color: #f8fafc; + outline: none; + transition: all 0.2s; + + &:focus { + background-color: #ffffff; + border-color: #3ba4d7; + box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); + } + } + } + + .rooms-scroll { + flex: 1; + overflow-y: auto; + padding: 0.5rem 0; + } +} + +.rooms-section-title { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.75rem 1rem 0.375rem; + font-size: 0.75rem; + font-weight: 700; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.05em; + + i { + font-size: 0.7rem; + color: #94a3b8; + } +} + +.chat-room-list-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + margin: 0.125rem 0.5rem; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background-color: #f1f5f9; + } + + &.selected { + background-color: #e0f2fe; + + .room-name { + color: #0369a1; + font-weight: 600; + } + } + + .room-icon { + flex-shrink: 0; + width: 36px; + height: 36px; + border-radius: 0.5rem; + background: linear-gradient(135deg, #3ba4d7, #0ea5e9); + display: flex; + align-items: center; + justify-content: center; + color: #ffffff; + font-size: 1.35rem; + } + + &.public-room .room-icon { + background: linear-gradient(135deg, #10b981, #059669); + } + + .room-meta { + flex: 1; + min-width: 0; + + .room-name { + font-size: 0.95rem; + color: #334155; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition: color 0.2s; + } + + .room-topic { + font-size: 0.8rem; + color: #94a3b8; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .room-badge { + flex-shrink: 0; + min-width: 24px; + height: 24px; + border-radius: 12px; + background-color: #e2e8f0; + color: #475569; + font-size: 0.75rem; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + padding: 0 0.375rem; + } +} + +.chat-hub-right-pane { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: #f8fafc; +} + +.chat-pane-placeholder { + flex: 1; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: #94a3b8; + gap: 1rem; + padding: 2rem; + text-align: center; + + i { + font-size: 4rem; + color: #cbd5e1; + } + + p { + font-size: 1.1rem; + max-width: 400px; + } +} + +.chat-hub-tab-content { + flex: 1; + overflow-y: auto; + padding: 1.5rem; +} + +.chat-room-detail-view { + display: flex; + flex-direction: column; + gap: 1.5rem; + + .detail-header { + display: flex; + align-items: flex-start; + gap: 1.5rem; + padding-bottom: 1.5rem; + border-bottom: 1px solid #e2e8f0; + flex-wrap: wrap; + + .detail-title { + flex: 1; + min-width: 200px; + + h2 { + font-size: 1.75rem; + font-weight: 800; + color: #1e293b; + margin-bottom: 0.25rem; + } + + .detail-subtitle { + font-size: 0.9rem; + color: #64748b; + display: flex; + align-items: center; + gap: 0.5rem; + } + } + + .detail-actions { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + + button { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + font-size: 0.9rem; + } + } + } + + .detail-section { + background-color: #ffffff; + border-radius: 0.5rem; + border: 1px solid #e2e8f0; + padding: 1.25rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); + + h3 { + font-size: 1.1rem; + font-weight: 700; + color: #334155; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid #f1f5f9; + } + + .info-grid { + display: grid; + grid-template-columns: 130px 1fr; + row-gap: 0.75rem; + font-size: 0.9rem; + + .info-label { + font-weight: 600; + color: #64748b; + } + + .info-value { + color: #1e293b; + word-break: break-all; + } + } + } +} + +.participants-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 0.5rem; +} + +.participant-card { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + background-color: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 0.375rem; + + .participant-name { + font-size: 0.875rem; + color: #334155; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.no-participants { + color: #94a3b8; + font-size: 0.9rem; + font-style: italic; +} + +.detail-actions-footer { + display: flex; + gap: 0.75rem; + + button { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + font-size: 0.9rem; + } +} + +.join-description { + color: #64748b; + font-size: 0.9rem; + margin-bottom: 1rem; +} + +.identities-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 0.75rem; +} + +.identity-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + background-color: #f8fafc; + border: 1px solid #e2e8f0; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s; + + &:hover { + background-color: #e0f2fe; + border-color: #3ba4d7; + } + + .identity-name { + font-size: 0.95rem; + font-weight: 600; + color: #334155; + } + + i { + color: #3ba4d7; + font-size: 0.9rem; + } +} + +.no-rooms { + padding: 1rem; + color: #94a3b8; + text-align: center; + font-style: italic; +} + +/* Chat Hub Responsive - Mobile */ +@media (max-width: 899px) { + .chat-hub-container { + flex-direction: column; + } + + .chat-hub-left-pane { + width: 100%; + min-width: 0; + max-width: none; + max-height: 45%; + border-right: none; + border-bottom: 1px solid #cbd5e1; + } + + .chat-hub-right-pane { + flex: 1; + min-height: 0; + } +} + +/* ===================================================== + CHAT HUB - Right Pane Conversation & Tabs Styling + ===================================================== */ + +.chat-hub-header-bar { + padding: 0.75rem 1.5rem; + background-color: #ffffff; + border-bottom: 1px solid #e2e8f0; + display: flex; + align-items: center; + justify-content: space-between; + height: 65px; + flex-shrink: 0; +} + +.chat-hub-header-bar .chat-header-info { + display: flex; + flex-direction: column; + overflow: hidden; +} + +.chat-hub-header-bar .chat-header-info .chat-header-name { + font-size: 1.15rem; + font-weight: 800; + color: #1e293b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.chat-hub-header-bar .chat-header-info .chat-header-topic { + font-size: 0.85rem; + color: #64748b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin-top: 0.125rem; +} + +.chat-hub-header-bar .chat-header-actions { + display: flex; + gap: 0.5rem; +} + +.chat-hub-header-bar .chat-header-actions button { + display: flex; + align-items: center; + gap: 0.35rem; + padding: 0.375rem 0.75rem; + font-size: 0.85rem; +} + +/* Room-header actions remain available on phones without consuming the + * message area: their accessible title still describes each icon. */ +@media (max-width: 700px) { + .chat-hub-header-bar { + height: auto; + min-height: 48px; + padding: .45rem .55rem; + gap: .45rem; + } + + .chat-hub-header-bar .chat-header-info { + min-width: 0; + flex: 1; + } + + .chat-hub-header-bar .chat-header-info .chat-header-name { + font-size: .95rem; + } + + .chat-hub-header-bar .chat-header-actions { + flex: 0 0 auto; + gap: .25rem; + } + + .chat-hub-header-bar .chat-header-actions button { + width: 32px; + min-width: 32px; + height: 32px; + padding: 0; + justify-content: center; + font-size: 0; + } + + .chat-hub-header-bar .chat-header-actions button i { + margin: 0; + font-size: .95rem; + } +} + +.chat-hub-tabs-container { + background-color: #ffffff; + border-bottom: 1px solid #cbd5e1; + padding: 0.5rem 1.5rem 0; +} + +.chat-hub-tabs { + display: flex; + gap: 0.5rem; +} + +.chat-hub-tabs .tab-btn { + padding: 0.625rem 1.25rem; + font-size: 0.95rem; + font-weight: 600; + color: #64748b; + background: transparent; + border: none; + border-radius: 0.375rem 0.375rem 0 0; + border-bottom: 3px solid transparent; + cursor: pointer; + box-shadow: none; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.chat-hub-tabs .tab-btn:hover { + color: #334155; + background-color: #f1f5f9; +} + +.chat-hub-tabs .tab-btn.active { + color: #3ba4d7; + border-bottom-color: #3ba4d7; + background-color: transparent; +} + +.chat-hub-tab-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + background-color: #f8fafc; +} + +.chat-hub-conversation-layout { + display: flex; + flex-direction: row; + height: 100%; + width: 100%; + overflow: hidden; +} + +.chat-hub-conversation-main { + display: flex; + flex-direction: column; + flex: 1; + height: 100%; + overflow: hidden; +} + +.chat-hub-rightbar { + width: 200px; + border-left: 1px solid #cbd5e1; + background-color: #ffffff; + display: flex; + flex-direction: column; + flex-shrink: 0; + position: relative; // anchors the hovered-participant .user-tooltip (top offset is measured against this) +} + +.chat-hub-rightbar .rightbar-title { + padding: 0.75rem 1rem; + font-size: 0.85rem; + font-weight: 700; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid #e2e8f0; +} + +.chat-hub-rightbar .rightbar-users-list { + flex: 1; + overflow-y: auto; + padding: 0.5rem; +} + +.chat-hub-rightbar .user { + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + color: #334155; + border-radius: 0.375rem; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 0.5rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + position: relative; +} + +.chat-hub-rightbar .user:hover { + background-color: #f1f5f9; + color: #0f172a; +} + +.chat-hub-rightbar .user .defaultAvatar { + width: 2rem; + height: 2rem; + font-size: 0.9rem; + flex-shrink: 0; +} + +.chat-hub-rightbar .user img.avatar { + width: 2rem; + height: 2rem; + flex-shrink: 0; +} + +@media (max-width: 899px) { + .chat-hub-rightbar { + display: none; + } +} + +.chat-hub-messages { + flex: 1; + overflow-y: auto; + padding: 1.25rem 1.5rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +/* Chat bubble overrides for two-pane layout */ +.chat-hub-messages .message { + display: flex; + flex-direction: column; + max-width: 70%; + padding: 0.625rem 0.875rem; + border-radius: 0.75rem; + font-size: 0.925rem; + line-height: 1.4; + word-break: break-word; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +.chat-hub-messages .message.incoming { + align-self: flex-start; + align-items: flex-start; + background-color: #ffffff; + color: #1e293b; + border: 1px solid #e2e8f0; + border-bottom-left-radius: 0.125rem; +} + +.chat-hub-messages .message.outgoing { + align-self: flex-end; + align-items: flex-end; + background-color: #3ba4d7; + color: #ffffff; + border-bottom-right-radius: 0.125rem; +} + +.chat-hub-messages .message .username { + font-size: 0.75rem; + margin-bottom: 0.25rem; + padding: 0 0.125rem; + font-weight: 700; +} + +.chat-hub-messages .message.incoming .username { + color: #0369a1; +} + +.chat-hub-messages .message.outgoing .username { + color: #e0f2fe; +} + +.chat-hub-messages .message .messagetext { + white-space: break-spaces; + margin: 0; +} + +.chat-hub-messages .message .datetime { + font-size: 0.7rem; + margin-top: 0.25rem; + padding: 0 0.125rem; + opacity: 0.8; +} + +.chat-hub-messages .message.incoming .datetime { + color: #64748b; +} + +.chat-hub-messages .message.outgoing .datetime { + color: #f1f5f9; +} + +.chat-hub-input-area { + padding: 0.75rem 1.5rem; + background-color: #ffffff; + border-top: 1px solid #cbd5e1; + display: flex; + gap: 0.75rem; + align-items: flex-end; + flex-shrink: 0; +} + +.chat-hub-input-area textarea.chat-hub-textarea { + flex: 1; + resize: vertical; + min-height: 40px; + max-height: 250px; + height: 40px; + padding: 0.5rem 0.75rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + font-size: 0.9rem; + outline: none; + transition: border-color 0.2s, box-shadow 0.2s; + background-color: #f8fafc; +} + +.chat-hub-input-area textarea.chat-hub-textarea:focus { + background-color: #ffffff; + border-color: #3ba4d7; + box-shadow: 0 0 0 3px rgba(59, 164, 215, 0.15); +} + +.chat-hub-input-area button.chat-hub-send-btn { + padding: 0.5rem 1.25rem; + font-size: 0.9rem; + height: 40px; + display: flex; + align-items: center; + gap: 0.5rem; + border-radius: 0.375rem; +} + +button.chat-hub-action-btn, +label.chat-hub-action-btn, +.chat-hub-action-btn { + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + width: 36px !important; + height: 36px !important; + min-width: 36px !important; + padding: 0 !important; + margin: 0 !important; + background: transparent !important; + border: none !important; + box-shadow: none !important; + color: #64748b !important; + font-size: 1.15rem !important; + border-radius: 0.375rem !important; + cursor: pointer !important; + transition: all 0.15s ease !important; + outline: none !important; +} + +button.chat-hub-action-btn:hover, +label.chat-hub-action-btn:hover, +.chat-hub-action-btn:hover { + background-color: #e2e8f0 !important; + color: #3b82f6 !important; + box-shadow: none !important; +} + +button.chat-hub-action-btn i, +label.chat-hub-action-btn i, +.chat-hub-action-btn i { + font-size: 1.15rem !important; + color: inherit !important; +} + +/* Compact Room Chat Style (No bubbles, unique nickname colors, IRC-style) */ +.chat-hub-messages.compact-container, +.messages.compact-container { + gap: 0 !important; + padding: 0.75rem 1rem !important; + background-color: #ffffff !important; + display: flex !important; + flex-direction: column !important; + + .message.compact { + display: block !important; + max-width: 100% !important; + padding: 0.1rem 0 !important; + border-radius: 0 !important; + background-color: transparent !important; + border: none !important; + box-shadow: none !important; + align-self: flex-start !important; + font-size: 0.875rem !important; + line-height: 1.45 !important; + margin: 0 !important; + white-space: nowrap !important; + // overflow: hidden !important; + // text-overflow: ellipsis !important; + + &:hover { + background-color: #f8fafc !important; + overflow: visible !important; + white-space: normal !important; + } + + .datetime { + color: #a0a0a0 !important; + margin-right: 0.4rem !important; + font-size: 0.78rem !important; + font-family: monospace !important; + opacity: 1 !important; + display: inline !important; + } + + .username { + font-weight: bold !important; + margin-right: 0.2rem !important; + font-size: 0.875rem !important; + display: inline !important; + } + + .messagetext { + color: #1e293b !important; + white-space: normal !important; + word-break: break-word !important; + display: inline !important; + margin: 0 !important; + } + } +} + + + + + +// Chat-hub extras: user tooltip, right-bar context menu, attach-file modal, +// emoji picker, create-lobby button (chat.js). Recovered from compiled styles.css. + +.chat-create-lobby-btn { + position: absolute; + bottom: 0.5rem; + right: 1.25rem; + background-color: #0084ff; + color: #ffffff; + border: none; + border-radius: 0.375rem; + padding: 0.35rem 0.75rem; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + box-shadow: 0 4px 6px -1px rgba(0, 132, 255, 0.2), 0 2px 4px -1px rgba(0, 132, 255, 0.1); + transition: background-color 0.2s, transform 0.2s; + display: flex; + align-items: center; + gap: 0.25rem; +} + +.chat-create-lobby-btn:hover { + background-color: #0073e6; + transform: translateY(-1px); +} + +.chat-create-lobby-btn:active { + transform: translateY(0); +} + +.chat-hub-rightbar .user .user-name { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; +} + +.chat-hub-rightbar .rightbar-context-menu { + position: absolute; + right: 1rem; + width: 210px; + background-color: #ffffff; + border: 1px solid #e2e8f0; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); + border-radius: 0.375rem; + z-index: 1010; + padding: 0.25rem 0; + display: flex; + flex-direction: column; +} + +.chat-hub-rightbar .rightbar-context-menu .menu-item { + padding: 0.5rem 1rem; + font-size: 0.85rem; + color: #334155; + cursor: pointer; + display: flex; + align-items: center; + transition: background-color 0.2s; +} + +.chat-hub-rightbar .rightbar-context-menu .menu-item:hover { + background-color: #f1f5f9; + color: #0f172a; +} + +.chat-emoji { + font-size: 1.45em; + line-height: 1; + vertical-align: -0.15em; + display: inline-block; +} + +.chat-hub-attach-btn, +.chat-hub-action-btn { + background-color: transparent !important; + border: none !important; + font-size: 1.15rem !important; + color: #64748b !important; + cursor: pointer !important; + padding: 0.4rem 0.5rem !important; + border-radius: 0.375rem !important; + flex-shrink: 0 !important; + display: inline-flex !important; + align-items: center !important; + justify-content: center !important; + transition: all 0.2s !important; + box-shadow: none !important; + margin: 0 !important; + line-height: 1 !important; + height: 36px !important; + width: 36px !important; +} + +.chat-hub-attach-btn:hover, +.chat-hub-action-btn:hover { + background-color: #f1f5f9 !important; + color: #3b82f6 !important; + transform: none !important; +} + +.attach-modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(15, 23, 42, 0.4); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 2000; +} + +.attach-modal { + background-color: #ffffff; + border-radius: 0.5rem; + width: 450px; + max-width: 90%; + padding: 1.5rem; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + gap: 1rem; +} + +.attach-modal .attach-modal-header { + display: flex; + align-items: center; + gap: 0.6rem; + margin-bottom: 0.25rem; +} + +.attach-modal .attach-modal-icon { + font-size: 1.2rem; + color: #3b82f6; +} + +.attach-modal h4 { + margin: 0; + font-size: 1.2rem; + color: #0f172a; +} + +.attach-modal p { + margin: 0; + font-size: 0.9rem; + color: #475569; +} + +.attach-modal .attach-path-row { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.attach-modal .attach-path-row input[type="text"] { + flex: 1; + padding: 0.75rem; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + font-size: 0.9rem; + outline: none; + transition: border-color 0.2s; + min-width: 0; +} + +.attach-modal .attach-path-row input[type="text"]:focus { + border-color: #3b82f6; + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15); +} + +.attach-browse-btn { + flex-shrink: 0; + display: flex; + align-items: center; + gap: 0.35rem; + padding: 0.625rem 0.9rem; + font-size: 0.875rem; + background-color: #f1f5f9; + color: #334155; + border: 1px solid #cbd5e1; + border-radius: 0.375rem; + cursor: pointer; + box-shadow: none; + transition: background-color 0.2s, border-color 0.2s; + white-space: nowrap; +} + +.attach-browse-btn:hover { + background-color: #e2e8f0; + border-color: #94a3b8; +} + +.attach-path-hint { + display: flex; + align-items: flex-start; + gap: 0.5rem; + padding: 0.6rem 0.75rem; + background-color: #fffbeb; + border: 1px solid #fcd34d; + border-left: 3px solid #f59e0b; + border-radius: 0.375rem; + font-size: 0.825rem; + color: #92400e; + line-height: 1.45; +} + +.attach-path-hint i { + color: #f59e0b; + margin-top: 0.1rem; + flex-shrink: 0; +} + +.attach-path-hint code { + font-family: monospace; + background-color: rgba(245, 158, 11, 0.15); + padding: 0.05rem 0.25rem; + border-radius: 0.2rem; +} + +.attach-modal .hashing-spinner { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.9rem; + color: #3b82f6; +} + +.attach-modal .error-text { + color: #ef4444; + font-size: 0.85rem; + margin: 0; +} + +.attach-modal .modal-buttons { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + margin-top: 0.5rem; +} + +.attach-modal .modal-buttons button { + padding: 0.5rem 1rem; + font-size: 0.9rem; + border-radius: 0.25rem; + border: none; + cursor: pointer; + transition: opacity 0.2s; +} + +.attach-modal .modal-buttons button:hover { + opacity: 0.9; +} + +.chat-hub-emoji-btn { + background-color: transparent; + border: none; + font-size: 1.3rem; + cursor: pointer; + padding: 0.35rem 0.4rem; + margin-right: 0.25rem; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + border-radius: 0.375rem; + line-height: 1; + transition: background-color 0.15s, transform 0.15s; + box-shadow: none; +} + +.chat-hub-emoji-btn:hover { + background-color: #f1f5f9; + transform: scale(1.1); +} + +.emoji-picker-wrapper { + position: relative; + flex-shrink: 0; + display: flex; + align-items: center; +} + +.emoji-picker { + position: absolute; + bottom: calc(100% + 0.5rem); + left: 0; + width: 320px; + background-color: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 0.625rem; + box-shadow: 0 8px 30px -4px rgba(0, 0, 0, 0.18), 0 4px 12px -2px rgba(0, 0, 0, 0.1); + z-index: 3000; + display: flex; + flex-direction: column; + overflow: hidden; + animation: emoji-pop 0.15s ease-out; +} + +.emoji-search-row { + display: flex; + align-items: center; + gap: 0.4rem; + padding: 0.6rem 0.75rem 0.4rem; + border-bottom: 1px solid #f1f5f9; +} + +.emoji-search-icon { + color: #94a3b8; + font-size: 0.8rem; + flex-shrink: 0; +} + +.emoji-search-input { + flex: 1; + border: 1px solid #e2e8f0; + border-radius: 0.375rem; + padding: 0.3rem 0.5rem; + font-size: 0.85rem; + outline: none; + background-color: #f8fafc; + transition: border-color 0.15s; +} + +.emoji-search-input:focus { + border-color: #3ba4d7; + background-color: #fff; +} + +.emoji-search-clear { + background: none; + border: none; + cursor: pointer; + color: #94a3b8; + padding: 0.2rem; + font-size: 0.8rem; + box-shadow: none; + display: flex; + align-items: center; +} + +.emoji-search-clear:hover { + color: #475569; +} + +.emoji-categories { + display: flex; + gap: 0.1rem; + padding: 0.35rem 0.5rem; + border-bottom: 1px solid #f1f5f9; + overflow-x: auto; + scrollbar-width: none; +} + +.emoji-categories::-webkit-scrollbar { + display: none; +} + +.emoji-cat-btn { + background: none; + border: none; + cursor: pointer; + font-size: 1.2rem; + padding: 0.3rem 0.35rem; + border-radius: 0.375rem; + line-height: 1; + box-shadow: none; + transition: background-color 0.1s; + flex-shrink: 0; +} + +.emoji-cat-btn:hover { + background-color: #f1f5f9; +} + +.emoji-cat-btn.active { + background-color: #e0f2fe; + box-shadow: inset 0 -2px 0 #3ba4d7; +} + +.emoji-grid { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 0; + padding: 0.4rem 0.35rem; + max-height: 220px; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: #cbd5e1 transparent; +} + +.emoji-grid::-webkit-scrollbar { + width: 4px; +} + +.emoji-grid::-webkit-scrollbar-track { + background: transparent; +} + +.emoji-grid::-webkit-scrollbar-thumb { + background-color: #cbd5e1; + border-radius: 4px; +} + +.emoji-btn { + background: none; + border: none; + cursor: pointer; + font-size: 1.7rem; + padding: 0.25rem; + border-radius: 0.3rem; + line-height: 1; + box-shadow: none; + text-align: center; + transition: background-color 0.1s, transform 0.1s; + display: flex; + align-items: center; + justify-content: center; + aspect-ratio: 1; +} + +.emoji-btn:hover { + background-color: #f1f5f9; + transform: scale(1.2); +} + +@keyframes emoji-pop { + from { opacity: 0; transform: scale(0.92) translateY(6px); } + to { opacity: 1; transform: scale(1) translateY(0); } + +} + +.chat-hub-messages.compact-container .message.compact, +.messages.compact-container .message.compact { + display: block !important; + max-width: 100% !important; + padding: 0.1rem 0 !important; + border-radius: 0 !important; + background-color: transparent !important; + border: none !important; + box-shadow: none !important; + align-self: flex-start !important; + font-size: 0.875rem !important; + line-height: 1.45 !important; + margin: 0 !important; + white-space: nowrap !important; +} + +.chat-hub-messages.compact-container .message.compact:hover, +.messages.compact-container .message.compact:hover { + background-color: #f8fafc !important; + overflow: visible !important; + white-space: normal !important; +} + +.chat-hub-messages.compact-container .message.compact .datetime, +.messages.compact-container .message.compact .datetime { + color: #a0a0a0 !important; + margin-right: 0.4rem !important; + font-size: 0.78rem !important; + font-family: monospace !important; + opacity: 1 !important; + display: inline !important; +} + +.chat-hub-messages.compact-container .message.compact .username, +.messages.compact-container .message.compact .username { + font-weight: bold !important; + margin-right: 0.2rem !important; + font-size: 0.875rem !important; + display: inline !important; +} + +.chat-hub-messages.compact-container .message.compact .messagetext, +.messages.compact-container .message.compact .messagetext { + color: #1e293b !important; + white-space: normal !important; + word-break: break-word !important; + display: inline !important; + margin: 0 !important; +} + +/* User Tooltip Styling (Standalone Fixed Floating Tooltip) */ +.user-tooltip { + position: fixed !important; + width: 280px !important; + background-color: #ffffe1 !important; + border: 1px solid #7f7f7f !important; + box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.25) !important; + padding: 0.5rem !important; + border-radius: 0.25rem !important; + z-index: 10000 !important; + white-space: normal !important; + display: flex !important; + gap: 0.5rem !important; + align-items: flex-start !important; + color: #000000 !important; + font-size: 0.8rem !important; + text-align: left !important; + pointer-events: none !important; +} + +.user-tooltip .tooltip-avatar { + flex-shrink: 0 !important; +} + +.user-tooltip .tooltip-avatar .jdenticon-avatar, +.user-tooltip .tooltip-avatar .defaultAvatar, +.user-tooltip .tooltip-avatar img.avatar { + width: 56px !important; + height: 56px !important; + min-width: 56px !important; + min-height: 56px !important; + border-radius: 2px !important; + border: 1px solid #999999 !important; + box-shadow: none !important; + object-fit: cover !important; +} + +.user-tooltip .tooltip-details { + display: flex !important; + flex-direction: column !important; + gap: 0.2rem !important; + min-width: 0 !important; + flex: 1 !important; +} + +.user-tooltip .tooltip-details .tooltip-row { + line-height: 1.2 !important; + display: flex !important; + flex-direction: row !important; + align-items: baseline !important; + gap: 0.35rem !important; + white-space: normal !important; + word-break: break-all !important; +} + +.user-tooltip .tooltip-details .tooltip-row .tooltip-label { + font-weight: bold !important; + color: #000000 !important; + font-size: 0.8rem !important; + flex-shrink: 0 !important; +} + +.user-tooltip .tooltip-details .tooltip-row .tooltip-value { + font-weight: normal !important; + color: #000000 !important; + font-size: 0.8rem !important; + overflow: hidden !important; + text-overflow: ellipsis !important; +} + +.user-tooltip .tooltip-details .tooltip-row .tooltip-value.tooltip-id { + font-family: monospace !important; + font-size: 0.75rem !important; + color: #0000bb !important; +} + +/* Chat Modal Dialogs (Create Room, Attach File, Invite Friends) */ +.attach-modal-overlay { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(15, 23, 42, 0.5); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; +} + +.attach-modal { + background: #ffffff; + border-radius: 0.5rem; + width: 480px; + max-width: 92vw; + box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 8px 10px -6px rgba(0, 0, 0, 0.1); + padding: 1.5rem; + display: flex; + flex-direction: column; + color: #1e293b; + box-sizing: border-box; +} + +.attach-modal h4 { + margin: 0 0 1rem 0; + font-size: 1.15rem; + font-weight: 700; + color: #0f172a; +} + +.attach-modal .attach-modal-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.attach-modal .attach-modal-header h4 { + margin: 0; +} + +.attach-modal .attach-modal-header .attach-modal-icon { + font-size: 1.25rem; + color: #3b82f6; +} + +/* Emoji Picker Dropdown & Grid Styling */ +.emoji-picker-wrapper { + position: relative; + display: inline-flex; +} + +.emoji-picker { + position: absolute; + bottom: 48px; + left: 0; + z-index: 9999; + width: 320px; + background: #ffffff; + border: 1px solid #cbd5e1; + border-radius: 12px; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.15), 0 8px 10px -6px rgba(0, 0, 0, 0.1); + padding: 0.75rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + font-family: inherit; + + .emoji-search-row { + position: relative; + display: flex; + align-items: center; + + .emoji-search-icon { + position: absolute; + left: 0.6rem; + color: #94a3b8; + font-size: 0.85rem; + pointer-events: none; + } + + input.emoji-search-input { + width: 100%; + padding: 0.4rem 1.8rem 0.4rem 1.8rem; + font-size: 0.85rem; + border: 1px solid #cbd5e1; + border-radius: 6px; + outline: none; + background-color: #f8fafc; + transition: border-color 0.15s ease; + + &:focus { + border-color: #3b82f6; + background-color: #ffffff; + } + } + + .emoji-search-clear { + position: absolute; + right: 0.5rem; + background: transparent; + border: none; + color: #94a3b8; + cursor: pointer; + padding: 0.2rem; + font-size: 0.85rem; + + &:hover { + color: #ef4444; + } + } + } + + .emoji-categories { + display: flex; + justify-content: space-between; + padding-bottom: 0.4rem; + border-bottom: 1px solid #f1f5f9; + margin-bottom: 0.25rem; + + .emoji-cat-btn { + background: transparent !important; + border: none !important; + font-size: 1.1rem !important; + padding: 0.25rem 0.35rem !important; + border-radius: 6px !important; + cursor: pointer !important; + transition: background-color 0.15s ease, transform 0.1s ease !important; + line-height: 1 !important; + width: auto !important; + height: auto !important; + min-width: unset !important; + + &:hover { + background-color: #f1f5f9 !important; + transform: scale(1.15); + } + + &.active { + background-color: #e0f2fe !important; + border-radius: 6px !important; + } + } + } + + .emoji-grid { + display: grid !important; + grid-template-columns: repeat(8, 1fr) !important; + gap: 2px !important; + max-height: 220px !important; + overflow-y: auto !important; + padding-right: 2px !important; + + &::-webkit-scrollbar { + width: 5px; + } + &::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 3px; + } + + .emoji-btn { + background: transparent !important; + border: none !important; + font-size: 1.25rem !important; + padding: 0.35rem 0 !important; + cursor: pointer !important; + border-radius: 6px !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + width: auto !important; + height: auto !important; + min-width: unset !important; + transition: background-color 0.1s ease, transform 0.1s ease !important; + + &:hover { + background-color: #e2e8f0 !important; + transform: scale(1.2); + } + } + } +} + +/* Rightbar / Chat Message Context Menus */ +.rightbar-context-menu, +.chat-msg-context-menu { + z-index: 9999 !important; + background-color: #ffffff !important; + border: 1px solid #cbd5e1 !important; + border-radius: 8px !important; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.15), 0 8px 10px -6px rgba(0, 0, 0, 0.1) !important; + padding: 0.35rem 0 !important; + font-family: inherit !important; + overflow: hidden !important; + + .menu-item, + .context-menu-item { + display: flex !important; + align-items: center !important; + padding: 0.55rem 0.85rem !important; + font-size: 0.875rem !important; + font-weight: 500 !important; + color: #1e293b !important; + cursor: pointer !important; + transition: background-color 0.15s ease, color 0.15s ease !important; + user-select: none !important; + + &:hover { + background-color: #f1f5f9 !important; + color: #0284c7 !important; + } + + i { + font-size: 0.95rem !important; + width: 1.25rem !important; + text-align: center !important; + } + } +} + +.rightbar-context-menu { + position: absolute !important; + right: 10px !important; + min-width: 220px !important; +} + +.chat-msg-context-menu { + position: fixed !important; + right: auto !important; + width: max-content !important; + min-width: 180px !important; + max-width: calc(100vw - 16px) !important; + box-sizing: border-box !important; + + .context-menu-item { + white-space: nowrap !important; + } +} + diff --git a/webui-src/app/scss/pages/_config.scss b/webui-src/app/scss/pages/_config.scss index 19d1256d..83a15dd9 100644 --- a/webui-src/app/scss/pages/_config.scss +++ b/webui-src/app/scss/pages/_config.scss @@ -145,3 +145,157 @@ color: #1e293b; } +/* Network configuration uses desktop-sized inline styles. Override them on + * phones so labels, fields, proxy details, and long IP addresses stay inside + * the viewport. */ +@media (max-width: 700px) { + .config-network { + min-width: 0; + overflow-x: hidden; + } + + .config-network .widget { + min-width: 0; + padding: .8rem; + } + + .config-network .nw-config-row { + display: flex !important; + flex-direction: column !important; + align-items: stretch !important; + gap: .35rem !important; + min-width: 0; + } + + .config-network .nw-config-row > label, + .config-network .nw-config-row > p { + margin: 0 !important; + } + + .config-network .nw-mode-group, + .config-network .nat-control-group, + .config-network .addr-control-group, + .config-network .proxy-control-group, + .config-network .addr-port-group { + width: 100%; + min-width: 0; + gap: .5rem !important; + } + + .config-network input[type=text], + .config-network input[type=number], + .config-network select { + width: 100% !important; + max-width: none !important; + min-width: 0 !important; + box-sizing: border-box; + } + + .config-network .port-group, + .config-network .status-indicator { + margin-left: 0 !important; + } + + .config-network .port-group input[type=number] { + width: 90px !important; + } + + .config-network .external-address { + width: 100%; + height: auto; + max-height: 9rem; + padding-left: 1.25rem; + overflow: auto; + overflow-wrap: anywhere; + word-break: break-word; + box-sizing: border-box; + } +} + +/* Chat settings: replace the desktop grid/table with phone-friendly rows. */ +@media (max-width: 700px) { + .node-config .config-grid { + display: flex !important; + flex-direction: column !important; + align-items: stretch !important; + gap: .6rem !important; + min-width: 0; + box-sizing: border-box; + } + + .node-config .default-id-selector { + width: 100%; + min-width: 0; + } + + .node-config .default-id-selector select, + .node-config .config-grid > select { + width: 100% !important; + min-width: 0 !important; + max-width: none !important; + box-sizing: border-box; + } + + .node-config .storage-input-group { + justify-content: flex-start; + } + + .node-config .table-container { + overflow: visible !important; + } + + .node-config .history-config-table, + .node-config .history-config-table tbody, + .node-config .history-config-table tr, + .node-config .history-config-table td { + display: block; + width: 100% !important; + box-sizing: border-box; + } + + .node-config .history-config-table { + table-layout: auto; + } + + .node-config .history-config-table thead { + display: none; + } + + .node-config .history-config-table tr { + margin: 0; + padding: .75rem; + border-bottom: 1px solid #e2e8f0 !important; + } + + .node-config .history-config-table tr:last-child { + border-bottom: 0 !important; + } + + .node-config .history-config-table td { + padding: .25rem 0 !important; + text-align: left !important; + } + + .node-config .history-config-table td:nth-child(2), + .node-config .history-config-table td:nth-child(3) { + display: flex; + align-items: center; + justify-content: space-between; + gap: .75rem; + } + + .node-config .history-config-table td:nth-child(2)::before { + content: 'Enable history'; + color: #64748b; + font-size: .8rem; + font-weight: 600; + } + + .node-config .history-config-table td:nth-child(3)::before { + content: 'Max saved messages'; + color: #64748b; + font-size: .8rem; + font-weight: 600; + } +} + diff --git a/webui-src/app/scss/pages/_feedreader.scss b/webui-src/app/scss/pages/_feedreader.scss new file mode 100644 index 00000000..93cde756 --- /dev/null +++ b/webui-src/app/scss/pages/_feedreader.scss @@ -0,0 +1,31 @@ +.feedreader-page { height: 100%; display: flex; flex-direction: column; color: #333; } +.feedreader-toolbar, .feedreader-section-title, .feedreader-article-actions { display: flex; align-items: center; gap: .6rem; } +.feedreader-toolbar { padding: 1rem; border-bottom: 1px solid #ddd; h2 { margin-right: auto; } } +.feedreader-columns { min-height: 0; flex: 1; display: grid; grid-template-columns: minmax(190px,.8fr) minmax(260px,1fr) minmax(320px,1.7fr); } +.feedreader-tree, .feedreader-messages, .feedreader-reader { min-width: 0; overflow: auto; border-right: 1px solid #ddd; } +.feedreader-tree-item { + display: flex; gap: .55rem; align-items: center; padding-block: .65rem; cursor: pointer; + &:hover, &.selected { background: #e9f4fa; } + span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +} +.feedreader-icon-button { margin-left: auto; border: 0; background: transparent; } +.feedreader-section-title { padding: .8rem 1rem; border-bottom: 1px solid #ddd; justify-content: space-between; } +.feedreader-message-row { + display: flex; flex-direction: column; gap: .2rem; padding: .8rem 1rem; border-bottom: 1px solid #eee; cursor: pointer; + &.unread { border-left: 4px solid #3ba4d7; } + &.read { opacity: .72; } + &.selected, &:hover { background: #f2f8fb; } + time, span { color: #777; font-size: .85rem; } +} +.feedreader-reader { padding: 1.25rem; border-right: 0; img { max-width: 100%; height: auto; } } +.feedreader-article-meta { color: #777; font-size: .85rem; } +.feedreader-article-body { line-height: 1.55; margin-top: 1.5rem; overflow-wrap: anywhere; } +.feedreader-article-actions { margin-top: 1.5rem; } +.feedreader-placeholder { padding: 1.25rem; color: #777; } +.feedreader-error { padding: .7rem 1rem; color: #a00; background: #fee; } +.feedreader-add { padding: .8rem 1rem; display: flex; gap: .6rem; align-items: center; border-bottom: 1px solid #ddd; input { min-width: 12rem; } } +@media (max-width: 900px) { + .feedreader-columns { grid-template-columns: 1fr; overflow: auto; } + .feedreader-tree, .feedreader-messages, .feedreader-reader { min-height: 14rem; border-right: 0; border-bottom: 1px solid #ddd; } + .feedreader-toolbar, .feedreader-add { flex-wrap: wrap; } +} diff --git a/webui-src/app/scss/pages/_files.scss b/webui-src/app/scss/pages/_files.scss index c440100c..26a0a9ca 100644 --- a/webui-src/app/scss/pages/_files.scss +++ b/webui-src/app/scss/pages/_files.scss @@ -55,7 +55,9 @@ table.friendsfiles td { word-wrap: break-word; } table.friendsfiles th:nth-child(1) { - width: 2%; + width: 1.5rem; + padding-left: 0.25rem; + padding-right: 0; } table.friendsfiles th:nth-child(2) { width: 50%; @@ -65,6 +67,12 @@ table.friendsfiles th:nth-child(4) { } table.friendsfiles td:nth-child(2) { text-align: start; + padding-left: 0.25rem; +} +table.friendsfiles td:nth-child(1) { + width: 1.5rem; + padding-left: 0.25rem; + padding-right: 0; } // File Search @@ -173,6 +181,147 @@ table.friendsfiles td:nth-child(2) { } } +/* File search results use div rows, not table rows. Give those rows their + own grid instead of relying on the older table selectors above. */ +.file-search-container { + align-items: stretch; + min-height: 16rem; + padding: 1rem; + background: #fff; + box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06); + + &__keywords { + flex: 0 0 13rem; + padding: 0 1rem 0 0; + + .keywords-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + } + + .keywords-header h5 { + margin: 0; + font-size: 1rem; + } + + .clear-btn { + padding: 0.35rem 0.7rem; + } + + .keywords-container a { + padding: 0.45rem 0.55rem; + border-radius: 0.35rem; + font-size: 0.95rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &:hover, + &.selected { + background: rgba(0, 154, 235, 0.1); + color: $primary-color; + } + } + } + + &__results { + flex: 1 1 auto; + min-width: 0; + overflow: visible; + + > h5 { + margin: 0; + color: #64748b; + } + } +} + +.results-container { + width: 100%; + border: 1px solid #dbe3ec; + border-radius: 0.5rem; + overflow: hidden; +} + +.results-row { + display: grid; + grid-template-columns: minmax(12rem, 2fr) minmax(5.5rem, 0.6fr) minmax(12rem, 1.6fr) auto; + gap: 1rem; + align-items: center; +} + +.results-header { + background: #f1f5f9; + border-bottom: 1px solid #dbe3ec; + color: #475569; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.02em; + text-transform: uppercase; + + .results-row { + padding: 0.65rem 0.85rem; + } +} + +.results-list .file-item { + padding: 0.75rem 0.85rem; + border-bottom: 1px solid #edf2f7; + transition: background-color 0.15s ease; + + &:last-child { + border-bottom: 0; + } + + &:hover { + background: #f8fafc; + } +} + +.results-cell { + min-width: 0; +} + +.results-cell.name-col { + display: flex; + align-items: center; + gap: 0.55rem; + color: #0f172a; + font-weight: 600; + + i { + color: #0284c7; + font-size: 1.1rem; + } + + span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + +.results-cell.size-col { + color: #475569; + white-space: nowrap; +} + +.results-cell.hash-col { + overflow: hidden; + color: #64748b; + font-family: ui-monospace, SFMono-Regular, Consolas, monospace; + font-size: 0.78rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.download-btn-v65 { + padding: 0.45rem 0.75rem; + white-space: nowrap; +} + .shareManagerPopupOverlay { @include popupOverlay; .shareManagerPopup { @@ -356,6 +505,93 @@ table.friendsfiles td:nth-child(2) { background: white; } + /* Friends Files is a hierarchy, so keep its controls and details together + as a compact list row instead of stacking every table cell as a card. */ + table.friendsfiles tr { + display: grid; + grid-template-columns: 1.25rem minmax(0, 1fr) auto; + align-items: center; + column-gap: 0.35rem; + margin-bottom: 0.5rem; + padding: 0.65rem 0.5rem; + } + + table.friendsfiles td { + display: block; + width: auto !important; + margin: 0; + padding: 0 !important; + border: 0 !important; + } + + table.friendsfiles td:nth-child(1) { + grid-column: 1; + } + + table.friendsfiles td:nth-child(2) { + grid-column: 2; + left: 0 !important; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + table.friendsfiles td:nth-child(3) { + grid-column: 3; + white-space: nowrap; + } + + table.friendsfiles td:nth-child(4) { + grid-column: 2 / -1; + margin-top: 0.4rem; + } + + table.myfiles tr { + display: grid; + grid-template-columns: 1.25rem minmax(0, 1fr) auto; + align-items: center; + column-gap: 0.35rem; + margin-bottom: 0.5rem; + padding: 0.65rem 0.5rem; + } + + table.myfiles td { + display: block; + width: auto !important; + margin: 0; + padding: 0 !important; + border: 0 !important; + } + + table.myfiles td:nth-child(1) { + grid-column: 1; + } + + table.myfiles td:nth-child(2) { + grid-column: 2; + left: 0 !important; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + table.myfiles td:nth-child(3) { + grid-column: 3; + white-space: nowrap; + } + + .my-files__configure-shares { + width: 2.4rem; + min-width: 2.4rem; + padding: 0.55rem !important; + } + + .my-files__configure-shares span { + display: none; + } + /* Search Container Layout */ .file-search-container { flex-direction: column; @@ -393,4 +629,61 @@ table.friendsfiles td:nth-child(2) { margin-bottom: 0.5rem; word-break: break-all; } + + .search-form { + width: auto; + flex: 1; + max-width: 18rem; + } + + .file-search-container { + gap: 0.75rem; + padding: 0.75rem; + + &__keywords { + padding: 0 0 0.75rem; + margin: 0; + } + + &__results { + width: 100%; + } + } + + .results-header { + display: none; + } + + .results-list .file-item { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + grid-template-areas: + 'name action' + 'size hash'; + gap: 0.45rem 0.75rem; + padding: 0.8rem; + } + + .results-cell.name-col { + grid-area: name; + } + + .results-cell.size-col { + grid-area: size; + font-size: 0.82rem; + } + + .results-cell.hash-col { + grid-area: hash; + max-width: 10rem; + text-align: right; + } + + .results-cell.action-col { + grid-area: action; + } + + .download-btn-v65 { + padding: 0.4rem 0.6rem; + } } diff --git a/webui-src/app/scss/pages/_home.scss b/webui-src/app/scss/pages/_home.scss index 5090d038..f536a3a7 100644 --- a/webui-src/app/scss/pages/_home.scss +++ b/webui-src/app/scss/pages/_home.scss @@ -119,4 +119,63 @@ } } } +} + +/* Responsive Mobile Home Page Styles */ +@media (max-width: 768px) { + .homepage { + margin: 1rem auto !important; + padding: 0 1rem !important; + gap: 2rem !important; + max-width: 100% !important; + box-sizing: border-box !important; + + .logo { + flex-direction: column !important; + gap: 0.5rem !important; + text-align: center !important; + + & img { + width: 60px !important; + } + + .retroshareText { + .retrotext { + font-size: 1.6rem !important; + } + & > b { + font-size: 0.75rem !important; + } + } + } + + .certificate { + gap: 2rem !important; + + &__heading { + & > h1 { + font-size: 1.35rem !important; + margin-bottom: 0.5rem !important; + } + font-size: 0.85rem !important; + } + + &__content { + padding: 1rem 0.75rem !important; + gap: 1.25rem !important; + + .retroshareID { + padding: 0.5rem !important; + font-size: 0.85rem !important; + max-width: 100% !important; + + .textArea { + font-size: 0.8rem !important; + word-break: break-all !important; + overflow-wrap: anywhere !important; + } + } + } + } + } } \ No newline at end of file diff --git a/webui-src/app/scss/pages/_index.scss b/webui-src/app/scss/pages/_index.scss index 9e3bdd9c..c6bc4bf4 100644 --- a/webui-src/app/scss/pages/_index.scss +++ b/webui-src/app/scss/pages/_index.scss @@ -8,4 +8,5 @@ @forward "channel"; @forward "forums"; @forward "board"; +@forward "feedreader"; @forward "config"; diff --git a/webui-src/app/scss/pages/_mail.scss b/webui-src/app/scss/pages/_mail.scss index f6bd3753..2567268d 100644 --- a/webui-src/app/scss/pages/_mail.scss +++ b/webui-src/app/scss/pages/_mail.scss @@ -9,6 +9,28 @@ margin: 0.25rem; padding: 0.75rem 0; } + + .sidebar a, + .sidebarquickview a { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .sidebar-badge { + margin-left: auto; + background-color: #3b82f6; + color: #ffffff; + font-size: 0.725rem; + font-weight: 700; + padding: 0.15rem 0.45rem; + border-radius: 999px; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 1.2rem; + } } .compose-mail { @@ -376,3 +398,421 @@ table.attachment-container { } } } + +/* Modern Attachment Card Styling */ +.attachments-wrapper { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: 0.5rem; +} + +.attachment-card { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 0.5rem; + padding: 0.75rem 1rem; + display: flex; + align-items: center; + gap: 0.75rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); + + .attachment-icon { + width: 40px; + height: 40px; + border-radius: 0.5rem; + background: #eff6ff; + color: #3b82f6; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.2rem; + flex-shrink: 0; + } + + .attachment-info { + flex: 1; + min-width: 0; + + .attachment-name { + font-weight: 600; + font-size: 0.9rem; + color: #1e293b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .attachment-size { + font-size: 0.75rem; + color: #64748b; + } + } + + .btn-attachment-download { + padding: 0.4rem 0.75rem; + font-size: 0.8rem; + height: 34px; + } +} + +.mobile-fab-compose { + display: none; +} + +/* Mobile Responsiveness for Mail UI */ +@media (max-width: 768px) { + .side-bar { + width: 100% !important; + flex-direction: row !important; + overflow-x: auto !important; + overflow-y: hidden !important; + white-space: nowrap !important; + border-bottom: 1px solid #cbd5e1 !important; + padding: 0.5rem !important; + flex-shrink: 0 !important; + background: #ffffff !important; + } + + .side-bar .mail-compose-btn { + display: none !important; + } + + .side-bar .sidebar { + width: auto !important; + flex-direction: row !important; + gap: 0.25rem !important; + } + + .side-bar .sidebar a { + padding: 0.5rem 0.75rem !important; + font-size: 0.85rem !important; + border-radius: 0.375rem !important; + border-left: none !important; + border-bottom: none !important; + display: inline-flex !important; + align-items: center !important; + gap: 0.35rem !important; + } + + .side-bar .sidebar a .sidebar-link-text { + display: none !important; + } + + .side-bar .sidebar a i { + margin-right: 0 !important; + font-size: 1.1rem !important; + } + + .side-bar .sidebar a .sidebar-badge { + margin-left: 0 !important; + background: #3b82f6 !important; + color: #ffffff !important; + font-size: 0.7rem !important; + font-weight: 700 !important; + padding: 0.15rem 0.4rem !important; + border-radius: 999px !important; + line-height: 1 !important; + } + + .side-bar .sidebar a.selected-sidebar-link { + background-color: #e0f2fe !important; + color: #0369a1 !important; + border-left: none !important; + font-weight: 600 !important; + } + + .side-bar .sidebarquickview { + display: none !important; /* Hide quickview tags on mobile bar; filter via colored dropdown */ + } + + select.mail-tag { + padding: 0.45rem 0.75rem !important; + border-radius: 0.5rem !important; + border: 1px solid #cbd5e1 !important; + font-size: 0.85rem !important; + font-weight: 600 !important; + background-color: #ffffff !important; + color: #1e293b !important; + outline: none !important; + cursor: pointer !important; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05) !important; + } + + .node-panel { + width: 100% !important; + flex: 1 !important; + padding: 0.5rem !important; + overflow: auto !important; + } + + /* Mail List Table -> Mobile Card List View */ + .table-pagination-container table.mails { + display: block !important; + width: 100% !important; + } + + .table-pagination-container table.mails tr:first-child { + display: none !important; + } + + .table-pagination-container table.mails tbody { + display: flex !important; + flex-direction: column !important; + gap: 0.5rem !important; + } + + .table-pagination-container table.mails tr.msgbody { + display: flex !important; + flex-direction: column !important; + padding: 0.85rem !important; + background: #ffffff !important; + border: 1px solid #e2e8f0 !important; + border-radius: 0.65rem !important; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03) !important; + position: relative !important; + margin-bottom: 0.25rem !important; + transition: all 0.2s ease !important; + } + + .table-pagination-container table.mails tr.msgbody.unread { + background: #f0f9ff !important; + border-color: #bae6fd !important; + border-left: 4px solid #0284c7 !important; + } + + .table-pagination-container table.mails tr.msgbody td { + display: block !important; + padding: 0 !important; + border: none !important; + } + + /* Top Row: Sender Info & Date */ + .table-pagination-container table.mails tr.msgbody td.cell-from { + order: 1 !important; + margin-bottom: 0.35rem !important; + padding-right: 5rem !important; + } + + .table-pagination-container table.mails tr.msgbody td.cell-from div { + display: flex !important; + align-items: center !important; + gap: 0.5rem !important; + width: 100% !important; + } + + .table-pagination-container table.mails tr.msgbody td.cell-from div .jdenticon-avatar, + .table-pagination-container table.mails tr.msgbody td.cell-from div .defaultAvatar, + .table-pagination-container table.mails tr.msgbody td.cell-from div img.avatar { + flex-shrink: 0 !important; + width: 28px !important; + height: 28px !important; + min-width: 28px !important; + min-height: 28px !important; + aspect-ratio: 1 / 1 !important; + object-fit: cover !important; + border-radius: 50% !important; + } + + .table-pagination-container table.mails tr.msgbody td.cell-from div span { + font-weight: 700 !important; + font-size: 0.95rem !important; + color: #1e293b !important; + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + } + + .table-pagination-container table.mails tr.msgbody td.cell-date { + order: 2 !important; + position: absolute !important; + top: 0.85rem !important; + right: 0.85rem !important; + font-size: 0.75rem !important; + font-weight: 600 !important; + color: #64748b !important; + white-space: nowrap !important; + } + + /* Subject Row */ + .table-pagination-container table.mails tr.msgbody td.cell-subject { + order: 3 !important; + margin-bottom: 0.25rem !important; + font-size: 0.9rem !important; + color: #334155 !important; + } + + .table-pagination-container table.mails tr.msgbody td.cell-subject span { + font-weight: 600 !important; + } + + /* Star & Attachments */ + .table-pagination-container table.mails tr.msgbody td.cell-star { + order: 4 !important; + position: absolute !important; + bottom: 0.75rem !important; + right: 0.85rem !important; + } + + .table-pagination-container table.mails tr.msgbody td.cell-attachment { + order: 5 !important; + position: absolute !important; + bottom: 0.75rem !important; + right: 2.5rem !important; + color: #64748b !important; + } + + /* Mail Reader (MessageView) Mobile Layout */ + .msg-view { + padding: 0.5rem !important; + gap: 0.75rem !important; + } + + .msg-view .msg-view-nav { + background: #ffffff !important; + padding: 0.5rem 0.75rem !important; + border-radius: 0.5rem !important; + border: 1px solid #e2e8f0 !important; + } + + .msg-view .msg-view-nav__action button { + padding: 0.45rem 0.65rem !important; + min-width: 38px !important; + height: 38px !important; + justify-content: center !important; + } + + .msg-view .msg-view-nav__action button .btn-text { + display: none !important; + } + + .msg-view .msg-view-nav__action button i { + margin: 0 !important; + font-size: 1rem !important; + } + + .msg-view__header { + background: #ffffff !important; + padding: 1rem !important; + border-radius: 0.5rem !important; + border: 1px solid #e2e8f0 !important; + } + + .msg-view__header h3 { + font-size: 1.25rem !important; + font-weight: 800 !important; + color: #0f172a !important; + line-height: 1.3 !important; + } + + .msg-view__body { + background: #ffffff !important; + padding: 1rem !important; + border-radius: 0.5rem !important; + border: 1px solid #e2e8f0 !important; + font-size: 0.95rem !important; + line-height: 1.6 !important; + color: #1e293b !important; + } + + .msg-view__attachment { + height: auto !important; + } + + /* Floating Action Button (Compose Mail) */ + .mobile-fab-compose { + position: fixed !important; + bottom: 1.75rem !important; + right: 1.5rem !important; + width: 54px !important; + height: 54px !important; + border-radius: 50% !important; + background: linear-gradient(135deg, #3b82f6, #2563eb) !important; + color: #ffffff !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; + font-size: 1.35rem !important; + box-shadow: 0 4px 14px rgba(37, 99, 235, 0.45) !important; + border: none !important; + z-index: 1000 !important; + cursor: pointer !important; + transition: transform 0.2s ease !important; + } + + .mobile-fab-compose:active { + transform: scale(0.92) !important; + } + + .composePopupOverlay .composePopup { + width: 95% !important; + height: 95% !important; + } +} + +.mail-mobile-nav-toggle { display: none; } +.mail-nav-drawer { display: contents; } +.mail-mobile-box-title { display: none; } + +/* Mail folders become a left drawer on phones. */ +@media (max-width: 768px) { + .side-bar { + height: 44px !important; + padding: .25rem !important; + overflow: visible !important; + } + + .mail-mobile-nav-toggle { + display: inline-flex !important; + width: 40px; + height: 40px; + align-items: center; + justify-content: center; + border: 0; + border-radius: 6px; + background: #ffffff; + color: #0f172a; + cursor: pointer; + font-size: 1.15rem; + } + + .mail-nav-drawer { + position: fixed; + top: 0; + left: 0; + z-index: 1100; + display: flex !important; + flex-direction: column; + width: min(82vw, 300px); + height: 100dvh; + padding: 1rem 0; + overflow-y: auto; + background: #ffffff; + border-right: 1px solid #e2e8f0; + box-shadow: 8px 0 24px rgba(15, 23, 42, .16); + transform: translateX(-105%); + transition: transform 180ms ease; + } + + .mail-nav-drawer--open { transform: translateX(0); } + .mail-mobile-box-title { + display: flex !important; + align-items: center; + gap: .55rem; + margin: .35rem 0 .55rem; + color: #0f172a; + font-size: 1.45rem; + font-weight: 600; + } + .mail-mobile-box-title i { color: #3b82f6; font-size: 1.35rem; } + .mail-box-content > .widget__heading { display: none; } + .mail-nav-drawer .mail-compose-btn { display: flex !important; margin: 0 .85rem .75rem !important; } + .mail-nav-drawer .sidebar, + .mail-nav-drawer .sidebarquickview { display: flex !important; flex-direction: column !important; width: 100% !important; } + .mail-nav-drawer .sidebarquickview { margin-top: .5rem; border-top: 1px solid #e2e8f0; padding-top: .5rem; } + .mail-nav-drawer .sidebarquickview h6 { display: block !important; margin: 0 .9rem .35rem; color: #64748b; } + .mail-nav-drawer .sidebar a, + .mail-nav-drawer .sidebarquickview a { display: flex !important; width: auto; padding: .7rem 1rem !important; border-radius: 0 !important; } + .mail-nav-drawer .sidebar a .sidebar-link-text { display: inline !important; } + .mail-nav-drawer .sidebar a i { margin-right: .75rem !important; font-size: 1.1rem !important; } +} diff --git a/webui-src/app/scss/pages/_network.scss b/webui-src/app/scss/pages/_network.scss index ff1e9ce6..4ea223ab 100644 --- a/webui-src/app/scss/pages/_network.scss +++ b/webui-src/app/scss/pages/_network.scss @@ -31,6 +31,22 @@ display: flex; align-items: center; gap: 1rem; + + .profile-avatar-wrapper { + position: relative; + flex-shrink: 0; + + .status-dot { + position: absolute; + bottom: -1px; + right: -1px; + width: 13px; + height: 13px; + border-radius: 50%; + border: 2px solid #ffffff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25); + } + } } .profile-info { @@ -61,7 +77,7 @@ display: inline-block; width: 8px; height: 8px; - background-color: #10b981; + background-color: var(--profile-status-color, #10b981); border-radius: 50%; } } @@ -159,7 +175,19 @@ } .friend-avatar { + position: relative; flex-shrink: 0; + + .status-dot { + position: absolute; + bottom: -1px; + right: -1px; + width: 13px; + height: 13px; + border-radius: 50%; + border: 2px solid #ffffff; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25); + } } .friend-meta { @@ -543,3 +571,97 @@ } } } + +/* Responsive Layout for Mobile/Small Screens */ +@media (max-width: 768px) { + .network-container { + flex-direction: column !important; + } + + .network-left-pane { + width: 100% !important; + max-width: none !important; + height: 45% !important; + border-right: none !important; + border-bottom: 1px solid #cbd5e1 !important; + } + + .network-right-pane { + height: 55% !important; + flex: 1 !important; + } + + /* Hide text labels on mobile screens so profile action buttons show icons only */ + .detail-actions button .btn-text, + .detail-header .detail-actions button .btn-text { + display: none !important; + } + + .detail-actions button, + .detail-header .detail-actions button { + padding: 0.45rem 0.65rem !important; + min-width: 38px !important; + height: 38px !important; + justify-content: center !important; + align-items: center !important; + } + + .detail-actions button i, + .detail-header .detail-actions button i { + margin: 0 !important; + font-size: 1.05rem !important; + } + + .network-detail-view .detail-header { + flex-direction: column !important; + align-items: flex-start !important; + gap: 1rem !important; + + .friend-avatar { + margin-bottom: 0.25rem !important; + } + } + + .locations-grid { + grid-template-columns: 1fr !important; + } + + /* Direct-chat composer: preserve room for typing on a narrow screen. */ + .network-chat-view .chat-input-area { + padding: .4rem !important; + gap: .2rem !important; + min-width: 0; + } + + .network-chat-view .chat-input-area .chat-hub-action-btn, + .network-chat-view .chat-input-area label.chat-hub-action-btn { + width: 28px !important; + height: 28px !important; + min-width: 28px !important; + padding: 0 !important; + font-size: .95rem !important; + } + + .network-chat-view .chat-input-area .emoji-picker-wrapper { + flex: 0 0 28px; + } + + .network-chat-view .chat-input-area textarea.chat-textarea { + min-width: 0 !important; + padding: .4rem !important; + } + + .network-chat-view .chat-input-area .send-btn { + width: 34px !important; + min-width: 34px !important; + height: 32px !important; + padding: 0 !important; + font-size: 0 !important; + justify-content: center; + } + + .network-chat-view .chat-input-area .send-btn i { + margin: 0 !important; + font-size: 1rem !important; + } +} diff --git a/webui-src/app/scss/pages/_people.scss b/webui-src/app/scss/pages/_people.scss index d835421e..c49014a8 100644 --- a/webui-src/app/scss/pages/_people.scss +++ b/webui-src/app/scss/pages/_people.scss @@ -229,100 +229,200 @@ img.avatar { color: #0f172a; } +/* Main People Page Flex Layout Container */ .people-container { - display: flex; - height: calc(100vh - 55px); - width: 100%; - overflow: hidden; + display: flex !important; + flex-direction: row !important; + height: 100% !important; + width: 100% !important; + overflow: hidden !important; + background-color: #f1f5f9 !important; } +/* Left Sidebar Pane (320px fixed width on Desktop) */ .people-left-pane { - width: 320px; - border-right: 1px solid #cbd5e1; - display: flex; - flex-direction: column; - background-color: #ffffff; - overflow: hidden; + width: 320px !important; + min-width: 300px !important; + max-width: 350px !important; + height: 100% !important; + border-right: 1px solid #cbd5e1 !important; + display: flex !important; + flex-direction: column !important; + background: #ffffff !important; + box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05) !important; + flex-shrink: 0 !important; + overflow: hidden !important; } +/* Right Content Details Pane */ .people-right-pane { - flex: 1; - display: flex; - flex-direction: column; - overflow: hidden; - background-color: #f8fafc; + flex: 1 !important; + min-width: 0 !important; + height: 100% !important; + display: flex !important; + flex-direction: column !important; + overflow: hidden !important; + background-color: #f8fafc !important; } -.people-left-pane .chat-item { - display: flex; - align-items: center; - padding: 0.75rem 1rem; - gap: 0.75rem; - border-bottom: 1px solid #f1f5f9; - cursor: pointer; - transition: background-color 0.15s ease; - position: relative; +/* Scrollable list section inside left sidebar */ +.people-list-container { + flex: 1 !important; + overflow-y: auto !important; + padding: 0.5rem 0 !important; } -.people-left-pane .chat-item:hover { - background-color: #f8fafc; -} +/* Active Chats list item */ +.chat-item { + display: flex !important; + align-items: center !important; + gap: 0.75rem !important; + padding: 0.65rem 0.85rem !important; + margin: 0.2rem 0.5rem !important; + border-radius: 0.5rem !important; + cursor: pointer !important; + transition: all 0.2s ease !important; + position: relative !important; + + &:hover { + background-color: #f1f5f9 !important; + } -.people-left-pane .chat-item.selected { - background-color: #eff6ff; - border-left: 3px solid #3b82f6; -} + &.selected { + background-color: #e0f2fe !important; -.people-left-pane .chat-item .chat-avatar-wrapper { - position: relative; - flex-shrink: 0; -} + .chat-name { + color: #0369a1 !important; + font-weight: 700 !important; + } + } -.people-left-pane .chat-item .chat-avatar-wrapper .status-dot { - position: absolute; - bottom: 0; - right: 0; - width: 10px; - height: 10px; - border-radius: 50%; - border: 2px solid #ffffff; -} + .chat-avatar-wrapper { + position: relative !important; + flex-shrink: 0 !important; + + .status-dot { + position: absolute !important; + bottom: -1px !important; + right: -1px !important; + width: 13px !important; + height: 13px !important; + border-radius: 50% !important; + border: 2px solid #ffffff !important; + } + } -.people-left-pane .chat-item .chat-info { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - gap: 0.15rem; -} + .chat-info { + flex: 1 !important; + min-width: 0 !important; + display: flex !important; + flex-direction: column !important; + + .chat-name { + font-size: 0.95rem !important; + font-weight: 600 !important; + color: #1e293b !important; + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + } -.people-left-pane .chat-item .chat-info .chat-name { - font-size: 0.9rem; - font-weight: 700; - color: #1e293b; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} + .chat-last-msg { + font-size: 0.825rem !important; + color: #64748b !important; + white-space: nowrap !important; + overflow: hidden !important; + text-overflow: ellipsis !important; + margin-top: 0.1rem !important; + } + } + + .chat-meta { + display: flex !important; + flex-direction: column !important; + align-items: flex-end !important; + flex-shrink: 0 !important; -.people-left-pane .chat-item .chat-info .chat-last-msg { - font-size: 0.8rem; - color: #64748b; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + .chat-time { + font-size: 0.75rem !important; + color: #94a3b8 !important; + font-weight: 500 !important; + } + } } -.people-left-pane .chat-item .chat-meta { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 0.25rem; - flex-shrink: 0; +/* Responsive Layout for Mobile/Small Screens */ +@media (max-width: 768px) { + .people-container { + flex-direction: column !important; + } + .people-left-pane { + width: 100% !important; + max-width: none !important; + height: 45% !important; + border-right: none !important; + border-bottom: 1px solid #cbd5e1 !important; + } + .people-right-pane { + height: 55% !important; + } + + /* Hide text labels on mobile screens so profile action buttons show icons only */ + .detail-actions button .btn-text, + .detail-header .detail-actions button .btn-text { + display: none !important; + } + + /* Hide Distant Chat Tunnel and Chatting as text on mobile screens to save header space */ + .chat-tunnel-status .tunnel-label, + .select-own-profile .chatting-as-label { + display: none !important; + } + + .detail-actions button, + .detail-header .detail-actions button { + padding: 0.45rem 0.65rem !important; + min-width: 38px !important; + height: 38px !important; + justify-content: center !important; + align-items: center !important; + } } -.people-left-pane .chat-item .chat-meta .chat-time { - font-size: 0.75rem; - color: #94a3b8; - white-space: nowrap; +/* People Sidebar Right-Click Context Menu */ +.people-context-menu { + position: absolute !important; + z-index: 9999 !important; + background-color: #ffffff !important; + border: 1px solid #cbd5e1 !important; + border-radius: 8px !important; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.15), 0 8px 10px -6px rgba(0, 0, 0, 0.1) !important; + padding: 0.35rem 0 !important; + min-width: 180px !important; + font-family: inherit !important; + overflow: hidden !important; + + .menu-item { + display: flex !important; + align-items: center !important; + padding: 0.6rem 0.9rem !important; + font-size: 0.875rem !important; + font-weight: 500 !important; + color: #1e293b !important; + cursor: pointer !important; + transition: background-color 0.15s ease, color 0.15s ease !important; + user-select: none !important; + + &:hover { + background-color: #f1f5f9 !important; + color: #0284c7 !important; + } + + i { + font-size: 1rem !important; + width: 1.25rem !important; + text-align: center !important; + } + } } + diff --git a/webui-src/app/statusbar.js b/webui-src/app/statusbar.js index a9156bdc..7769a5ec 100644 --- a/webui-src/app/statusbar.js +++ b/webui-src/app/statusbar.js @@ -346,25 +346,36 @@ const StatusBar = { } return m('.statusbar', [ - m('.statusbar-left', { style: 'display: flex; align-items: center; gap: 0.75rem;' }, [ + m('.statusbar-left', [ m('.statusbar-item', [ - m('i.fas.fa-users', { style: 'margin-right: 0.5rem; color: #94a3b8;' }), - m('span', `Friends: ${State.onlineCount}/${State.friendCount}`), + m('i.fas.fa-users', { style: 'margin-right: 0.35rem; color: #94a3b8;' }), + m('span.statusbar-label', 'Friends:\u00a0'), + m('span.statusbar-value', `${State.onlineCount}/${State.friendCount}`), ]), // NAT — hidden when in hidden/darknet mode (same as Qt) !isHiddenMode && m('.statusbar-divider'), - !isHiddenMode && m('.statusbar-item', { title: natTooltip, style: 'cursor: help;' }, [ - m('span', { style: 'margin-right: 0.5rem;' }, 'NAT:'), - m('.status-bullet', { style: { backgroundColor: natColor } }), + !isHiddenMode && m('.statusbar-item.statusbar-item--nat', { + title: natTooltip, + style: 'cursor: help; margin-left: 0.6rem;', + }, [ + m('span.statusbar-label', { style: 'margin-right: 0.35rem;' }, 'NAT:'), + m('.status-bullet', { + style: { backgroundColor: natColor, marginLeft: '0.15rem', marginRight: '0.45rem' }, + }), ]), // DHT — hidden when in hidden/darknet mode (same as Qt) !isHiddenMode && m('.statusbar-divider'), - !isHiddenMode && m('.statusbar-item', { title: dhtTooltip, style: 'cursor: help;' }, [ - m('span', { style: 'margin-right: 0.5rem;' }, 'DHT:'), - m('.status-bullet', { style: { backgroundColor: dhtColor } }), - State.dhtActive && State.dhtOk && m('span', { style: 'margin-left: 0.5rem;' }, `${formatUnit(State.dhtRsNetSize)} (${formatUnit(State.dhtNetSize)})`), + !isHiddenMode && m('.statusbar-item.statusbar-item--dht', { + title: dhtTooltip, + style: 'cursor: help; margin-left: 0.6rem;', + }, [ + m('span.statusbar-label', { style: 'margin-right: 0.35rem;' }, 'DHT:'), + m('.status-bullet', { + style: { backgroundColor: dhtColor, marginLeft: '0.15rem', marginRight: '0.35rem' }, + }), + State.dhtActive && State.dhtOk && m('span.statusbar-extra-info', { style: 'margin-left: 0.35rem;' }, `${formatUnit(State.dhtRsNetSize)} (${formatUnit(State.dhtNetSize)})`), ]), // Tor / I2P — only shown when in hidden/darknet mode (same as Qt) @@ -379,23 +390,25 @@ const StatusBar = { ]), // RatesStatus — Bandwidth speeds & total cumulative transfer (Down | Up) - m('.statusbar-right', { style: 'display: flex; align-items: center; gap: 0.75rem;' }, [ + m('.statusbar-right', [ m('.statusbar-item', { title: `Downloaded: ${formatBytes(State.totalIn)}`, - style: 'cursor: help; display: flex; align-items: center;' + style: 'cursor: help;' }, [ - m('i.fas.fa-arrow-down', { style: 'color: #22c55e; margin-right: 0.35rem;' }), - m('span', `Down: ${State.rateIn.toFixed(2)} kB/s`), - m('span', { style: 'color: #64748b; font-size: 0.8rem; margin-left: 0.35rem;' }, `(${formatBytes(State.totalIn)})`), + m('i.fas.fa-arrow-down', { style: 'color: #22c55e; margin-right: 0.25rem;' }), + m('span.statusbar-label', 'Down:\u00a0'), + m('span.statusbar-value', `${State.rateIn.toFixed(1)} kB/s`), + m('span.statusbar-total-bytes', { style: 'color: #64748b; font-size: 0.8rem; margin-left: 0.25rem;' }, `(${formatBytes(State.totalIn)})`), ]), m('.statusbar-divider'), m('.statusbar-item', { title: `Uploaded: ${formatBytes(State.totalOut)}`, - style: 'cursor: help; display: flex; align-items: center;' + style: 'cursor: help;' }, [ - m('i.fas.fa-arrow-up', { style: 'color: #3b82f6; margin-right: 0.35rem;' }), - m('span', `Up: ${State.rateOut.toFixed(2)} kB/s`), - m('span', { style: 'color: #64748b; font-size: 0.8rem; margin-left: 0.35rem;' }, `(${formatBytes(State.totalOut)})`), + m('i.fas.fa-arrow-up', { style: 'color: #3b82f6; margin-right: 0.25rem;' }), + m('span.statusbar-label', 'Up:\u00a0'), + m('span.statusbar-value', `${State.rateOut.toFixed(1)} kB/s`), + m('span.statusbar-total-bytes', { style: 'color: #64748b; font-size: 0.8rem; margin-left: 0.25rem;' }, `(${formatBytes(State.totalOut)})`), ]), ]), ]); diff --git a/webui-src/app/widgets.js b/webui-src/app/widgets.js index bad711e1..060c4c3d 100644 --- a/webui-src/app/widgets.js +++ b/webui-src/app/widgets.js @@ -1,22 +1,55 @@ const m = require('mithril'); const Sidebar = () => { - let active = 0; + let mobileOpen = false; + let isMobileWidth = false; + let widthQuery; + let onWidthChange; + + const links = (v) => v.attrs.tabs.map((panelName) => { + const href = v.attrs.baseRoute + panelName; + const selected = m.route.get().toLowerCase().startsWith(href.toLowerCase()); + return m('a', { + class: selected ? 'selected-sidebar-link' : '', + href, + onclick: (event) => { + event.preventDefault(); + mobileOpen = false; + m.route.set(href); + }, + }, panelName); + }); + return { - view: (v) => - m( - '.sidebar', - v.attrs.tabs.map((panelName, index) => - m( - m.route.Link, - { - class: index === active ? 'selected-sidebar-link' : '', - onclick: () => (active = index), - href: v.attrs.baseRoute + panelName, - }, - panelName - ) - ) - ), + oninit: () => { + widthQuery = window.matchMedia('(max-width: 700px)'); + isMobileWidth = widthQuery.matches; + onWidthChange = (event) => { + isMobileWidth = event.matches; + if (!isMobileWidth) mobileOpen = false; + m.redraw(); + }; + if (widthQuery.addEventListener) widthQuery.addEventListener('change', onWidthChange); + else widthQuery.addListener(onWidthChange); + }, + onremove: () => { + if (!widthQuery || !onWidthChange) return; + if (widthQuery.removeEventListener) widthQuery.removeEventListener('change', onWidthChange); + else widthQuery.removeListener(onWidthChange); + }, + view: (v) => { + if (!v.attrs.mobileDrawer || !isMobileWidth) return m('.sidebar', links(v)); + return m('.sidebar-drawer', [ + m('button.sidebar-mobile-toggle[type=button][aria-label=Open navigation]', { + 'aria-expanded': mobileOpen, + onclick: () => { mobileOpen = !mobileOpen; }, + }, m('i.fas.fa-bars')), + mobileOpen ? m('.sidebar-drawer__backdrop', { onclick: () => { mobileOpen = false; } }) : null, + m('.sidebar', { class: mobileOpen ? 'sidebar--mobile-open' : '' }, [ + m('.sidebar-drawer__title', 'Navigation'), + ...links(v), + ]), + ]); + }, }; }; const SidebarQuickView = () => { diff --git a/webui-src/styles.css b/webui-src/styles.css index a9de9cbb..53fba7c3 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 rgb(0,94.5826771654,154)}button:active{outline:none;box-shadow:inset 3px 3px 0 rgb(0,94.5826771654,154)}button.red{width:max-content;height:max-content;color:#fff;background:#ff3a4a;font-size:1rem;padding:.4rem 1rem;border:0;border-radius:5px;cursor:pointer;box-shadow:inset -3px -3px 0 rgb(211,0,17.1370558376)}button.red:active{outline:none;box-shadow:inset 3px 3px 0 rgb(211,0,17.1370558376)}.media-item{display:flex;margin-top:.5rem;padding:1rem;border:1px solid rgba(20,20,27,.1);border-radius:4px}.media-item__details{flex-basis:40%;display:flex;align-items:start;gap:.5rem}.media-item__details img{width:6rem;object-fit:contain}.media-item__desc{flex-basis:60%}.active-link{background:hsla(0,0%,100%,.1) !important}.nav-menu{background-color:#14141b;box-shadow:0 5px 5px #222;display:flex;flex-direction:column;align-items:center;height:100%;padding:.25rem;margin-right:0rem}.nav-menu__logo{padding:1.2rem 0;display:flex;align-items:center;gap:.3rem}.nav-menu__logo img{width:1.6rem}.nav-menu__logo h5{line-height:1;color:#fff}.nav-menu__box{padding:2rem .125rem;display:flex;flex-direction:column;gap:.5rem;position:relative}.nav-menu__box .item{margin:0;padding:.675rem .5rem;width:10rem;display:flex;align-items:center;line-height:1;border-radius:.5rem;text-decoration:none;color:#ccc;text-transform:capitalize;transition:0ms}.nav-menu__box .item:hover{background-color:rgba(238,243,246,.15)}.nav-menu__box .item i.sidenav-icon{width:2.5rem;height:1.4rem;display:grid;place-items:center}.nav-menu__box .item.item-selected{color:#9bdaff;background-color:rgba(155,218,255,.15);font-weight:medium}.nav-menu__box button.toggle-nav{display:none;position:absolute;padding:0;top:0;right:-1rem;background:rgb(77.5,186.5157480315,255);width:1.5rem;height:1.5rem;aspect-ratio:1;justify-content:center;align-items:center;border-radius:50%;box-shadow:none}.nav-menu.collapsed .nav-menu__logo .logo-container{display:flex;flex-direction:column;align-items:center;gap:.5rem}.nav-menu.collapsed .nav-menu__logo .logo-container>*:not(img){display:block}.nav-menu.collapsed .nav-menu__logo .nav-menu__logo-text{display:none !important}.nav-menu.collapsed .nav-menu__box .item{padding:.675rem 0;width:2.5rem;justify-content:center;transition:300ms}.nav-menu.collapsed .nav-menu__box .item span,.nav-menu.collapsed .nav-menu__box .item p{display:none !important}.nav-menu.collapsed button i{rotate:180deg}.nav-menu:hover button.toggle-nav{display:flex}.sidebar{width:13rem;background-color:#fff;display:flex;flex-direction:column}.sidebar a{text-decoration:none;text-transform:capitalize;padding:1rem;cursor:pointer;color:#999}.sidebar a:hover{color:#222}.sidebar .selected-sidebar-link{font-weight:bold;color:#222;border-left:5px solid #3ba4d7;animation:expand-left-border .1s}.sidebarquickview>h6{padding:.5rem}.sidebarquickview a{text-decoration:none;text-transform:capitalize;padding:.5rem 1rem;display:block;color:#999}.sidebarquickview a a:hover{color:#222}.sidebarquickview .selected-sidebarquickview-link{font-weight:bold;color:#222;border-left:5px solid #3ba4d7;animation:expand-left-border .1s}.node-panel{width:100%;padding:.5rem;animation:fadein .5s}@keyframes expand-left-border{from{border-left:0}to{border-left:5px solid #3ba4d7}}@media(max-width: 700px){.tab-content{flex-direction:column}.sidebar{width:100% !important;flex-direction:row !important;overflow-x:auto !important;overflow-y:hidden !important;white-space:nowrap !important;border-bottom:1px solid rgba(20,20,27,.1) !important;background:#fff !important;z-index:50 !important;flex-shrink:0 !important;height:auto !important;padding:0 !important}.sidebar a{display:inline-block !important;padding:.8rem 1.2rem !important;border-bottom:3px solid rgba(0,0,0,0) !important;border-left:none !important}.sidebar .selected-sidebar-link{border-left:none !important;border-bottom:3px solid #3ba4d7 !important;animation:none !important}.sidebarquickview>h4,.sidebarquickview>h6{display:none !important}}.posts{height:100%;margin-top:1rem;flex-direction:column;overflow:auto}.posts__heading{display:flex;flex-direction:column;justify-content:space-between}.posts-container{height:100%;padding:1rem;display:grid;grid-template-columns:repeat(auto-fill, minmax(150px, 1fr));gap:2rem;border:1px solid rgba(20,20,27,.1);border-radius:4px;overflow:auto}.posts-container-card{min-height:240px;flex-direction:column;border:1px solid rgba(20,20,27,.5);border-radius:4px;cursor:pointer;text-align:center}.posts-container-card img{flex-basis:90%;object-fit:cover}.posts-container-card p{padding:0 .125rem;flex-basis:10%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.progress-bar{width:100%;height:2rem;position:relative;text-align:center;background-color:#eef3f6;border-radius:20px;overflow:hidden}.progress-bar__status{position:absolute;top:0;left:0;height:100%;color:#14141b;background-color:#019dff}.progress-bar__percent{position:absolute;inset:0;margin:auto;width:fit-content;height:fit-content}.progress-bar-chunks{position:relative;margin-top:.5rem;width:100%;height:2rem;display:flex;border-radius:.25rem;overflow:hidden;background-color:#eef3f6}.progress-bar-chunks .chunk{width:100%}.progress-bar-chunks .chunk[data-chunkVal="0"]{background-color:rgba(155,218,255,.2)}.progress-bar-chunks .chunk[data-chunkVal="1"]{background-color:#ff3a4a}.progress-bar-chunks .chunk[data-chunkVal="2"]{background-color:#019dff}.progress-bar-chunks .chunk[data-chunkVal="3"]{background-color:#fcba03}.progress-bar-chunks__percent{position:absolute;inset:0;margin:auto;width:fit-content;height:fit-content}.statusbar{display:flex;justify-content:space-between;align-items:center;height:28px;background-color:#14141b;border-top:1px solid #2e2e38;padding:0 1rem;font-size:.8rem;color:#94a3b8;z-index:100;box-sizing:border-box;user-select:none;flex-shrink:0}.statusbar-left{display:flex;align-items:center}.statusbar-right{display:flex;align-items:center;gap:1.5rem}.statusbar-item{display:flex;align-items:center}.statusbar-divider{width:1px;height:14px;background-color:#2e2e38}.status-bullet{width:8px;height:8px;border-radius:50%;display:inline-block;box-shadow:0 0 4px rgba(0,0,0,.5)}.widget{height:100%;padding:1rem;display:flex;flex-direction:column;gap:.5rem;background-color:#fff;border-radius:.5rem;overflow:auto}.widget .top-heading{display:flex;justify-content:space-between}.widget__heading{display:flex;justify-content:space-between;align-items:center;border-bottom:2px solid #999}.widget__body{height:100%;display:flex;flex-direction:column;overflow:auto}.widget__body-heading{display:flex;justify-content:space-between;align-items:center}.widget__body-heading .action{display:flex;gap:.5rem}.widget__body-content{height:100%;overflow:auto}.widget__body-box{display:flex;flex-direction:column;gap:.5rem}.widget-half{max-width:50%}#modal-container{display:none;position:fixed;z-index:1;height:100%;top:0;left:0;width:100%;background-color:rgba(0,0,0,.2)}.modal-content{position:absolute;color:#555;width:40%;min-height:10rem;height:max-content;padding:1.5rem;inset:0;margin:auto;background-color:#fff;border-radius:.5rem;animation:fadein .5s;display:flex;flex-direction:column}.modal-content button:last-child{margin-top:auto}.modal-content .close-btn{position:absolute;right:1.5rem}.modal-content .widget{padding:0}#notification-container{position:absolute;bottom:0;right:0}.login-page{background-image:linear-gradient(-45deg, rgba(1, 157, 255, 0.75), rgba(17, 143, 204, 0.75));height:100%;animation:fadein .5s}.login-page .login-container{background-color:#fff;box-shadow:3px 3px 5px rgba(20,20,27,.4);margin:auto;position:relative;top:100px;max-width:400px;max-height:500px;border-radius:5px;display:flex;flex-direction:column;align-items:center}.login-page .login-container input{padding:.375rem .75rem;border-radius:.275rem}.login-page .login-container *{margin-bottom:1rem}.login-page .login-container>img{margin:1rem 0 2rem}.login-page .login-container extra{margin:0}.login-page .login-container>a{text-decoration:underline;cursor:pointer}.login-page .extra>label,.login-page .extra>br,.login-page .extra>input{margin-bottom:0}.homepage{margin:2rem auto 0;display:flex;flex-direction:column;gap:4rem}.homepage .logo{display:flex;justify-content:center;align-items:center}.homepage .logo img{width:90px}.homepage .logo .retroshareText{display:flex;flex-direction:column;align-items:center}.homepage .logo .retroshareText .retrotext{font-size:36px;font-weight:600;line-height:1.125}.homepage .logo .retroshareText .retrotext>span{color:#118fcc}.homepage .logo .retroshareText>b{font-size:14px;line-height:1}.homepage .certificate{display:flex;flex-direction:column;gap:4rem}.homepage .certificate__heading{text-align:center}.homepage .certificate__heading>h1{margin-bottom:1rem}.homepage .certificate__content{display:flex;flex-direction:column;gap:2rem;padding:2rem;text-align:center;border:1.5px solid rgba(17,143,204,.2);border-radius:6px;box-shadow:0px 0px 8px 2px rgba(20,20,27,.05)}.homepage .certificate__content .rsId>p{margin-bottom:.5rem;color:#118fcc}.homepage .certificate__content .retroshareID{padding:.25rem;display:flex;align-items:center;justify-self:start;font-size:1.25rem;border-radius:4px;background:rgba(20,20,27,.05)}.homepage .certificate__content .retroshareID .textArea{padding:0;width:100%;height:auto;font-size:1rem;font-family:monospace;background:rgba(0,0,0,0);border:none;resize:none;overflow:hidden;field-sizing:content}.homepage .certificate__content .retroshareID i{color:#118fcc}.homepage .certificate__content .retroshareID>i{margin:0 .5rem;cursor:pointer}.homepage .certificate__content .webhelp{padding:.5rem;background:#f5f5f5;display:flex;justify-content:center;align-items:center;gap:.5rem;border-radius:4px;border:1px solid rgba(20,20,27,.5);width:fit-content;cursor:pointer}.homepage .certificate__content .webhelp-container{display:grid;place-items:center}.homepage .certificate__content .webhelp:hover{background:#eef3f6;border:1px solid #14141b}.homepage .certificate__content .webhelp>i{font-size:1.2rem;color:green}.homepage .certificate__content .add-friend>h6,.homepage .certificate__content .webhelp-container>h6{font-weight:normal;margin-bottom:.5rem}.network-container{display:flex;height:100%;width:100%;overflow:hidden;background-color:#f1f5f9}.network-left-pane{width:320px;min-width:300px;max-width:350px;border-right:1px solid #cbd5e1;display:flex;flex-direction:column;background:#fff;box-shadow:2px 0 5px rgba(0,0,0,.05)}.own-profile-card{padding:1.25rem;border-bottom:1px solid #e2e8f0;background:linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);display:flex;flex-direction:column;gap:.75rem}.own-profile-card .profile-header{display:flex;align-items:center;gap:1rem}.own-profile-card .profile-info{display:flex;flex-direction:column;flex:1;overflow:hidden}.own-profile-card .profile-info .profile-name{font-weight:700;color:#1e293b;font-size:1.1rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.own-profile-card .profile-info .profile-status{font-size:.85rem;color:#10b981;font-weight:500;display:flex;align-items:center;gap:.35rem}.own-profile-card .profile-info .profile-status::before{content:"";display:inline-block;width:8px;height:8px;background-color:#10b981;border-radius:50%}.own-profile-card .own-identity-select-container{display:flex;flex-direction:column;gap:.25rem}.own-profile-card .own-identity-select-container label{font-size:.75rem;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:.05em}.own-profile-card .own-identity-select-container select.own-identity-select{width:100%;padding:.375rem .5rem;font-size:.85rem;border:1px solid #cbd5e1;border-radius:.375rem;background-color:#fff;color:#334155;outline:none;cursor:pointer;transition:border-color .2s}.own-profile-card .own-identity-select-container select.own-identity-select:focus{border-color:#3ba4d7}.friends-list-container{flex:1;display:flex;flex-direction:column;overflow:hidden;position:relative}.friends-list-container .searchbar-container{padding:.75rem 1rem;border-bottom:1px solid #e2e8f0}.friends-list-container .searchbar-container input.searchbar{width:100%;padding:.5rem .75rem;font-size:.9rem;border:1px solid #cbd5e1;border-radius:.375rem;background-color:#f8fafc;outline:none;transition:all .2s}.friends-list-container .searchbar-container input.searchbar:focus{background-color:#fff;border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.friends-list-container .friends-scroll{flex:1;overflow-y:auto;padding:.5rem 0}.friend-list-item{display:flex;align-items:center;gap:.75rem;padding:.75rem 1rem;margin:.125rem .5rem;border-radius:.5rem;cursor:pointer;transition:all .2s}.friend-list-item:hover{background-color:#f1f5f9}.friend-list-item.selected{background-color:#e0f2fe}.friend-list-item.selected .friend-name{color:#0369a1;font-weight:600}.friend-list-item .friend-avatar{flex-shrink:0}.friend-list-item .friend-meta{flex:1;min-width:0}.friend-list-item .friend-meta .friend-name{font-size:.95rem;color:#334155;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .2s}.friend-list-item .friend-meta .friend-status{font-size:.8rem;color:#94a3b8}.friend-list-item .friend-meta .friend-status.online{color:#10b981;font-weight:500}.network-right-pane{flex:1;display:flex;flex-direction:column;overflow:hidden;background-color:#f8fafc}.network-pane-placeholder{flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;color:#94a3b8;gap:1rem;padding:2rem;text-align:center}.network-pane-placeholder i{font-size:4rem;color:#cbd5e1}.network-pane-placeholder p{font-size:1.1rem;max-width:400px}.network-tabs{display:flex;background-color:#fff;border-bottom:1px solid #cbd5e1;padding:.5rem 1rem 0;gap:.5rem}.network-tabs .tab-btn{padding:.625rem 1.25rem;font-size:.95rem;font-weight:600;color:#64748b;background:rgba(0,0,0,0);border:none;border-radius:.375rem .375rem 0 0;border-bottom:3px solid rgba(0,0,0,0);cursor:pointer;box-shadow:none;transition:all .2s}.network-tabs .tab-btn:hover{color:#334155;background-color:#f1f5f9}.network-tabs .tab-btn.active{color:#3ba4d7;border-bottom-color:#3ba4d7;background-color:rgba(0,0,0,0)}.network-tab-content{flex:1;overflow-y:auto;padding:1.5rem}.network-detail-view{display:flex;flex-direction:column;gap:1.5rem}.network-detail-view .detail-header{display:flex;align-items:center;gap:1.5rem;padding-bottom:1.5rem;border-bottom:1px solid #e2e8f0}.network-detail-view .detail-header .detail-title{flex:1}.network-detail-view .detail-header .detail-title h2{font-size:1.75rem;font-weight:800;color:#1e293b;margin-bottom:.25rem}.network-detail-view .detail-header .detail-title .detail-subtitle{font-size:.9rem;color:#64748b;display:flex;align-items:center;gap:.5rem}.network-detail-view .detail-header .detail-actions{display:flex;gap:.75rem}.network-detail-view .detail-header .detail-actions button{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.9rem}.network-detail-view .detail-section{background-color:#fff;border-radius:.5rem;border:1px solid #e2e8f0;padding:1.25rem;box-shadow:0 1px 3px rgba(0,0,0,.02)}.network-detail-view .detail-section h3{font-size:1.1rem;font-weight:700;color:#334155;margin-bottom:1rem;padding-bottom:.5rem;border-bottom:1px solid #f1f5f9}.network-detail-view .detail-section .info-grid{display:grid;grid-template-columns:120px 1fr;row-gap:.75rem;font-size:.9rem}.network-detail-view .detail-section .info-grid .info-label{font-weight:600;color:#64748b}.network-detail-view .detail-section .info-grid .info-value{color:#1e293b;word-break:break-all}.network-detail-view .locations-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));gap:1rem}.network-detail-view .location-card{background-color:#fff;border:1px solid #e2e8f0;border-radius:.5rem;padding:1rem;display:flex;flex-direction:column;gap:.5rem;box-shadow:0 1px 3px rgba(0,0,0,.02)}.network-detail-view .location-card .loc-header{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #f1f5f9;padding-bottom:.5rem;margin-bottom:.25rem}.network-detail-view .location-card .loc-header .loc-name{font-weight:700;color:#334155;font-size:.95rem}.network-detail-view .location-card .loc-header .loc-status{font-size:.75rem;font-weight:600;padding:.125rem .5rem;border-radius:.25rem}.network-detail-view .location-card .loc-header .loc-status.online{background-color:#d1fae5;color:#065f46}.network-detail-view .location-card .loc-header .loc-status.offline{background-color:#f1f5f9;color:#475569}.network-detail-view .location-card .loc-body{font-size:.85rem;display:grid;grid-template-columns:80px 1fr;row-gap:.25rem}.network-detail-view .location-card .loc-body .loc-label{color:#64748b}.network-detail-view .location-card .loc-body .loc-val{color:#334155;word-break:break-all}.network-detail-view .location-card .loc-footer{margin-top:.5rem;display:flex;justify-content:flex-end}.network-detail-view .location-card .loc-footer button{font-size:.8rem;padding:.25rem .75rem}.network-chat-view{display:flex;flex-direction:column;height:100%;overflow:hidden;background-color:#f8fafc}.network-chat-view .chat-messages{flex:1;overflow-y:auto;padding:1.25rem;display:flex;flex-direction:column;gap:1rem}.network-chat-view .chat-bubble-container{display:flex;flex-direction:column;max-width:70%}.network-chat-view .chat-bubble-container.outgoing{align-self:flex-end;align-items:flex-end}.network-chat-view .chat-bubble-container.outgoing .chat-bubble{background-color:#3ba4d7;color:#fff;border-bottom-right-radius:.125rem}.network-chat-view .chat-bubble-container.incoming{align-self:flex-start;align-items:flex-start}.network-chat-view .chat-bubble-container.incoming .chat-bubble{background-color:#fff;color:#1e293b;border:1px solid #e2e8f0;border-bottom-left-radius:.125rem}.network-chat-view .chat-bubble-container .chat-sender{font-size:.75rem;color:#64748b;margin-bottom:.25rem;padding:0 .25rem}.network-chat-view .chat-bubble-container .chat-bubble{padding:.625rem .875rem;border-radius:.75rem;font-size:.925rem;line-height:1.4;white-space:break-spaces;word-break:break-word;box-shadow:0 1px 2px rgba(0,0,0,.05)}.network-chat-view .chat-bubble-container .chat-time{font-size:.7rem;color:#94a3b8;margin-top:.25rem;padding:0 .25rem}.network-chat-view .chat-input-area{padding:1rem;background-color:#fff;border-top:1px solid #cbd5e1;display:flex;gap:.75rem;align-items:center}.network-chat-view .chat-input-area textarea.chat-textarea{flex:1;resize:none;height:40px;padding:.5rem .75rem;border:1px solid #cbd5e1;border-radius:.375rem;font-size:.9rem;outline:none;transition:all .2s}.network-chat-view .chat-input-area textarea.chat-textarea:focus{border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.network-chat-view .chat-input-area button.send-btn{padding:.5rem 1.25rem;font-size:.9rem;height:40px;display:flex;align-items:center;gap:.5rem}.network-chat-view .chat-warning{flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;color:#64748b;text-align:center;padding:2rem;gap:1rem}.network-chat-view .chat-warning i{font-size:3rem;color:#cbd5e1}.network-chat-view .chat-warning h4{font-weight:700;color:#334155}.network-chat-view .chat-warning p{max-width:350px;font-size:.9rem}.identity{color:#444;font-size:1.1em;margin:20px;padding:10px;border:1px solid #aaa;border-radius:20px}.identity>h4{margin:5px;font-size:1.3em}.identity button{font-size:.9em}.identity .details{display:grid;grid-template-columns:140px auto;grid-row-gap:5px;justify-content:left}.defaultAvatar{width:3rem;height:3rem;aspect-ratio:1;background:#b0c4de;border-radius:50%;display:grid;place-items:center}.defaultAvatar p{font-weight:900;color:#666f7f;transform:translateY(1px)}img.avatar{display:block;width:3rem;height:max-content;aspect-ratio:1;margin-right:.3em;border-radius:50%}.counter{margin-left:.5em}.counter:before{content:"("}.counter:after{content:")"}.chatInit{margin-left:.5em;color:green;cursor:pointer}.people-sidebar-header{display:flex;flex-direction:column;padding:.75rem 1rem .5rem 1rem;gap:.75rem;border-bottom:1px solid #e2e8f0;background-color:#fff}.people-sidebar-header .searchbar-wrapper{position:relative;display:flex;align-items:center}.people-sidebar-header .searchbar-wrapper i.fa-search{position:absolute;left:.85rem;color:#94a3b8;font-size:.9rem}.people-sidebar-header .searchbar-wrapper input.searchbar-input{width:100%;padding:.5rem .75rem .5rem 2.25rem;border:1px solid #e2e8f0;border-radius:.5rem;font-size:.9rem;background-color:#f8fafc;color:#1e293b;outline:none;transition:all .2s ease}.people-sidebar-header .searchbar-wrapper input.searchbar-input:focus{border-color:#3b82f6;background-color:#fff;box-shadow:0 0 0 3px rgba(59,130,246,.1)}.people-sidebar-header .segmented-control{display:flex;background-color:#f1f5f9;padding:3px;border-radius:.5rem;gap:4px}.people-sidebar-header .segmented-control button.segment-tab{flex:1;display:flex;align-items:center;justify-content:center;gap:.5rem;padding:.5rem .75rem;font-size:.9rem;font-weight:600;color:#64748b;background:rgba(0,0,0,0);border:none;border-radius:.375rem;cursor:pointer;box-shadow:none;transition:all .2s ease}.people-sidebar-header .segmented-control button.segment-tab:hover{color:#1e293b}.people-sidebar-header .segmented-control button.segment-tab.active{background-color:#fff;color:#0f172a;box-shadow:0 1px 3px rgba(0,0,0,.1),0 1px 2px rgba(0,0,0,.06)}.people-sidebar-header .segmented-control button.segment-tab.active .segment-badge{background-color:#019dff;color:#fff}.people-sidebar-header .segmented-control button.segment-tab .segment-badge{display:inline-flex;align-items:center;justify-content:center;background-color:#cbd5e1;color:#334155;font-size:.75rem;font-weight:700;min-width:1.25rem;height:1.25rem;padding:0 .35rem;border-radius:9999px;line-height:1;transition:all .2s ease}.people-sidebar-header .sub-filter-row{display:flex;align-items:center;justify-content:space-between;gap:.5rem;min-height:32px}.people-sidebar-header .sub-filter-row select.filter-select{padding:.35rem .6rem;border:1px solid #cbd5e1;border-radius:.375rem;font-size:.85rem;font-weight:600;color:#475569;background-color:#fff;cursor:pointer;outline:none}.people-sidebar-header .sub-filter-row .btn-add-id{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:.375rem;background-color:#3b82f6;color:#fff;border:none;cursor:pointer;font-size:.9rem;transition:background-color .2s}.people-sidebar-header .sub-filter-row .btn-add-id:hover{background-color:#2563eb}.friends-list-container .people-context-menu{position:absolute;left:2rem;width:210px;background-color:#fff;border:1px solid #e2e8f0;box-shadow:0 4px 10px rgba(0,0,0,.15);border-radius:.375rem;z-index:1010;padding:.25rem 0;display:flex;flex-direction:column}.friends-list-container .people-context-menu .menu-item{padding:.5rem 1rem;font-size:.85rem;color:#334155;cursor:pointer;display:flex;align-items:center;transition:background-color .2s}.friends-list-container .people-context-menu .menu-item:hover{background-color:#f1f5f9;color:#0f172a}.people-container{display:flex;height:calc(100vh - 55px);width:100%;overflow:hidden}.people-left-pane{width:320px;border-right:1px solid #cbd5e1;display:flex;flex-direction:column;background-color:#fff;overflow:hidden}.people-right-pane{flex:1;display:flex;flex-direction:column;overflow:hidden;background-color:#f8fafc}.people-left-pane .chat-item{display:flex;align-items:center;padding:.75rem 1rem;gap:.75rem;border-bottom:1px solid #f1f5f9;cursor:pointer;transition:background-color .15s ease;position:relative}.people-left-pane .chat-item:hover{background-color:#f8fafc}.people-left-pane .chat-item.selected{background-color:#eff6ff;border-left:3px solid #3b82f6}.people-left-pane .chat-item .chat-avatar-wrapper{position:relative;flex-shrink:0}.people-left-pane .chat-item .chat-avatar-wrapper .status-dot{position:absolute;bottom:0;right:0;width:10px;height:10px;border-radius:50%;border:2px solid #fff}.people-left-pane .chat-item .chat-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:.15rem}.people-left-pane .chat-item .chat-info .chat-name{font-size:.9rem;font-weight:700;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.people-left-pane .chat-item .chat-info .chat-last-msg{font-size:.8rem;color:#64748b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.people-left-pane .chat-item .chat-meta{display:flex;flex-direction:column;align-items:flex-end;gap:.25rem;flex-shrink:0}.people-left-pane .chat-item .chat-meta .chat-time{font-size:.75rem;color:#94a3b8;white-space:nowrap}.lobby{margin:10px;border:1px solid #aaa;border-radius:20px}.lobby .mainname{margin:20px;font-weight:100;font-size:1.2em}.topic{color:#666}.lobby>.topic{font-size:.95em;margin-left:25px;margin-bottom:5px}.lefttitle{margin-top:15px;margin-bottom:0;font-weight:100;font-size:1.2em}.leftname{margin-top:5px;margin-bottom:5px;padding:5px;font-weight:100;font-size:1em}.leftlobby>.topic{font-size:.75em;margin-left:15px;margin-bottom:5px}.subscribed,.public{cursor:pointer}.leftlobby{border:1px solid #aaa;border-radius:10px;margin-top:5px;background-color:#fff}.leftlobby.selected-lobby,.selectedidentity{color:#fff;background-color:#3ba4d7}.rightbar{position:absolute;width:185px;background-color:#fff;overflow:auto;top:130px;bottom:15px;right:15px}.user{padding:5px}.lobbyName{padding:15px;margin-top:2rem}.lobbies{position:absolute;width:185px;left:165px;bottom:15px;top:130px;overflow:auto}.messages,.setup{position:absolute;background-color:#fff;top:130px;left:360px;right:215px;overflow:auto}.messages{bottom:115px}.messagetext{white-space:break-spaces;margin-right:5px}.message>*{margin-left:5px}.username{color:#006400;font-weight:bolder}.chatMessage{position:absolute;background-color:#fff;height:85px;bottom:15px;right:215px;left:360px}textarea.chatMsg{height:100%;width:100%}.chatatchar{margin-left:.2em;margin-right:.2em;color:silver}.setupicon{margin-left:1em;cursor:pointer}.leaveicon{margin-left:1em;cursor:pointer;color:#d40000}.selectidentity{margin:15px;font-size:1.2em}.setup>.identity{cursor:pointer}.setup{bottom:15px}.createDistantChat{margin-top:1em}.no-lobbies .messages,.no-lobbies .chatMessage,.no-lobbies .setup{left:165px}@media(min-width: 900px){.node-panel.chat-room{display:grid !important;grid-template-columns:250px 1fr 200px !important;grid-template-rows:auto 1fr auto !important;grid-template-areas:"lobbies header rightbar" "lobbies messages rightbar" "lobbies input rightbar" !important;padding:0 !important;height:100% !important}.node-panel.chat-room .lobbyName{grid-area:header;padding:10px;border-bottom:1px solid #eee;margin:0;z-index:10;background:#fff}.node-panel.chat-room .lobbies{grid-area:lobbies;position:static !important;width:auto !important;height:auto !important;border-right:1px solid #ccc;overflow-y:auto;display:block !important;top:auto !important;bottom:auto !important;left:auto !important}.node-panel.chat-room .messages{grid-area:messages;position:static !important;width:auto !important;height:auto !important;overflow-y:auto;padding:10px;left:auto !important;right:auto !important;top:auto !important;bottom:auto !important;margin:0 !important}.node-panel.chat-room .rightbar{grid-area:rightbar;position:static !important;width:auto !important;border-left:1px solid #ccc;overflow-y:auto;display:block !important}.node-panel.chat-room .chatMessage{grid-area:input;position:static !important;width:auto !important;height:auto !important;border-top:1px solid #eee;left:auto !important;right:auto !important;bottom:auto !important;flex:0 0 auto;padding:10px !important;background:#fff;z-index:10}}@media(max-width: 899px){.node-panel.chat-room{display:flex !important;flex-direction:column !important;height:100% !important;position:relative !important}.node-panel.chat-room .lobbyName{flex:0 0 auto}.node-panel.chat-room .messages{flex:1 !important;overflow-y:auto !important;position:relative !important;top:0 !important;bottom:0 !important;left:0 !important;right:0 !important;width:100% !important;height:auto !important;margin:0 !important}.node-panel.chat-room .chatMessage{flex:0 0 auto !important;position:relative !important;bottom:0 !important;left:0 !important;right:0 !important;width:100% !important;height:auto !important;z-index:100}.node-panel.chat-room .rightbar,.node-panel.chat-room .lobbies{display:none !important;position:fixed !important;top:60px !important;bottom:0 !important;width:80% !important;background:#fff !important;z-index:200 !important;box-shadow:2px 0 10px rgba(0,0,0,.2) !important}.node-panel.chat-room.show-lobbies .lobbies{display:block !important;left:0 !important}.node-panel.chat-room.show-users .rightbar{display:block !important;right:0 !important}.chat-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.4);z-index:150}.show-lobbies .chat-overlay,.show-users .chat-overlay{display:block}.mobile-menu-icons{display:flex;gap:15px;font-size:1.2rem}.mobile-menu-icons i{cursor:pointer;padding:5px}}@media(min-width: 900px){.mobile-menu-icons{display:none}}.chat-hub-container{display:flex;height:100%;width:100%;overflow:hidden;background-color:#f1f5f9}.chat-hub-left-pane{width:320px;min-width:300px;max-width:350px;border-right:1px solid #cbd5e1;display:flex;flex-direction:column;background:#fff;box-shadow:2px 0 5px rgba(0,0,0,.05)}.chat-own-profile-card{padding:1.25rem;border-bottom:1px solid #e2e8f0;background:linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);position:relative}.chat-own-profile-card .profile-header{display:flex;align-items:center;gap:1rem}.chat-own-profile-card .profile-info{display:flex;flex-direction:column;flex:1;overflow:hidden}.chat-own-profile-card .profile-info .profile-name{font-weight:700;color:#1e293b;font-size:1.1rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-own-profile-card .profile-info .profile-status{font-size:.85rem;color:#10b981;font-weight:500;display:flex;align-items:center;gap:.35rem}.chat-own-profile-card .profile-info .profile-status::before{content:"";display:inline-block;width:8px;height:8px;background-color:#10b981;border-radius:50%}.chat-rooms-list-container{flex:1;display:flex;flex-direction:column;overflow:hidden}.chat-rooms-list-container .searchbar-container{padding:.75rem 1rem;border-bottom:1px solid #e2e8f0}.chat-rooms-list-container .searchbar-container input.searchbar{width:100%;padding:.5rem .75rem;font-size:.9rem;border:1px solid #cbd5e1;border-radius:.375rem;background-color:#f8fafc;outline:none;transition:all .2s}.chat-rooms-list-container .searchbar-container input.searchbar:focus{background-color:#fff;border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.chat-rooms-list-container .rooms-scroll{flex:1;overflow-y:auto;padding:.5rem 0}.rooms-section-title{display:flex;align-items:center;gap:.5rem;padding:.75rem 1rem .375rem;font-size:.75rem;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.05em}.rooms-section-title i{font-size:.7rem;color:#94a3b8}.chat-room-list-item{display:flex;align-items:center;gap:.75rem;padding:.75rem 1rem;margin:.125rem .5rem;border-radius:.5rem;cursor:pointer;transition:all .2s}.chat-room-list-item:hover{background-color:#f1f5f9}.chat-room-list-item.selected{background-color:#e0f2fe}.chat-room-list-item.selected .room-name{color:#0369a1;font-weight:600}.chat-room-list-item .room-icon{flex-shrink:0;width:36px;height:36px;border-radius:.5rem;background:linear-gradient(135deg, #3ba4d7, #0ea5e9);display:flex;align-items:center;justify-content:center;color:#fff;font-size:1.35rem}.chat-room-list-item.public-room .room-icon{background:linear-gradient(135deg, #10b981, #059669)}.chat-room-list-item .room-meta{flex:1;min-width:0}.chat-room-list-item .room-meta .room-name{font-size:.95rem;color:#334155;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .2s}.chat-room-list-item .room-meta .room-topic{font-size:.8rem;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-room-list-item .room-badge{flex-shrink:0;min-width:24px;height:24px;border-radius:12px;background-color:#e2e8f0;color:#475569;font-size:.75rem;font-weight:700;display:flex;align-items:center;justify-content:center;padding:0 .375rem}.chat-hub-right-pane{flex:1;display:flex;flex-direction:column;overflow:hidden;background-color:#f8fafc}.chat-pane-placeholder{flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;color:#94a3b8;gap:1rem;padding:2rem;text-align:center}.chat-pane-placeholder i{font-size:4rem;color:#cbd5e1}.chat-pane-placeholder p{font-size:1.1rem;max-width:400px}.chat-hub-tab-content{flex:1;overflow-y:auto;padding:1.5rem}.chat-room-detail-view{display:flex;flex-direction:column;gap:1.5rem}.chat-room-detail-view .detail-header{display:flex;align-items:flex-start;gap:1.5rem;padding-bottom:1.5rem;border-bottom:1px solid #e2e8f0;flex-wrap:wrap}.chat-room-detail-view .detail-header .detail-title{flex:1;min-width:200px}.chat-room-detail-view .detail-header .detail-title h2{font-size:1.75rem;font-weight:800;color:#1e293b;margin-bottom:.25rem}.chat-room-detail-view .detail-header .detail-title .detail-subtitle{font-size:.9rem;color:#64748b;display:flex;align-items:center;gap:.5rem}.chat-room-detail-view .detail-header .detail-actions{display:flex;gap:.75rem;flex-wrap:wrap}.chat-room-detail-view .detail-header .detail-actions button{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.9rem}.chat-room-detail-view .detail-section{background-color:#fff;border-radius:.5rem;border:1px solid #e2e8f0;padding:1.25rem;box-shadow:0 1px 3px rgba(0,0,0,.02)}.chat-room-detail-view .detail-section h3{font-size:1.1rem;font-weight:700;color:#334155;margin-bottom:1rem;padding-bottom:.5rem;border-bottom:1px solid #f1f5f9}.chat-room-detail-view .detail-section .info-grid{display:grid;grid-template-columns:130px 1fr;row-gap:.75rem;font-size:.9rem}.chat-room-detail-view .detail-section .info-grid .info-label{font-weight:600;color:#64748b}.chat-room-detail-view .detail-section .info-grid .info-value{color:#1e293b;word-break:break-all}.participants-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(180px, 1fr));gap:.5rem}.participant-card{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;background-color:#f8fafc;border:1px solid #e2e8f0;border-radius:.375rem}.participant-card .participant-name{font-size:.875rem;color:#334155;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.no-participants{color:#94a3b8;font-size:.9rem;font-style:italic}.detail-actions-footer{display:flex;gap:.75rem}.detail-actions-footer button{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.9rem}.join-description{color:#64748b;font-size:.9rem;margin-bottom:1rem}.identities-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(200px, 1fr));gap:.75rem}.identity-card{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1rem;background-color:#f8fafc;border:1px solid #e2e8f0;border-radius:.5rem;cursor:pointer;transition:all .2s}.identity-card:hover{background-color:#e0f2fe;border-color:#3ba4d7}.identity-card .identity-name{font-size:.95rem;font-weight:600;color:#334155}.identity-card i{color:#3ba4d7;font-size:.9rem}.no-rooms{padding:1rem;color:#94a3b8;text-align:center;font-style:italic}@media(max-width: 899px){.chat-hub-container{flex-direction:column}.chat-hub-left-pane{width:100%;min-width:0;max-width:none;max-height:45%;border-right:none;border-bottom:1px solid #cbd5e1}.chat-hub-right-pane{flex:1;min-height:0}}.chat-hub-header-bar{padding:.75rem 1.5rem;background-color:#fff;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;justify-content:space-between;height:65px;flex-shrink:0}.chat-hub-header-bar .chat-header-info{display:flex;flex-direction:column;overflow:hidden}.chat-hub-header-bar .chat-header-info .chat-header-name{font-size:1.15rem;font-weight:800;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-hub-header-bar .chat-header-info .chat-header-topic{font-size:.85rem;color:#64748b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:.125rem}.chat-hub-header-bar .chat-header-actions{display:flex;gap:.5rem}.chat-hub-header-bar .chat-header-actions button{display:flex;align-items:center;gap:.35rem;padding:.375rem .75rem;font-size:.85rem}.chat-hub-tabs-container{background-color:#fff;border-bottom:1px solid #cbd5e1;padding:.5rem 1.5rem 0}.chat-hub-tabs{display:flex;gap:.5rem}.chat-hub-tabs .tab-btn{padding:.625rem 1.25rem;font-size:.95rem;font-weight:600;color:#64748b;background:rgba(0,0,0,0);border:none;border-radius:.375rem .375rem 0 0;border-bottom:3px solid rgba(0,0,0,0);cursor:pointer;box-shadow:none;transition:all .2s;display:flex;align-items:center;gap:.5rem}.chat-hub-tabs .tab-btn:hover{color:#334155;background-color:#f1f5f9}.chat-hub-tabs .tab-btn.active{color:#3ba4d7;border-bottom-color:#3ba4d7;background-color:rgba(0,0,0,0)}.chat-hub-tab-content{flex:1;display:flex;flex-direction:column;overflow:hidden;background-color:#f8fafc}.chat-hub-conversation-layout{display:flex;flex-direction:row;height:100%;width:100%;overflow:hidden}.chat-hub-conversation-main{display:flex;flex-direction:column;flex:1;height:100%;overflow:hidden}.chat-hub-rightbar{width:200px;border-left:1px solid #cbd5e1;background-color:#fff;display:flex;flex-direction:column;flex-shrink:0;position:relative}.chat-hub-rightbar .rightbar-title{padding:.75rem 1rem;font-size:.85rem;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid #e2e8f0}.chat-hub-rightbar .rightbar-users-list{flex:1;overflow-y:auto;padding:.5rem}.chat-hub-rightbar .user{padding:.5rem .75rem;font-size:.9rem;color:#334155;border-radius:.375rem;transition:all .2s;display:flex;align-items:center;gap:.5rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;position:relative}.chat-hub-rightbar .user:hover{background-color:#f1f5f9;color:#0f172a}.chat-hub-rightbar .user .defaultAvatar{width:2rem;height:2rem;font-size:.9rem;flex-shrink:0}.chat-hub-rightbar .user img.avatar{width:2rem;height:2rem;flex-shrink:0}@media(max-width: 899px){.chat-hub-rightbar{display:none}}.chat-hub-messages{flex:1;overflow-y:auto;padding:1.25rem 1.5rem;display:flex;flex-direction:column;gap:1rem}.chat-hub-messages .message{display:flex;flex-direction:column;max-width:70%;padding:.625rem .875rem;border-radius:.75rem;font-size:.925rem;line-height:1.4;word-break:break-word;box-shadow:0 1px 2px rgba(0,0,0,.05)}.chat-hub-messages .message.incoming{align-self:flex-start;align-items:flex-start;background-color:#fff;color:#1e293b;border:1px solid #e2e8f0;border-bottom-left-radius:.125rem}.chat-hub-messages .message.outgoing{align-self:flex-end;align-items:flex-end;background-color:#3ba4d7;color:#fff;border-bottom-right-radius:.125rem}.chat-hub-messages .message .username{font-size:.75rem;margin-bottom:.25rem;padding:0 .125rem;font-weight:700}.chat-hub-messages .message.incoming .username{color:#0369a1}.chat-hub-messages .message.outgoing .username{color:#e0f2fe}.chat-hub-messages .message .messagetext{white-space:break-spaces;margin:0}.chat-hub-messages .message .datetime{font-size:.7rem;margin-top:.25rem;padding:0 .125rem;opacity:.8}.chat-hub-messages .message.incoming .datetime{color:#64748b}.chat-hub-messages .message.outgoing .datetime{color:#f1f5f9}.chat-hub-input-area{padding:.75rem 1.5rem;background-color:#fff;border-top:1px solid #cbd5e1;display:flex;gap:.75rem;align-items:flex-end;flex-shrink:0}.chat-hub-input-area textarea.chat-hub-textarea{flex:1;resize:vertical;min-height:40px;max-height:250px;height:40px;padding:.5rem .75rem;border:1px solid #cbd5e1;border-radius:.375rem;font-size:.9rem;outline:none;transition:border-color .2s,box-shadow .2s;background-color:#f8fafc}.chat-hub-input-area textarea.chat-hub-textarea:focus{background-color:#fff;border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.chat-hub-input-area button.chat-hub-send-btn{padding:.5rem 1.25rem;font-size:.9rem;height:40px;display:flex;align-items:center;gap:.5rem;border-radius:.375rem}.chat-hub-messages.compact-container,.messages.compact-container{gap:0 !important;padding:.75rem 1rem !important;background-color:#fff !important;display:flex !important;flex-direction:column !important}.chat-hub-messages.compact-container .message.compact,.messages.compact-container .message.compact{display:block !important;max-width:100% !important;padding:.1rem 0 !important;border-radius:0 !important;background-color:rgba(0,0,0,0) !important;border:none !important;box-shadow:none !important;align-self:flex-start !important;font-size:.875rem !important;line-height:1.45 !important;margin:0 !important;white-space:nowrap !important}.chat-hub-messages.compact-container .message.compact:hover,.messages.compact-container .message.compact:hover{background-color:#f8fafc !important;overflow:visible !important;white-space:normal !important}.chat-hub-messages.compact-container .message.compact .datetime,.messages.compact-container .message.compact .datetime{color:#a0a0a0 !important;margin-right:.4rem !important;font-size:.78rem !important;font-family:monospace !important;opacity:1 !important;display:inline !important}.chat-hub-messages.compact-container .message.compact .username,.messages.compact-container .message.compact .username{font-weight:bold !important;margin-right:.2rem !important;font-size:.875rem !important;display:inline !important}.chat-hub-messages.compact-container .message.compact .messagetext,.messages.compact-container .message.compact .messagetext{color:#1e293b !important;white-space:normal !important;word-break:break-word !important;display:inline !important;margin:0 !important}.chat-create-lobby-btn{position:absolute;bottom:.5rem;right:1.25rem;background-color:#0084ff;color:#fff;border:none;border-radius:.375rem;padding:.35rem .75rem;font-size:.85rem;font-weight:600;cursor:pointer;box-shadow:0 4px 6px -1px rgba(0,132,255,.2),0 2px 4px -1px rgba(0,132,255,.1);transition:background-color .2s,transform .2s;display:flex;align-items:center;gap:.25rem}.chat-create-lobby-btn:hover{background-color:#0073e6;transform:translateY(-1px)}.chat-create-lobby-btn:active{transform:translateY(0)}.chat-hub-rightbar .user .user-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}.user-tooltip{position:absolute;width:260px;background-color:#ffffe1;border:1px solid #7f7f7f;box-shadow:2px 2px 6px rgba(0,0,0,.25);padding:.5rem;border-radius:.25rem;z-index:10000;white-space:normal;display:flex;gap:.5rem;align-items:flex-start}.chat-hub-rightbar .user-tooltip{left:-275px;transform:translateY(-50%);z-index:1000}.user-tooltip .tooltip-avatar{flex-shrink:0}.user-tooltip .tooltip-details{display:flex;flex-direction:column;gap:.25rem;font-size:.8rem;color:#000;text-align:left}.user-tooltip .tooltip-row{line-height:1.2}.user-tooltip .tooltip-label{font-weight:bold}.user-tooltip .tooltip-value{font-weight:normal;word-break:break-all}.user-tooltip .tooltip-value.tooltip-id{font-family:monospace}.chat-hub-rightbar .rightbar-context-menu{position:absolute;right:1rem;width:210px;background-color:#fff;border:1px solid #e2e8f0;box-shadow:0 4px 10px rgba(0,0,0,.15);border-radius:.375rem;z-index:1010;padding:.25rem 0;display:flex;flex-direction:column}.chat-hub-rightbar .rightbar-context-menu .menu-item{padding:.5rem 1rem;font-size:.85rem;color:#334155;cursor:pointer;display:flex;align-items:center;transition:background-color .2s}.chat-hub-rightbar .rightbar-context-menu .menu-item:hover{background-color:#f1f5f9;color:#0f172a}.chat-emoji{font-size:1.45em;line-height:1;vertical-align:-0.15em;display:inline-block}.chat-hub-attach-btn,.chat-hub-action-btn{background-color:rgba(0,0,0,0) !important;border:none !important;font-size:1.15rem !important;color:#64748b !important;cursor:pointer !important;padding:.4rem .5rem !important;border-radius:.375rem !important;flex-shrink:0 !important;display:inline-flex !important;align-items:center !important;justify-content:center !important;transition:all .2s !important;box-shadow:none !important;margin:0 !important;line-height:1 !important;height:36px !important;width:36px !important}.chat-hub-attach-btn:hover,.chat-hub-action-btn:hover{background-color:#f1f5f9 !important;color:#3b82f6 !important;transform:none !important}.attach-modal-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:rgba(15,23,42,.4);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:2000}.attach-modal{background-color:#fff;border-radius:.5rem;width:450px;max-width:90%;padding:1.5rem;box-shadow:0 10px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);display:flex;flex-direction:column;gap:1rem}.attach-modal .attach-modal-header{display:flex;align-items:center;gap:.6rem;margin-bottom:.25rem}.attach-modal .attach-modal-icon{font-size:1.2rem;color:#3b82f6}.attach-modal h4{margin:0;font-size:1.2rem;color:#0f172a}.attach-modal p{margin:0;font-size:.9rem;color:#475569}.attach-modal .attach-path-row{display:flex;gap:.5rem;align-items:center}.attach-modal .attach-path-row input[type=text]{flex:1;padding:.75rem;border:1px solid #cbd5e1;border-radius:.375rem;font-size:.9rem;outline:none;transition:border-color .2s;min-width:0}.attach-modal .attach-path-row input[type=text]:focus{border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.15)}.attach-browse-btn{flex-shrink:0;display:flex;align-items:center;gap:.35rem;padding:.625rem .9rem;font-size:.875rem;background-color:#f1f5f9;color:#334155;border:1px solid #cbd5e1;border-radius:.375rem;cursor:pointer;box-shadow:none;transition:background-color .2s,border-color .2s;white-space:nowrap}.attach-browse-btn:hover{background-color:#e2e8f0;border-color:#94a3b8}.attach-path-hint{display:flex;align-items:flex-start;gap:.5rem;padding:.6rem .75rem;background-color:#fffbeb;border:1px solid #fcd34d;border-left:3px solid #f59e0b;border-radius:.375rem;font-size:.825rem;color:#92400e;line-height:1.45}.attach-path-hint i{color:#f59e0b;margin-top:.1rem;flex-shrink:0}.attach-path-hint code{font-family:monospace;background-color:rgba(245,158,11,.15);padding:.05rem .25rem;border-radius:.2rem}.attach-modal .hashing-spinner{display:flex;align-items:center;gap:.5rem;font-size:.9rem;color:#3b82f6}.attach-modal .error-text{color:#ef4444;font-size:.85rem;margin:0}.attach-modal .modal-buttons{display:flex;justify-content:flex-end;gap:.75rem;margin-top:.5rem}.attach-modal .modal-buttons button{padding:.5rem 1rem;font-size:.9rem;border-radius:.25rem;border:none;cursor:pointer;transition:opacity .2s}.attach-modal .modal-buttons button:hover{opacity:.9}.chat-hub-emoji-btn{background-color:rgba(0,0,0,0);border:none;font-size:1.3rem;cursor:pointer;padding:.35rem .4rem;margin-right:.25rem;flex-shrink:0;display:flex;align-items:center;justify-content:center;border-radius:.375rem;line-height:1;transition:background-color .15s,transform .15s;box-shadow:none}.chat-hub-emoji-btn:hover{background-color:#f1f5f9;transform:scale(1.1)}.emoji-picker-wrapper{position:relative;flex-shrink:0;display:flex;align-items:center}.emoji-picker{position:absolute;bottom:calc(100% + .5rem);left:0;width:320px;background-color:#fff;border:1px solid #e2e8f0;border-radius:.625rem;box-shadow:0 8px 30px -4px rgba(0,0,0,.18),0 4px 12px -2px rgba(0,0,0,.1);z-index:3000;display:flex;flex-direction:column;overflow:hidden;animation:emoji-pop .15s ease-out}.emoji-search-row{display:flex;align-items:center;gap:.4rem;padding:.6rem .75rem .4rem;border-bottom:1px solid #f1f5f9}.emoji-search-icon{color:#94a3b8;font-size:.8rem;flex-shrink:0}.emoji-search-input{flex:1;border:1px solid #e2e8f0;border-radius:.375rem;padding:.3rem .5rem;font-size:.85rem;outline:none;background-color:#f8fafc;transition:border-color .15s}.emoji-search-input:focus{border-color:#3ba4d7;background-color:#fff}.emoji-search-clear{background:none;border:none;cursor:pointer;color:#94a3b8;padding:.2rem;font-size:.8rem;box-shadow:none;display:flex;align-items:center}.emoji-search-clear:hover{color:#475569}.emoji-categories{display:flex;gap:.1rem;padding:.35rem .5rem;border-bottom:1px solid #f1f5f9;overflow-x:auto;scrollbar-width:none}.emoji-categories::-webkit-scrollbar{display:none}.emoji-cat-btn{background:none;border:none;cursor:pointer;font-size:1.2rem;padding:.3rem .35rem;border-radius:.375rem;line-height:1;box-shadow:none;transition:background-color .1s;flex-shrink:0}.emoji-cat-btn:hover{background-color:#f1f5f9}.emoji-cat-btn.active{background-color:#e0f2fe;box-shadow:inset 0 -2px 0 #3ba4d7}.emoji-grid{display:grid;grid-template-columns:repeat(7, 1fr);gap:0;padding:.4rem .35rem;max-height:220px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#cbd5e1 rgba(0,0,0,0)}.emoji-grid::-webkit-scrollbar{width:4px}.emoji-grid::-webkit-scrollbar-track{background:rgba(0,0,0,0)}.emoji-grid::-webkit-scrollbar-thumb{background-color:#cbd5e1;border-radius:4px}.emoji-btn{background:none;border:none;cursor:pointer;font-size:1.7rem;padding:.25rem;border-radius:.3rem;line-height:1;box-shadow:none;text-align:center;transition:background-color .1s,transform .1s;display:flex;align-items:center;justify-content:center;aspect-ratio:1}.emoji-btn:hover{background-color:#f1f5f9;transform:scale(1.2)}@keyframes emoji-pop{from{opacity:0;transform:scale(0.92) translateY(6px)}to{opacity:1;transform:scale(1) translateY(0)}}.side-bar{display:flex;flex-direction:column;background:#fff}.side-bar .mail-compose-btn{width:96%;margin:.25rem;padding:.75rem 0}.compose-mail__from{display:flex;justify-content:flex-start;align-items:center;gap:.5rem;padding-bottom:.5rem;border-bottom:2px solid #eef3f6}.compose-mail__recipients{padding:.5rem 0;display:flex;flex-direction:column;gap:.5rem;border-bottom:2px solid #eef3f6}.compose-mail__recipients__container{display:flex;gap:.5rem}.compose-mail__recipients__container>label{text-transform:capitalize}.compose-mail__recipients__container .recipients{width:100%;display:flex;gap:.5rem;flex-wrap:wrap}.compose-mail__recipients__container .recipients__selected{padding:.125rem .5rem;display:flex;align-items:center;gap:.5rem;border:1px solid #eef3f6;border-radius:3px;cursor:default}.compose-mail__recipients__container .recipients__selected i{cursor:pointer;padding:.25rem}.compose-mail__recipients__container .recipients__input{display:flex;position:relative;flex-grow:1}.compose-mail__recipients__container .recipients__input-field{flex-grow:1;min-width:200px;padding:0;border:none;box-shadow:none}.compose-mail__recipients__container .recipients__input-field:focus+.recipients__input-list{display:flex}.compose-mail__recipients__container .recipients__input-list{z-index:1;position:absolute;top:1rem;padding:0;width:100%;max-height:15rem;flex-direction:column;overflow:auto;display:none;background:#fff;border-top:1px solid #eef3f6;border-bottom:1px solid #eef3f6}.compose-mail__recipients__container .recipients__input-list:hover{display:flex}.compose-mail__recipients__container .recipients__input-list li{list-style:none;padding:.25rem .5rem;cursor:pointer;background:#fff;border:1px solid #eef3f6;border-top:0px}.compose-mail__recipients__container .recipients__input-list li:hover{background:#eef3f6}.compose-mail__recipients__container .recipients__input-list li:last-child{border-bottom:0px}.compose-mail__recipients .remove-recipient{padding:.125rem .5rem}.compose-mail input[type=text].compose-mail__subject{padding:.5rem 0;border:none;box-shadow:none;border-bottom:2px solid #eef3f6;border-radius:0}.compose-mail__message{margin:.5rem 0;height:100%;display:flex;flex-direction:column;overflow:auto}.compose-mail__message-body{height:100%;outline:rgba(0,0,0,0)}.compose-mail__send-btn{display:flex;align-items:center;gap:.5rem}.compose-mail__send-btn i{transform:translateY(-1px)}.msg-view{height:100%;display:flex;flex-direction:column;gap:1rem;overflow:auto}.msg-view-nav{display:flex;justify-content:space-between;align-items:column}.msg-view-nav__action{display:flex;gap:.5rem}.msg-view__header{display:flex;flex-direction:column;gap:1rem}.msg-view__header>h3{line-height:1}.msg-view__header .msg-details{display:flex;gap:1rem}.msg-view__header .msg-details__avatar{height:max-content}.msg-view__header .msg-details__info{display:flex;flex-direction:column}.msg-view__header .msg-details__info-item{display:flex;gap:.5rem}.msg-view__body{height:100%;overflow:auto;font-size:14px !important}.msg-view__attachment{height:50%;overflow:auto;display:flex;flex-direction:column}.msg-view__attachment-items{height:100%;overflow:auto}.mail-tag{width:8rem;padding:.5rem}.msgHeader{display:flex}.msgHeaderDetails{display:flex;flex-direction:column}table.mails th:nth-child(1){width:5%;color:#fcba03}table.mails th:nth-child(2){width:5%;color:hsl(202.5,30.7692307692%,44.9019607843%)}table.mails th:nth-child(3){width:50%;text-align:start}table.mails th:nth-child(4),table.mails th:nth-child(5){width:20%;text-align:start}table.mails td:nth-child(3){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.mails td:nth-child(4),table.mails td:nth-child(5){text-align:start}table.mails tr:hover{background-color:#eef3f6;cursor:pointer}table.mails tr.unread{color:#000;background-color:#eef3f6}table.mails>tr:hover{cursor:auto;background-color:#fff}table.mails th.sortable-th{cursor:pointer;user-select:none;transition:background-color .2s,color .2s}table.mails th.sortable-th:hover{background-color:#eef3f6;color:hsl(202.5,30.7692307692%,14.9019607843%)}input.star-check{display:none}input.star-check+label.star-check{color:gray}input.star-check:checked+label.star-check{color:#fcba03}#truncate{height:6rem;overflow:auto}#truncate.truncated-view{height:1.75rem;overflow:hidden}.toggle-truncate{font-size:.75rem;padding:0 .25rem;background:#999;color:#14141b;box-shadow:none;border-radius:2px}table.attachment-container{padding:0}table.attachment-container>tr{border:0}table.attachment-container .attachment-header{width:100%;display:flex;justify-content:space-between}table.attachment-container .attachment-header th{text-align:start}table.attachment-container .attachment-header th:nth-child(1){flex-basis:45%}table.attachment-container .attachment-header th:nth-child(2){flex-basis:15%}table.attachment-container .attachment-header th:nth-child(3){flex-basis:10%}table.attachment-container .attachment-header th:nth-child(4){flex-basis:20%}table.attachment-container .attachment-header th:nth-child(5){text-align:center;flex-basis:10%}table.attachment-container .attachment{width:100%;display:flex;justify-content:space-between;text-align:start}table.attachment-container .attachment__name{flex-basis:45%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}table.attachment-container .attachment__name span{margin-left:8px}table.attachment-container .attachment__from{flex-basis:15%}table.attachment-container .attachment__size{flex-basis:10%}table.attachment-container .attachment__date{flex-basis:20%}table.attachment-container .attachment td:nth-child(5){display:flex;justify-content:center;align-items:center;flex-basis:10%}table.attachment-container .attachment td:nth-child(5) button{font-size:.875rem}.view-toggle{height:max-content;border:1px solid #019dff;border-radius:4px;display:flex}.view-toggle *{padding:4px 12px;border-radius:4px}.composePopupOverlay{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1;background-color:rgba(0,0,0,.2)}.composePopupOverlay .composePopup{position:absolute;inset:0;margin:auto;width:80%;height:90%}.composePopupOverlay .composePopup>.widget{padding:2rem}.composePopupOverlay .composePopup .close-btn{position:absolute;top:1.5rem;right:1.5rem}.file-view{width:100%;padding:1rem;margin-top:1.5rem;border-radius:8px;border:1px solid #ccc;animation:fadein .5s}.file-view__heading{display:flex;justify-content:space-between;margin-bottom:.5rem}.file-view__heading-chunk{display:flex;gap:1rem}.file-view__body{display:flex;flex-direction:column;gap:1rem}.file-view__body-details{display:flex;align-items:center}.file-view__body-details-stat{width:100%;display:grid;grid-template-columns:repeat(5, 1fr)}.file-view__body-details-stat span>i{margin-right:.5rem}.file-view__body-details-action{display:flex;gap:1rem;height:100%}.file-view__body-details-action button,.file-view__body-details-action button.red{padding:.25rem .75rem}table.myfiles td{word-wrap:break-word}table.myfiles th:nth-child(1){width:2%}table.myfiles th:nth-child(2){width:50%}table.myfiles td:nth-child(2){text-align:start}table.friendsfiles td{word-wrap:break-word}table.friendsfiles th:nth-child(1){width:2%}table.friendsfiles th:nth-child(2){width:50%}table.friendsfiles th:nth-child(4){width:40%}table.friendsfiles td:nth-child(2){text-align:start}.file-search-container{margin-top:1rem;padding:8px;display:flex;gap:8px;border:1px solid rgba(20,20,27,.2);border-radius:6px;height:100%;overflow:auto}.file-search-container__keywords{flex-basis:15%;padding-right:.25rem;border-right:1px solid rgba(20,20,27,.1)}.file-search-container__keywords .keywords-container{display:flex;flex-direction:column;border-top:2.5px solid rgba(20,20,27,.08);margin-top:.125rem;padding-top:.25rem}.file-search-container__keywords .keywords-container a{font-size:1.2rem;text-decoration:none;color:#14141b}.file-search-container__keywords .keywords-container a.selected{color:#019dff}.file-search-container__results{flex-basis:85%;height:100%;overflow:auto}.file-search-container__results .results-container .results-header tr{display:flex}.file-search-container__results .results-container .results-header tr th{font-size:1.25rem;font-weight:bold;text-align:left}.file-search-container__results .results-container .results-header tr th:nth-child(1){flex-basis:40%}.file-search-container__results .results-container .results-header tr th:nth-child(2){flex-basis:10%;text-align:center}.file-search-container__results .results-container .results-header tr th:nth-child(3){flex-basis:40%}.file-search-container__results .results-container .results-header tr th:nth-child(4){flex-basis:10%}.file-search-container__results .results-container .results{height:100%;overflow:auto}.file-search-container__results .results-container .results tr{display:flex}.file-search-container__results .results-container .results tr .results__hash,.file-search-container__results .results-container .results tr .results__name{text-align:left;flex-basis:40%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.file-search-container__results .results-container .results tr .results__hash span,.file-search-container__results .results-container .results tr .results__name span{margin-left:8px}.file-search-container__results .results-container .results tr .results__size{flex-basis:10%}.file-search-container__results .results-container .results tr .results__download{flex-basis:10%;display:flex;justify-content:start;align-items:center}.search-form{display:flex;width:40%}.search-form input{width:100%}.search-form button{margin-left:.5rem}.shareManagerPopupOverlay{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1;background-color:rgba(0,0,0,.2)}.shareManagerPopupOverlay .shareManagerPopup{position:absolute;inset:0;margin:auto;width:80%;height:90%}.shareManagerPopupOverlay .shareManagerPopup>.widget{padding:1.5rem}.shareManagerPopupOverlay .shareManagerPopup .close-btn{position:absolute;top:1.5rem;right:1.5rem}.share-manager{display:flex;flex-direction:column;justify-content:space-between}.share-manager__table{margin:1rem 0 auto}.share-manager__table thead{font-weight:bold;text-align:left}.share-manager__table thead td:nth-child(1),.share-manager__table thead td:nth-child(2){padding-left:.5rem}.share-manager__table thead td:nth-child(3) .tooltip,.share-manager__table thead td:nth-child(4) .tooltip{font-weight:normal;font-size:1rem}.share-manager__table tbody{text-align:left}.share-manager__table tbody td:nth-child(4){font-size:1rem}.share-manager__table td input{border:0 !important}.share-manager__table td input[type=text]{width:100%}.share-manager__table td:nth-child(1){width:45%}.share-manager__table td:nth-child(2){width:20%}.share-manager__table td:nth-child(3){width:10%}.share-manager__table td:nth-child(4){width:25%}.share-manager__actions{display:flex;justify-content:space-between}.share-manager__form{display:flex;flex-direction:column;gap:.5rem}.share-manager__form_input{display:flex;flex-direction:column;gap:.5rem}.share-manager__form_input input{flex-grow:1}.share-manager .share-flags input.share-flags-check{display:none}.share-manager .share-flags input.share-flags-check+label.share-flags-label{color:gray;margin-right:.25rem;padding:.25rem .25rem .125rem;border:1px solid #6d6d6d;border-radius:.5rem}.share-manager .share-flags input.share-flags-check:checked+label.share-flags-label{color:#118fcc}.share-manager label span{display:inline-block;width:1.125rem}.manage-visibility label{width:100%;cursor:pointer}.manage-visibility{display:flex;justify-content:space-between}@media(max-width: 700px){.file-view__body-details{flex-direction:column;align-items:flex-start;gap:1rem}.file-view__body-details-stat{grid-template-columns:1fr;gap:.5rem}.file-view__body-details-stat span{display:flex;align-items:center}.share-manager__table,.share-manager__table thead,.share-manager__table tbody,.share-manager__table tr,.share-manager__table td{display:block;width:100% !important}.share-manager__table thead{display:none}.share-manager__table tr{border:1px solid #ccc;border-radius:8px;margin-bottom:1rem;padding:.5rem;background:#fff}.share-manager__table td{margin-bottom:.5rem;border:none !important;padding-left:0 !important}table.myfiles,table.myfiles tr,table.myfiles td,table.friendsfiles,table.friendsfiles tr,table.friendsfiles td{display:block;width:100% !important}table.myfiles th,table.friendsfiles th{display:none}table.myfiles tr,table.friendsfiles tr{border:1px solid #ccc;border-radius:8px;margin-bottom:1rem;padding:.5rem;background:#fff}.file-search-container{flex-direction:column}.file-search-container__keywords{flex-basis:auto;width:100%;border-right:none;border-bottom:1px solid rgba(20,20,27,.1);padding-bottom:1rem;margin-bottom:1rem}.results-container,.results-container thead,.results-container tbody,.results-container tr,.results-container td{display:block;width:100% !important}.results-container thead{display:none}.results-container tr{border-bottom:1px solid #eee;padding:1rem 0}.results-container td{margin-bottom:.5rem;word-break:break-all}}.file-section{margin-top:2rem;display:flex;flex-direction:column}.comments-section{margin-top:2rem;display:flex;justify-content:space-between}.comments-section__menu{display:flex;gap:1rem}.comments-section__menu-id{display:flex;align-items:center;gap:.25rem}#toggleunsub{position:relative;background:gray}table.channels th:nth-child(1){width:50%;text-align:start}table.channels td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.channels tr:hover{background-color:#eef3f6;cursor:pointer}table.channels tr.hidden{display:none}table{padding:.5rem}table.comments{border:1px solid #eee}table.comments th{height:40px}table.comments th:nth-child(1){width:2%}table.comments th:nth-child(2){width:40%}table.comments td{word-wrap:break-word}table.comments td:nth-child(2){text-align:start}table.files th:first-child{text-align:start;width:60%}table.files tr td:first-child{text-align:start}table.files td{word-wrap:break-word}#mtags{width:160px;text-align:center;font-size:medium;margin-left:10px;height:40px}.forums-node-panel{position:relative;bottom:200px;margin-left:200px;animation:fadein .5s}table.forums th:nth-child(1){width:50%;text-align:start}table.forums td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.forums tr:hover{background-color:#eef3f6;cursor:pointer}table.forums tr.hidden{display:none}#searchforum{position:relative;margin-left:250px}#forumdetails{position:relative;padding:10px}.p{margin:0}#toggleunsub{position:relative;background:gray}table.threads tr:hover{background-color:#eef3f6;cursor:pointer}table.threads td{word-wrap:break-word}table.threadreply th:nth-child(2){width:50%}table.threadreply th:nth-child(1){width:2%}table.threadreply td:nth-child(2){width:50%;text-align:start}table.threadreply td{word-wrap:break-word}table.threadreply tr:hover{background-color:#eef3f6;cursor:pointer}table.boards th:nth-child(1){width:50%;text-align:start}table.boards td:nth-child(1){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.boards tr:hover{background-color:#eef3f6;cursor:pointer}table.boards tr.hidden{display:none}#toggleunsub{position:relative;background:gray}#options{width:100px;text-align:center;font-size:medium;margin-left:20px;height:40px}#composepopup{height:80%;width:70%}#mtags{width:160px;text-align:center;font-size:medium;margin-left:10px;height:40px}.mail .permission-flag{margin-bottom:1rem;display:flex;gap:1rem}.mail-tags{padding:.5rem;border:1px solid rgba(20,20,27,.2);border-radius:6px}.mail-tags__container{display:flex;flex-direction:column}.mail-tags__container .tag-item{display:flex;align-items:center;gap:4px;border-bottom:1px solid rgba(20,20,27,.1);padding:2px 0}.mail-tags__container .tag-item:last-child{border:none}.mail-tags__container .tag-item__color{width:1.25rem;height:1.25rem;aspect-ratio:1}.mail-tags__container .tag-item__name{font-size:1.125rem}.mail-tags__container .tag-item__modify{margin-left:auto;font-size:.75rem;display:flex;gap:4px}.mail-tags__container .tag-item:hover{background-color:#eef3f6}.mail-tags__container .tag-item button,.mail-tags__container .tag-item button.red{padding:.25rem .6rem}.mail-tags-form .input-field{margin-bottom:.5rem}.mail-tags-form .input-field label{margin-right:.5rem}.external-address{margin:0;padding-left:1rem;height:100px;overflow:hidden auto}.external-address::-webkit-scrollbar{display:none}.proxy-server{display:flex;flex-direction:column;gap:4px}.proxy-server__tor>h4,.proxy-server__i2p>h4{margin-bottom:.25rem}.proxy-server__tor>input,.proxy-server__i2p>input{margin-right:.5rem}.proxy-server__tor .proxy-outgoing,.proxy-server__i2p .proxy-outgoing{display:inline-flex;align-items:center;gap:.5rem}.proxy-server__tor .proxy-outgoing__status,.proxy-server__i2p .proxy-outgoing__status{width:1rem;height:1rem;aspect-ratio:1;border:1px solid #000;border-radius:50%}.config-files{display:flex;flex-direction:column;gap:1rem}.proxy-server-container{width:100%;display:flex;flex-direction:column;gap:1rem}.proxy-description{color:#334155;font-size:.95rem;margin-bottom:.5rem}.proxy-rows-container{display:flex;flex-direction:column;gap:.75rem;width:100%}.proxy-row{display:grid;grid-template-columns:160px 220px 220px auto;gap:.75rem;align-items:center;width:100%}.proxy-label{font-size:.95rem;font-weight:500;color:#1e293b}.proxy-addr-input,.proxy-port-input{width:100% !important;max-width:none !important}.proxy-status-container{display:flex;align-items:center;gap:.5rem}.proxy-status-bullet{width:14px;height:14px;border-radius:50%;display:inline-block;border:1px solid #475569}.proxy-status-text{font-size:.95rem;color:#1e293b} + */@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{white-space:nowrap !important;flex-shrink:0 !important}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)}button.red{white-space:nowrap !important;flex-shrink:0 !important}.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%}@media(max-width: 768px){.media-item__desc{display:none !important}.media-item__details{flex-basis:100% !important;width:100% !important}}.active-link{background:hsla(0,0%,100%,.1) !important}.nav-menu{background-color:#14141b;box-shadow:0 5px 5px #222;display:flex;flex-direction:column;align-items:center;height:100%;padding:.25rem;margin-right:0rem}.nav-menu__logo{padding:1.2rem 0;display:flex;align-items:center;gap:.3rem}.nav-menu__logo img{width:1.6rem}.nav-menu__logo h5{line-height:1;color:#fff}.nav-menu__box{padding:2rem .125rem;display:flex;flex-direction:column;gap:.5rem;position:relative}.nav-menu__box .item{margin:0;padding:.675rem .5rem;width:10rem;display:flex;align-items:center;line-height:1;border-radius:.5rem;text-decoration:none;color:#ccc;text-transform:capitalize;transition:0ms}.nav-menu__box .item:hover{background-color:rgba(238,243,246,.15)}.nav-menu__box .item i.sidenav-icon{width:2.5rem;height:1.4rem;display:grid;place-items:center}.nav-menu__box .item.item-selected{color:#9bdaff;background-color:rgba(155,218,255,.15);font-weight:medium}.nav-menu__box button.toggle-nav{display:none;position:absolute;padding:0;top:0;right:-1rem;background:rgb(77.5,186.5157480315,255);width:1.5rem;height:1.5rem;aspect-ratio:1;justify-content:center;align-items:center;border-radius:50%;box-shadow:none}.nav-menu.collapsed .nav-menu__logo .logo-container{display:flex;flex-direction:column;align-items:center;gap:.5rem}.nav-menu.collapsed .nav-menu__logo .logo-container>*:not(img){display:block}.nav-menu.collapsed .nav-menu__logo .nav-menu__logo-text{display:none !important}.nav-menu.collapsed .nav-menu__box .item{padding:.675rem 0;width:2.5rem;justify-content:center;transition:300ms}.nav-menu.collapsed .nav-menu__box .item span,.nav-menu.collapsed .nav-menu__box .item p{display:none !important}.nav-menu.collapsed button i{rotate:180deg}.nav-menu:hover button.toggle-nav{display:flex}.sidebar{width:13rem;background-color:#fff;display:flex;flex-direction:column}.sidebar a{text-decoration:none;text-transform:capitalize;padding:1rem;cursor:pointer;color:#999}.sidebar a:hover{color:#222}.sidebar .selected-sidebar-link{font-weight:bold;color:#222;border-left:5px solid #3ba4d7;animation:expand-left-border .1s}.sidebar-mobile-toggle,.sidebar-drawer__title,.sidebar-drawer__backdrop{display:none !important}.sidebarquickview>h6{padding:.5rem}.sidebarquickview a{text-decoration:none;text-transform:capitalize;padding:.5rem 1rem;display:block;color:#999}.sidebarquickview a a:hover{color:#222}.sidebarquickview .selected-sidebarquickview-link{font-weight:bold;color:#222;border-left:5px solid #3ba4d7;animation:expand-left-border .1s}.node-panel{width:100%;padding:.5rem;animation:fadein .5s}@keyframes expand-left-border{from{border-left:0}to{border-left:5px solid #3ba4d7}}@media(max-width: 700px){.tab-content{flex-direction:column}.sidebar{width:100% !important;flex-direction:row !important;overflow-x:auto !important;overflow-y:hidden !important;white-space:nowrap !important;border-bottom:1px solid rgba(20,20,27,.1) !important;background:#fff !important;z-index:50 !important;flex-shrink:0 !important;height:auto !important;padding:0 !important}.sidebar a{display:inline-block !important;padding:.8rem 1.2rem !important;border-bottom:3px solid rgba(0,0,0,0) !important;border-left:none !important}.sidebar .selected-sidebar-link{border-left:none !important;border-bottom:3px solid #3ba4d7 !important;animation:none !important}.sidebarquickview>h4,.sidebarquickview>h6{display:none !important}.sidebar-drawer{display:block;position:relative;width:100%;height:44px;z-index:1000}.sidebar-mobile-toggle{display:inline-flex !important;width:40px;height:40px;align-items:center;justify-content:center;border:0;border-radius:6px;background:#fff;color:#0f172a;cursor:pointer;font-size:1.15rem}.sidebar-mobile-toggle:hover{background:#f1f5f9}.sidebar-drawer .sidebar{position:fixed !important;top:0;left:0;display:flex !important;flex-direction:column !important;width:min(82vw,300px) !important;height:100dvh !important;padding:1rem 0 !important;overflow-y:auto !important;overflow-x:hidden !important;transform:translateX(-105%);transition:transform 180ms ease;border:0 !important;border-right:1px solid #e2e8f0 !important;background:#fff !important;opacity:1 !important;pointer-events:auto !important;box-shadow:8px 0 24px rgba(15,23,42,.16);white-space:normal !important;z-index:1002}.sidebar-drawer .sidebar.sidebar--mobile-open{transform:translateX(0)}.sidebar-drawer .sidebar a{display:block !important;padding:.8rem 1.25rem !important;border:0 !important;border-left:4px solid rgba(0,0,0,0) !important;color:#334155 !important;font-size:.95rem}.sidebar-drawer .sidebar .selected-sidebar-link{border-left-color:#3ba4d7 !important;border-bottom:0 !important;background:#f0f9ff;color:#0f172a !important}.sidebar-drawer__title{display:block !important;margin:0 1.25rem .65rem;padding-bottom:.75rem;border-bottom:1px solid #e2e8f0;color:#64748b;font-size:.75rem;font-weight:700;letter-spacing:.06em;text-transform:uppercase}.sidebar-drawer__backdrop{display:none !important;position:fixed;inset:0;background:rgba(15,23,42,.35);pointer-events:none;z-index:1001}}@media(min-width: 701px){.sidebar-drawer{display:contents}.sidebar-mobile-toggle,.sidebar-drawer__title,.sidebar-drawer__backdrop{display:none !important}}.posts{height:100%;margin-top:1rem;flex-direction:column;overflow:auto}.posts__heading{display:flex;flex-direction:column;justify-content:space-between}.posts-container{height:100%;padding:1rem;display:grid;grid-template-columns:repeat(auto-fill, minmax(150px, 1fr));gap:2rem;border:1px solid rgba(20,20,27,.1);border-radius:4px;overflow:auto}.posts-container-card{min-height:240px;flex-direction:column;border:1px solid rgba(20,20,27,.5);border-radius:4px;cursor:pointer;text-align:center}.posts-container-card img{flex-basis:90%;object-fit:cover}.posts-container-card p{padding:0 .125rem;flex-basis:10%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.progress-bar{width:100%;height:2rem;position:relative;text-align:center;background-color:#eef3f6;border-radius:20px;overflow:hidden}.progress-bar__status{position:absolute;top:0;left:0;height:100%;color:#14141b;background-color:#019dff}.progress-bar__percent{position:absolute;inset:0;margin:auto;width:fit-content;height:fit-content}.progress-bar-chunks{position:relative;margin-top:.5rem;width:100%;height:2rem;display:flex;border-radius:.25rem;overflow:hidden;background-color:#eef3f6}.progress-bar-chunks .chunk{width:100%}.progress-bar-chunks .chunk[data-chunkVal="0"]{background-color:rgba(155,218,255,.2)}.progress-bar-chunks .chunk[data-chunkVal="1"]{background-color:#ff3a4a}.progress-bar-chunks .chunk[data-chunkVal="2"]{background-color:#019dff}.progress-bar-chunks .chunk[data-chunkVal="3"]{background-color:#fcba03}.progress-bar-chunks__percent{position:absolute;inset:0;margin:auto;width:fit-content;height:fit-content}.statusbar{display:flex;justify-content:space-between;align-items:center;height:28px;background-color:#14141b;border-top:1px solid #2e2e38;padding:0 1rem;font-size:.8rem;color:#94a3b8;z-index:100;box-sizing:border-box;user-select:none;flex-shrink:0}.statusbar-left{display:flex;align-items:center;gap:.75rem}.statusbar-right{display:flex;align-items:center;gap:.75rem}.statusbar-item{display:flex;align-items:center;gap:.3rem}.statusbar-divider{width:1px;height:14px;background-color:#2e2e38}.status-bullet{width:8px;height:8px;border-radius:50%;display:inline-block;box-shadow:0 0 4px rgba(0,0,0,.5)}@media(max-width: 768px){.statusbar{height:22px !important;padding:0 .4rem !important;font-size:.72rem !important}.statusbar .statusbar-left,.statusbar .statusbar-right{gap:.35rem !important}.statusbar .statusbar-label,.statusbar .statusbar-total-bytes,.statusbar .statusbar-extra-info{display:none !important}.statusbar .statusbar-divider{height:10px !important;margin:0 .1rem !important}}.widget{height:100%;padding:1rem;display:flex;flex-direction:column;gap:.5rem;background-color:#fff;border-radius:.5rem;overflow:auto}.widget .top-heading{display:flex;justify-content:space-between}.widget__heading{display:flex;justify-content:space-between;align-items:center;border-bottom:2px solid #999}.widget__body{height:100%;display:flex;flex-direction:column;overflow:auto}.widget__body-heading{display:flex;justify-content:space-between;align-items:center}.widget__body-heading .action{display:flex;gap:.5rem}.widget__body-content{height:100%;overflow:auto}.widget__body-box{display:flex;flex-direction:column;gap:.5rem}.widget-half{max-width:50%}#modal-container{display:none;position:fixed;z-index:1;height:100%;top:0;left:0;width:100%;background-color:rgba(0,0,0,.2)}.modal-content{position:absolute;color:#555;width:40%;min-height:10rem;height:max-content;padding:1.5rem;inset:0;margin:auto;background-color:#fff;border-radius:.5rem;animation:fadein .5s;display:flex;flex-direction:column}.modal-content button:last-child{margin-top:auto}.modal-content .close-btn{position:absolute;right:1.5rem}.modal-content .widget{padding:0}#notification-container{position:absolute;bottom:0;right:0}.login-page{background-image:linear-gradient(-45deg, rgba(1, 157, 255, 0.75), rgba(17, 143, 204, 0.75));height:100%;animation:fadein .5s}.login-page .login-container{background-color:#fff;box-shadow:3px 3px 5px rgba(20,20,27,.4);margin:auto;position:relative;top:100px;max-width:400px;max-height:500px;border-radius:5px;display:flex;flex-direction:column;align-items:center}.login-page .login-container input{padding:.375rem .75rem;border-radius:.275rem}.login-page .login-container *{margin-bottom:1rem}.login-page .login-container>img{margin:1rem 0 2rem}.login-page .login-container extra{margin:0}.login-page .login-container>a{text-decoration:underline;cursor:pointer}.login-page .extra>label,.login-page .extra>br,.login-page .extra>input{margin-bottom:0}.homepage{margin:2rem auto 0;display:flex;flex-direction:column;gap:4rem}.homepage .logo{display:flex;justify-content:center;align-items:center}.homepage .logo img{width:90px}.homepage .logo .retroshareText{display:flex;flex-direction:column;align-items:center}.homepage .logo .retroshareText .retrotext{font-size:36px;font-weight:600;line-height:1.125}.homepage .logo .retroshareText .retrotext>span{color:#118fcc}.homepage .logo .retroshareText>b{font-size:14px;line-height:1}.homepage .certificate{display:flex;flex-direction:column;gap:4rem}.homepage .certificate__heading{text-align:center}.homepage .certificate__heading>h1{margin-bottom:1rem}.homepage .certificate__content{display:flex;flex-direction:column;gap:2rem;padding:2rem;text-align:center;border:1.5px solid rgba(17,143,204,.2);border-radius:6px;box-shadow:0px 0px 8px 2px rgba(20,20,27,.05)}.homepage .certificate__content .rsId>p{margin-bottom:.5rem;color:#118fcc}.homepage .certificate__content .retroshareID{padding:.25rem;display:flex;align-items:center;justify-self:start;font-size:1.25rem;border-radius:4px;background:rgba(20,20,27,.05)}.homepage .certificate__content .retroshareID .textArea{padding:0;width:100%;height:auto;font-size:1rem;font-family:monospace;background:rgba(0,0,0,0);border:none;resize:none;overflow:hidden;field-sizing:content}.homepage .certificate__content .retroshareID i{color:#118fcc}.homepage .certificate__content .retroshareID>i{margin:0 .5rem;cursor:pointer}.homepage .certificate__content .webhelp{padding:.5rem;background:#f5f5f5;display:flex;justify-content:center;align-items:center;gap:.5rem;border-radius:4px;border:1px solid rgba(20,20,27,.5);width:fit-content;cursor:pointer}.homepage .certificate__content .webhelp-container{display:grid;place-items:center}.homepage .certificate__content .webhelp:hover{background:#eef3f6;border:1px solid #14141b}.homepage .certificate__content .webhelp>i{font-size:1.2rem;color:green}.homepage .certificate__content .add-friend>h6,.homepage .certificate__content .webhelp-container>h6{font-weight:normal;margin-bottom:.5rem}@media(max-width: 768px){.homepage{margin:1rem auto !important;padding:0 1rem !important;gap:2rem !important;max-width:100% !important;box-sizing:border-box !important}.homepage .logo{flex-direction:column !important;gap:.5rem !important;text-align:center !important}.homepage .logo img{width:60px !important}.homepage .logo .retroshareText .retrotext{font-size:1.6rem !important}.homepage .logo .retroshareText>b{font-size:.75rem !important}.homepage .certificate{gap:2rem !important}.homepage .certificate__heading>h1{font-size:1.35rem !important;margin-bottom:.5rem !important}.homepage .certificate__heading{font-size:.85rem !important}.homepage .certificate__content{padding:1rem .75rem !important;gap:1.25rem !important}.homepage .certificate__content .retroshareID{padding:.5rem !important;font-size:.85rem !important;max-width:100% !important}.homepage .certificate__content .retroshareID .textArea{font-size:.8rem !important;word-break:break-all !important;overflow-wrap:anywhere !important}}.network-container{display:flex;height:100%;width:100%;overflow:hidden;background-color:#f1f5f9}.network-left-pane{width:320px;min-width:300px;max-width:350px;border-right:1px solid #cbd5e1;display:flex;flex-direction:column;background:#fff;box-shadow:2px 0 5px rgba(0,0,0,.05)}.own-profile-card{padding:1.25rem;border-bottom:1px solid #e2e8f0;background:linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);display:flex;flex-direction:column;gap:.75rem}.own-profile-card .profile-header{display:flex;align-items:center;gap:1rem}.own-profile-card .profile-header .profile-avatar-wrapper{position:relative;flex-shrink:0}.own-profile-card .profile-header .profile-avatar-wrapper .status-dot{position:absolute;bottom:-1px;right:-1px;width:13px;height:13px;border-radius:50%;border:2px solid #fff;box-shadow:0 1px 3px rgba(0,0,0,.25)}.own-profile-card .profile-info{display:flex;flex-direction:column;flex:1;overflow:hidden}.own-profile-card .profile-info .profile-name{font-weight:700;color:#1e293b;font-size:1.1rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.own-profile-card .profile-info .profile-status{font-size:.85rem;color:#10b981;font-weight:500;display:flex;align-items:center;gap:.35rem}.own-profile-card .profile-info .profile-status::before{content:"";display:inline-block;width:8px;height:8px;background-color:var(--profile-status-color, #10b981);border-radius:50%}.own-profile-card .own-identity-select-container{display:flex;flex-direction:column;gap:.25rem}.own-profile-card .own-identity-select-container label{font-size:.75rem;color:#64748b;font-weight:600;text-transform:uppercase;letter-spacing:.05em}.own-profile-card .own-identity-select-container select.own-identity-select{width:100%;padding:.375rem .5rem;font-size:.85rem;border:1px solid #cbd5e1;border-radius:.375rem;background-color:#fff;color:#334155;outline:none;cursor:pointer;transition:border-color .2s}.own-profile-card .own-identity-select-container select.own-identity-select:focus{border-color:#3ba4d7}.friends-list-container{flex:1;display:flex;flex-direction:column;overflow:hidden;position:relative}.friends-list-container .searchbar-container{padding:.75rem 1rem;border-bottom:1px solid #e2e8f0}.friends-list-container .searchbar-container input.searchbar{width:100%;padding:.5rem .75rem;font-size:.9rem;border:1px solid #cbd5e1;border-radius:.375rem;background-color:#f8fafc;outline:none;transition:all .2s}.friends-list-container .searchbar-container input.searchbar:focus{background-color:#fff;border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.friends-list-container .friends-scroll{flex:1;overflow-y:auto;padding:.5rem 0}.friend-list-item{display:flex;align-items:center;gap:.75rem;padding:.75rem 1rem;margin:.125rem .5rem;border-radius:.5rem;cursor:pointer;transition:all .2s}.friend-list-item:hover{background-color:#f1f5f9}.friend-list-item.selected{background-color:#e0f2fe}.friend-list-item.selected .friend-name{color:#0369a1;font-weight:600}.friend-list-item .friend-avatar{position:relative;flex-shrink:0}.friend-list-item .friend-avatar .status-dot{position:absolute;bottom:-1px;right:-1px;width:13px;height:13px;border-radius:50%;border:2px solid #fff;box-shadow:0 1px 3px rgba(0,0,0,.25)}.friend-list-item .friend-meta{flex:1;min-width:0}.friend-list-item .friend-meta .friend-name{font-size:.95rem;color:#334155;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .2s}.friend-list-item .friend-meta .friend-status{font-size:.8rem;color:#94a3b8}.friend-list-item .friend-meta .friend-status.online{color:#10b981;font-weight:500}.network-right-pane{flex:1;display:flex;flex-direction:column;overflow:hidden;background-color:#f8fafc}.network-pane-placeholder{flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;color:#94a3b8;gap:1rem;padding:2rem;text-align:center}.network-pane-placeholder i{font-size:4rem;color:#cbd5e1}.network-pane-placeholder p{font-size:1.1rem;max-width:400px}.network-tabs{display:flex;background-color:#fff;border-bottom:1px solid #cbd5e1;padding:.5rem 1rem 0;gap:.5rem}.network-tabs .tab-btn{padding:.625rem 1.25rem;font-size:.95rem;font-weight:600;color:#64748b;background:rgba(0,0,0,0);border:none;border-radius:.375rem .375rem 0 0;border-bottom:3px solid rgba(0,0,0,0);cursor:pointer;box-shadow:none;transition:all .2s}.network-tabs .tab-btn:hover{color:#334155;background-color:#f1f5f9}.network-tabs .tab-btn.active{color:#3ba4d7;border-bottom-color:#3ba4d7;background-color:rgba(0,0,0,0)}.network-tab-content{flex:1;overflow-y:auto;padding:1.5rem}.network-detail-view{display:flex;flex-direction:column;gap:1.5rem}.network-detail-view .detail-header{display:flex;align-items:center;gap:1.5rem;padding-bottom:1.5rem;border-bottom:1px solid #e2e8f0}.network-detail-view .detail-header .detail-title{flex:1}.network-detail-view .detail-header .detail-title h2{font-size:1.75rem;font-weight:800;color:#1e293b;margin-bottom:.25rem}.network-detail-view .detail-header .detail-title .detail-subtitle{font-size:.9rem;color:#64748b;display:flex;align-items:center;gap:.5rem}.network-detail-view .detail-header .detail-actions{display:flex;gap:.75rem}.network-detail-view .detail-header .detail-actions button{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.9rem}.network-detail-view .detail-section{background-color:#fff;border-radius:.5rem;border:1px solid #e2e8f0;padding:1.25rem;box-shadow:0 1px 3px rgba(0,0,0,.02)}.network-detail-view .detail-section h3{font-size:1.1rem;font-weight:700;color:#334155;margin-bottom:1rem;padding-bottom:.5rem;border-bottom:1px solid #f1f5f9}.network-detail-view .detail-section .info-grid{display:grid;grid-template-columns:120px 1fr;row-gap:.75rem;font-size:.9rem}.network-detail-view .detail-section .info-grid .info-label{font-weight:600;color:#64748b}.network-detail-view .detail-section .info-grid .info-value{color:#1e293b;word-break:break-all}.network-detail-view .locations-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));gap:1rem}.network-detail-view .location-card{background-color:#fff;border:1px solid #e2e8f0;border-radius:.5rem;padding:1rem;display:flex;flex-direction:column;gap:.5rem;box-shadow:0 1px 3px rgba(0,0,0,.02)}.network-detail-view .location-card .loc-header{display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #f1f5f9;padding-bottom:.5rem;margin-bottom:.25rem}.network-detail-view .location-card .loc-header .loc-name{font-weight:700;color:#334155;font-size:.95rem}.network-detail-view .location-card .loc-header .loc-status{font-size:.75rem;font-weight:600;padding:.125rem .5rem;border-radius:.25rem}.network-detail-view .location-card .loc-header .loc-status.online{background-color:#d1fae5;color:#065f46}.network-detail-view .location-card .loc-header .loc-status.offline{background-color:#f1f5f9;color:#475569}.network-detail-view .location-card .loc-body{font-size:.85rem;display:grid;grid-template-columns:80px 1fr;row-gap:.25rem}.network-detail-view .location-card .loc-body .loc-label{color:#64748b}.network-detail-view .location-card .loc-body .loc-val{color:#334155;word-break:break-all}.network-detail-view .location-card .loc-footer{margin-top:.5rem;display:flex;justify-content:flex-end}.network-detail-view .location-card .loc-footer button{font-size:.8rem;padding:.25rem .75rem}.network-chat-view{display:flex;flex-direction:column;height:100%;overflow:hidden;background-color:#f8fafc}.network-chat-view .chat-messages{flex:1;overflow-y:auto;padding:1.25rem;display:flex;flex-direction:column;gap:1rem}.network-chat-view .chat-bubble-container{display:flex;flex-direction:column;max-width:70%}.network-chat-view .chat-bubble-container.outgoing{align-self:flex-end;align-items:flex-end}.network-chat-view .chat-bubble-container.outgoing .chat-bubble{background-color:#3ba4d7;color:#fff;border-bottom-right-radius:.125rem}.network-chat-view .chat-bubble-container.incoming{align-self:flex-start;align-items:flex-start}.network-chat-view .chat-bubble-container.incoming .chat-bubble{background-color:#fff;color:#1e293b;border:1px solid #e2e8f0;border-bottom-left-radius:.125rem}.network-chat-view .chat-bubble-container .chat-sender{font-size:.75rem;color:#64748b;margin-bottom:.25rem;padding:0 .25rem}.network-chat-view .chat-bubble-container .chat-bubble{padding:.625rem .875rem;border-radius:.75rem;font-size:.925rem;line-height:1.4;white-space:break-spaces;word-break:break-word;box-shadow:0 1px 2px rgba(0,0,0,.05)}.network-chat-view .chat-bubble-container .chat-time{font-size:.7rem;color:#94a3b8;margin-top:.25rem;padding:0 .25rem}.network-chat-view .chat-input-area{padding:1rem;background-color:#fff;border-top:1px solid #cbd5e1;display:flex;gap:.75rem;align-items:center}.network-chat-view .chat-input-area textarea.chat-textarea{flex:1;resize:none;height:40px;padding:.5rem .75rem;border:1px solid #cbd5e1;border-radius:.375rem;font-size:.9rem;outline:none;transition:all .2s}.network-chat-view .chat-input-area textarea.chat-textarea:focus{border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.network-chat-view .chat-input-area button.send-btn{padding:.5rem 1.25rem;font-size:.9rem;height:40px;display:flex;align-items:center;gap:.5rem}.network-chat-view .chat-warning{flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;color:#64748b;text-align:center;padding:2rem;gap:1rem}.network-chat-view .chat-warning i{font-size:3rem;color:#cbd5e1}.network-chat-view .chat-warning h4{font-weight:700;color:#334155}.network-chat-view .chat-warning p{max-width:350px;font-size:.9rem}@media(max-width: 768px){.network-container{flex-direction:column !important}.network-left-pane{width:100% !important;max-width:none !important;height:45% !important;border-right:none !important;border-bottom:1px solid #cbd5e1 !important}.network-right-pane{height:55% !important;flex:1 !important}.detail-actions button .btn-text,.detail-header .detail-actions button .btn-text{display:none !important}.detail-actions button,.detail-header .detail-actions button{padding:.45rem .65rem !important;min-width:38px !important;height:38px !important;justify-content:center !important;align-items:center !important}.detail-actions button i,.detail-header .detail-actions button i{margin:0 !important;font-size:1.05rem !important}.network-detail-view .detail-header{flex-direction:column !important;align-items:flex-start !important;gap:1rem !important}.network-detail-view .detail-header .friend-avatar{margin-bottom:.25rem !important}.locations-grid{grid-template-columns:1fr !important}.network-chat-view .chat-input-area{padding:.4rem !important;gap:.2rem !important;min-width:0}.network-chat-view .chat-input-area .chat-hub-action-btn,.network-chat-view .chat-input-area label.chat-hub-action-btn{width:28px !important;height:28px !important;min-width:28px !important;padding:0 !important;font-size:.95rem !important}.network-chat-view .chat-input-area .emoji-picker-wrapper{flex:0 0 28px}.network-chat-view .chat-input-area textarea.chat-textarea{min-width:0 !important;padding:.4rem !important}.network-chat-view .chat-input-area .send-btn{width:34px !important;min-width:34px !important;height:32px !important;padding:0 !important;font-size:0 !important;justify-content:center}.network-chat-view .chat-input-area .send-btn i{margin:0 !important;font-size:1rem !important}}.identity{color:#444;font-size:1.1em;margin:20px;padding:10px;border:1px solid #aaa;border-radius:20px}.identity>h4{margin:5px;font-size:1.3em}.identity button{font-size:.9em}.identity .details{display:grid;grid-template-columns:140px auto;grid-row-gap:5px;justify-content:left}.defaultAvatar{width:3rem;height:3rem;aspect-ratio:1;background:#b0c4de;border-radius:50%;display:grid;place-items:center}.defaultAvatar p{font-weight:900;color:#666f7f;transform:translateY(1px)}img.avatar{display:block;width:3rem;height:max-content;aspect-ratio:1;margin-right:.3em;border-radius:50%}.counter{margin-left:.5em}.counter:before{content:"("}.counter:after{content:")"}.chatInit{margin-left:.5em;color:green;cursor:pointer}.people-sidebar-header{display:flex;flex-direction:column;padding:.75rem 1rem .5rem 1rem;gap:.75rem;border-bottom:1px solid #e2e8f0;background-color:#fff}.people-sidebar-header .searchbar-wrapper{position:relative;display:flex;align-items:center}.people-sidebar-header .searchbar-wrapper i.fa-search{position:absolute;left:.85rem;color:#94a3b8;font-size:.9rem}.people-sidebar-header .searchbar-wrapper input.searchbar-input{width:100%;padding:.5rem .75rem .5rem 2.25rem;border:1px solid #e2e8f0;border-radius:.5rem;font-size:.9rem;background-color:#f8fafc;color:#1e293b;outline:none;transition:all .2s ease}.people-sidebar-header .searchbar-wrapper input.searchbar-input:focus{border-color:#3b82f6;background-color:#fff;box-shadow:0 0 0 3px rgba(59,130,246,.1)}.people-sidebar-header .segmented-control{display:flex;background-color:#f1f5f9;padding:3px;border-radius:.5rem;gap:4px}.people-sidebar-header .segmented-control button.segment-tab{flex:1;display:flex;align-items:center;justify-content:center;gap:.5rem;padding:.5rem .75rem;font-size:.9rem;font-weight:600;color:#64748b;background:rgba(0,0,0,0);border:none;border-radius:.375rem;cursor:pointer;box-shadow:none;transition:all .2s ease}.people-sidebar-header .segmented-control button.segment-tab:hover{color:#1e293b}.people-sidebar-header .segmented-control button.segment-tab.active{background-color:#fff;color:#0f172a;box-shadow:0 1px 3px rgba(0,0,0,.1),0 1px 2px rgba(0,0,0,.06)}.people-sidebar-header .segmented-control button.segment-tab.active .segment-badge{background-color:#019dff;color:#fff}.people-sidebar-header .segmented-control button.segment-tab .segment-badge{display:inline-flex;align-items:center;justify-content:center;background-color:#cbd5e1;color:#334155;font-size:.75rem;font-weight:700;min-width:1.25rem;height:1.25rem;padding:0 .35rem;border-radius:9999px;line-height:1;transition:all .2s ease}.people-sidebar-header .sub-filter-row{display:flex;align-items:center;justify-content:space-between;gap:.5rem;min-height:32px}.people-sidebar-header .sub-filter-row select.filter-select{padding:.35rem .6rem;border:1px solid #cbd5e1;border-radius:.375rem;font-size:.85rem;font-weight:600;color:#475569;background-color:#fff;cursor:pointer;outline:none}.people-sidebar-header .sub-filter-row .btn-add-id{display:flex;align-items:center;justify-content:center;width:32px;height:32px;border-radius:.375rem;background-color:#3b82f6;color:#fff;border:none;cursor:pointer;font-size:.9rem;transition:background-color .2s}.people-sidebar-header .sub-filter-row .btn-add-id:hover{background-color:#2563eb}.friends-list-container .people-context-menu{position:absolute;left:2rem;width:210px;background-color:#fff;border:1px solid #e2e8f0;box-shadow:0 4px 10px rgba(0,0,0,.15);border-radius:.375rem;z-index:1010;padding:.25rem 0;display:flex;flex-direction:column}.friends-list-container .people-context-menu .menu-item{padding:.5rem 1rem;font-size:.85rem;color:#334155;cursor:pointer;display:flex;align-items:center;transition:background-color .2s}.friends-list-container .people-context-menu .menu-item:hover{background-color:#f1f5f9;color:#0f172a}.people-container{display:flex !important;flex-direction:row !important;height:100% !important;width:100% !important;overflow:hidden !important;background-color:#f1f5f9 !important}.people-left-pane{width:320px !important;min-width:300px !important;max-width:350px !important;height:100% !important;border-right:1px solid #cbd5e1 !important;display:flex !important;flex-direction:column !important;background:#fff !important;box-shadow:2px 0 5px rgba(0,0,0,.05) !important;flex-shrink:0 !important;overflow:hidden !important}.people-right-pane{flex:1 !important;min-width:0 !important;height:100% !important;display:flex !important;flex-direction:column !important;overflow:hidden !important;background-color:#f8fafc !important}.people-list-container{flex:1 !important;overflow-y:auto !important;padding:.5rem 0 !important}.chat-item{display:flex !important;align-items:center !important;gap:.75rem !important;padding:.65rem .85rem !important;margin:.2rem .5rem !important;border-radius:.5rem !important;cursor:pointer !important;transition:all .2s ease !important;position:relative !important}.chat-item:hover{background-color:#f1f5f9 !important}.chat-item.selected{background-color:#e0f2fe !important}.chat-item.selected .chat-name{color:#0369a1 !important;font-weight:700 !important}.chat-item .chat-avatar-wrapper{position:relative !important;flex-shrink:0 !important}.chat-item .chat-avatar-wrapper .status-dot{position:absolute !important;bottom:-1px !important;right:-1px !important;width:13px !important;height:13px !important;border-radius:50% !important;border:2px solid #fff !important}.chat-item .chat-info{flex:1 !important;min-width:0 !important;display:flex !important;flex-direction:column !important}.chat-item .chat-info .chat-name{font-size:.95rem !important;font-weight:600 !important;color:#1e293b !important;white-space:nowrap !important;overflow:hidden !important;text-overflow:ellipsis !important}.chat-item .chat-info .chat-last-msg{font-size:.825rem !important;color:#64748b !important;white-space:nowrap !important;overflow:hidden !important;text-overflow:ellipsis !important;margin-top:.1rem !important}.chat-item .chat-meta{display:flex !important;flex-direction:column !important;align-items:flex-end !important;flex-shrink:0 !important}.chat-item .chat-meta .chat-time{font-size:.75rem !important;color:#94a3b8 !important;font-weight:500 !important}@media(max-width: 768px){.people-container{flex-direction:column !important}.people-left-pane{width:100% !important;max-width:none !important;height:45% !important;border-right:none !important;border-bottom:1px solid #cbd5e1 !important}.people-right-pane{height:55% !important}.detail-actions button .btn-text,.detail-header .detail-actions button .btn-text{display:none !important}.chat-tunnel-status .tunnel-label,.select-own-profile .chatting-as-label{display:none !important}.detail-actions button,.detail-header .detail-actions button{padding:.45rem .65rem !important;min-width:38px !important;height:38px !important;justify-content:center !important;align-items:center !important}}.people-context-menu{position:absolute !important;z-index:9999 !important;background-color:#fff !important;border:1px solid #cbd5e1 !important;border-radius:8px !important;box-shadow:0 10px 25px -5px rgba(0,0,0,.15),0 8px 10px -6px rgba(0,0,0,.1) !important;padding:.35rem 0 !important;min-width:180px !important;font-family:inherit !important;overflow:hidden !important}.people-context-menu .menu-item{display:flex !important;align-items:center !important;padding:.6rem .9rem !important;font-size:.875rem !important;font-weight:500 !important;color:#1e293b !important;cursor:pointer !important;transition:background-color .15s ease,color .15s ease !important;user-select:none !important}.people-context-menu .menu-item:hover{background-color:#f1f5f9 !important;color:#0284c7 !important}.people-context-menu .menu-item i{font-size:1rem !important;width:1.25rem !important;text-align:center !important}.lobby{margin:10px;border:1px solid #aaa;border-radius:20px}.lobby .mainname{margin:20px;font-weight:100;font-size:1.2em}.topic{color:#666}.lobby>.topic{font-size:.95em;margin-left:25px;margin-bottom:5px}.lefttitle{margin-top:15px;margin-bottom:0;font-weight:100;font-size:1.2em}.leftname{margin-top:5px;margin-bottom:5px;padding:5px;font-weight:100;font-size:1em}.leftlobby>.topic{font-size:.75em;margin-left:15px;margin-bottom:5px}.subscribed,.public{cursor:pointer}.leftlobby{border:1px solid #aaa;border-radius:10px;margin-top:5px;background-color:#fff}.leftlobby.selected-lobby,.selectedidentity{color:#fff;background-color:#3ba4d7}.rightbar{position:absolute;width:185px;background-color:#fff;overflow:auto;top:130px;bottom:15px;right:15px}.user{padding:5px}.lobbyName{padding:15px;margin-top:2rem}.lobbies{position:absolute;width:185px;left:165px;bottom:15px;top:130px;overflow:auto}.messages,.setup{position:absolute;background-color:#fff;top:130px;left:360px;right:215px;overflow:auto}.messages{bottom:115px}.messagetext{white-space:break-spaces;margin-right:5px}.message>*{margin-left:5px}.username{color:#006400;font-weight:bolder}.chatMessage{position:absolute;background-color:#fff;height:85px;bottom:15px;right:215px;left:360px}textarea.chatMsg{height:100%;width:100%}.chatatchar{margin-left:.2em;margin-right:.2em;color:silver}.setupicon{margin-left:1em;cursor:pointer}.leaveicon{margin-left:1em;cursor:pointer;color:#d40000}.selectidentity{margin:15px;font-size:1.2em}.setup>.identity{cursor:pointer}.setup{bottom:15px}.createDistantChat{margin-top:1em}.no-lobbies .messages,.no-lobbies .chatMessage,.no-lobbies .setup{left:165px}@media(min-width: 900px){.node-panel.chat-room{display:grid !important;grid-template-columns:250px 1fr 200px !important;grid-template-rows:auto 1fr auto !important;grid-template-areas:"lobbies header rightbar" "lobbies messages rightbar" "lobbies input rightbar" !important;padding:0 !important;height:100% !important}.node-panel.chat-room .lobbyName{grid-area:header;padding:10px;border-bottom:1px solid #eee;margin:0;z-index:10;background:#fff}.node-panel.chat-room .lobbies{grid-area:lobbies;position:static !important;width:auto !important;height:auto !important;border-right:1px solid #ccc;overflow-y:auto;display:block !important;top:auto !important;bottom:auto !important;left:auto !important}.node-panel.chat-room .messages{grid-area:messages;position:static !important;width:auto !important;height:auto !important;overflow-y:auto;padding:10px;left:auto !important;right:auto !important;top:auto !important;bottom:auto !important;margin:0 !important}.node-panel.chat-room .rightbar{grid-area:rightbar;position:static !important;width:auto !important;border-left:1px solid #ccc;overflow-y:auto;display:block !important}.node-panel.chat-room .chatMessage{grid-area:input;position:static !important;width:auto !important;height:auto !important;border-top:1px solid #eee;left:auto !important;right:auto !important;bottom:auto !important;flex:0 0 auto;padding:10px !important;background:#fff;z-index:10}}@media(max-width: 899px){.node-panel.chat-room{display:flex !important;flex-direction:column !important;height:100% !important;position:relative !important}.node-panel.chat-room .lobbyName{flex:0 0 auto}.node-panel.chat-room .messages{flex:1 !important;overflow-y:auto !important;position:relative !important;top:0 !important;bottom:0 !important;left:0 !important;right:0 !important;width:100% !important;height:auto !important;margin:0 !important}.node-panel.chat-room .chatMessage{flex:0 0 auto !important;position:relative !important;bottom:0 !important;left:0 !important;right:0 !important;width:100% !important;height:auto !important;z-index:100}.node-panel.chat-room .rightbar,.node-panel.chat-room .lobbies{display:none !important;position:fixed !important;top:60px !important;bottom:0 !important;width:80% !important;background:#fff !important;z-index:200 !important;box-shadow:2px 0 10px rgba(0,0,0,.2) !important}.node-panel.chat-room.show-lobbies .lobbies{display:block !important;left:0 !important}.node-panel.chat-room.show-users .rightbar{display:block !important;right:0 !important}.chat-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.4);z-index:150}.show-lobbies .chat-overlay,.show-users .chat-overlay{display:block}.mobile-menu-icons{display:flex;gap:15px;font-size:1.2rem}.mobile-menu-icons i{cursor:pointer;padding:5px}}@media(min-width: 900px){.mobile-menu-icons{display:none}}.chat-hub-container{display:flex;height:100%;width:100%;overflow:hidden;background-color:#f1f5f9}.chat-hub-left-pane{width:320px;min-width:300px;max-width:350px;border-right:1px solid #cbd5e1;display:flex;flex-direction:column;background:#fff;box-shadow:2px 0 5px rgba(0,0,0,.05)}.chat-own-profile-card{padding:.85rem 1.25rem !important;border-bottom:1px solid #e2e8f0 !important;background:linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%) !important;display:flex !important;align-items:center !important;justify-content:space-between !important;gap:.75rem !important;position:relative}.chat-own-profile-card .profile-header{display:flex !important;align-items:center !important;gap:.75rem !important;flex:1 !important;min-width:0 !important}.chat-own-profile-card .chat-create-lobby-btn{display:flex !important;align-items:center !important;gap:.35rem !important;padding:.4rem .85rem !important;font-size:.85rem !important;font-weight:600 !important;border-radius:.375rem !important;cursor:pointer !important;flex-shrink:0 !important;white-space:nowrap !important}.chat-own-profile-card .profile-info{display:flex;flex-direction:column;flex:1;overflow:hidden}.chat-own-profile-card .profile-info .profile-name{font-weight:700;color:#1e293b;font-size:1.1rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-own-profile-card .profile-info .profile-status{font-size:.85rem;color:#10b981;font-weight:500;display:flex;align-items:center;gap:.35rem}.chat-own-profile-card .profile-info .profile-status::before{content:"";display:inline-block;width:8px;height:8px;background-color:#10b981;border-radius:50%}.chat-rooms-list-container{flex:1;display:flex;flex-direction:column;overflow:hidden}.chat-rooms-list-container .searchbar-container{padding:.75rem 1rem;border-bottom:1px solid #e2e8f0}.chat-rooms-list-container .searchbar-container input.searchbar{width:100%;padding:.5rem .75rem;font-size:.9rem;border:1px solid #cbd5e1;border-radius:.375rem;background-color:#f8fafc;outline:none;transition:all .2s}.chat-rooms-list-container .searchbar-container input.searchbar:focus{background-color:#fff;border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.chat-rooms-list-container .rooms-scroll{flex:1;overflow-y:auto;padding:.5rem 0}.rooms-section-title{display:flex;align-items:center;gap:.5rem;padding:.75rem 1rem .375rem;font-size:.75rem;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.05em}.rooms-section-title i{font-size:.7rem;color:#94a3b8}.chat-room-list-item{display:flex;align-items:center;gap:.75rem;padding:.75rem 1rem;margin:.125rem .5rem;border-radius:.5rem;cursor:pointer;transition:all .2s}.chat-room-list-item:hover{background-color:#f1f5f9}.chat-room-list-item.selected{background-color:#e0f2fe}.chat-room-list-item.selected .room-name{color:#0369a1;font-weight:600}.chat-room-list-item .room-icon{flex-shrink:0;width:36px;height:36px;border-radius:.5rem;background:linear-gradient(135deg, #3ba4d7, #0ea5e9);display:flex;align-items:center;justify-content:center;color:#fff;font-size:1.35rem}.chat-room-list-item.public-room .room-icon{background:linear-gradient(135deg, #10b981, #059669)}.chat-room-list-item .room-meta{flex:1;min-width:0}.chat-room-list-item .room-meta .room-name{font-size:.95rem;color:#334155;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:color .2s}.chat-room-list-item .room-meta .room-topic{font-size:.8rem;color:#94a3b8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-room-list-item .room-badge{flex-shrink:0;min-width:24px;height:24px;border-radius:12px;background-color:#e2e8f0;color:#475569;font-size:.75rem;font-weight:700;display:flex;align-items:center;justify-content:center;padding:0 .375rem}.chat-hub-right-pane{flex:1;display:flex;flex-direction:column;overflow:hidden;background-color:#f8fafc}.chat-pane-placeholder{flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;color:#94a3b8;gap:1rem;padding:2rem;text-align:center}.chat-pane-placeholder i{font-size:4rem;color:#cbd5e1}.chat-pane-placeholder p{font-size:1.1rem;max-width:400px}.chat-hub-tab-content{flex:1;overflow-y:auto;padding:1.5rem}.chat-room-detail-view{display:flex;flex-direction:column;gap:1.5rem}.chat-room-detail-view .detail-header{display:flex;align-items:flex-start;gap:1.5rem;padding-bottom:1.5rem;border-bottom:1px solid #e2e8f0;flex-wrap:wrap}.chat-room-detail-view .detail-header .detail-title{flex:1;min-width:200px}.chat-room-detail-view .detail-header .detail-title h2{font-size:1.75rem;font-weight:800;color:#1e293b;margin-bottom:.25rem}.chat-room-detail-view .detail-header .detail-title .detail-subtitle{font-size:.9rem;color:#64748b;display:flex;align-items:center;gap:.5rem}.chat-room-detail-view .detail-header .detail-actions{display:flex;gap:.75rem;flex-wrap:wrap}.chat-room-detail-view .detail-header .detail-actions button{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.9rem}.chat-room-detail-view .detail-section{background-color:#fff;border-radius:.5rem;border:1px solid #e2e8f0;padding:1.25rem;box-shadow:0 1px 3px rgba(0,0,0,.02)}.chat-room-detail-view .detail-section h3{font-size:1.1rem;font-weight:700;color:#334155;margin-bottom:1rem;padding-bottom:.5rem;border-bottom:1px solid #f1f5f9}.chat-room-detail-view .detail-section .info-grid{display:grid;grid-template-columns:130px 1fr;row-gap:.75rem;font-size:.9rem}.chat-room-detail-view .detail-section .info-grid .info-label{font-weight:600;color:#64748b}.chat-room-detail-view .detail-section .info-grid .info-value{color:#1e293b;word-break:break-all}.participants-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(180px, 1fr));gap:.5rem}.participant-card{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;background-color:#f8fafc;border:1px solid #e2e8f0;border-radius:.375rem}.participant-card .participant-name{font-size:.875rem;color:#334155;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.no-participants{color:#94a3b8;font-size:.9rem;font-style:italic}.detail-actions-footer{display:flex;gap:.75rem}.detail-actions-footer button{display:flex;align-items:center;gap:.5rem;padding:.5rem 1rem;font-size:.9rem}.join-description{color:#64748b;font-size:.9rem;margin-bottom:1rem}.identities-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(200px, 1fr));gap:.75rem}.identity-card{display:flex;align-items:center;justify-content:space-between;padding:.75rem 1rem;background-color:#f8fafc;border:1px solid #e2e8f0;border-radius:.5rem;cursor:pointer;transition:all .2s}.identity-card:hover{background-color:#e0f2fe;border-color:#3ba4d7}.identity-card .identity-name{font-size:.95rem;font-weight:600;color:#334155}.identity-card i{color:#3ba4d7;font-size:.9rem}.no-rooms{padding:1rem;color:#94a3b8;text-align:center;font-style:italic}@media(max-width: 899px){.chat-hub-container{flex-direction:column}.chat-hub-left-pane{width:100%;min-width:0;max-width:none;max-height:45%;border-right:none;border-bottom:1px solid #cbd5e1}.chat-hub-right-pane{flex:1;min-height:0}}.chat-hub-header-bar{padding:.75rem 1.5rem;background-color:#fff;border-bottom:1px solid #e2e8f0;display:flex;align-items:center;justify-content:space-between;height:65px;flex-shrink:0}.chat-hub-header-bar .chat-header-info{display:flex;flex-direction:column;overflow:hidden}.chat-hub-header-bar .chat-header-info .chat-header-name{font-size:1.15rem;font-weight:800;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.chat-hub-header-bar .chat-header-info .chat-header-topic{font-size:.85rem;color:#64748b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:.125rem}.chat-hub-header-bar .chat-header-actions{display:flex;gap:.5rem}.chat-hub-header-bar .chat-header-actions button{display:flex;align-items:center;gap:.35rem;padding:.375rem .75rem;font-size:.85rem}@media(max-width: 700px){.chat-hub-header-bar{height:auto;min-height:48px;padding:.45rem .55rem;gap:.45rem}.chat-hub-header-bar .chat-header-info{min-width:0;flex:1}.chat-hub-header-bar .chat-header-info .chat-header-name{font-size:.95rem}.chat-hub-header-bar .chat-header-actions{flex:0 0 auto;gap:.25rem}.chat-hub-header-bar .chat-header-actions button{width:32px;min-width:32px;height:32px;padding:0;justify-content:center;font-size:0}.chat-hub-header-bar .chat-header-actions button i{margin:0;font-size:.95rem}}.chat-hub-tabs-container{background-color:#fff;border-bottom:1px solid #cbd5e1;padding:.5rem 1.5rem 0}.chat-hub-tabs{display:flex;gap:.5rem}.chat-hub-tabs .tab-btn{padding:.625rem 1.25rem;font-size:.95rem;font-weight:600;color:#64748b;background:rgba(0,0,0,0);border:none;border-radius:.375rem .375rem 0 0;border-bottom:3px solid rgba(0,0,0,0);cursor:pointer;box-shadow:none;transition:all .2s;display:flex;align-items:center;gap:.5rem}.chat-hub-tabs .tab-btn:hover{color:#334155;background-color:#f1f5f9}.chat-hub-tabs .tab-btn.active{color:#3ba4d7;border-bottom-color:#3ba4d7;background-color:rgba(0,0,0,0)}.chat-hub-tab-content{flex:1;display:flex;flex-direction:column;overflow:hidden;background-color:#f8fafc}.chat-hub-conversation-layout{display:flex;flex-direction:row;height:100%;width:100%;overflow:hidden}.chat-hub-conversation-main{display:flex;flex-direction:column;flex:1;height:100%;overflow:hidden}.chat-hub-rightbar{width:200px;border-left:1px solid #cbd5e1;background-color:#fff;display:flex;flex-direction:column;flex-shrink:0;position:relative}.chat-hub-rightbar .rightbar-title{padding:.75rem 1rem;font-size:.85rem;font-weight:700;color:#64748b;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid #e2e8f0}.chat-hub-rightbar .rightbar-users-list{flex:1;overflow-y:auto;padding:.5rem}.chat-hub-rightbar .user{padding:.5rem .75rem;font-size:.9rem;color:#334155;border-radius:.375rem;transition:all .2s;display:flex;align-items:center;gap:.5rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;position:relative}.chat-hub-rightbar .user:hover{background-color:#f1f5f9;color:#0f172a}.chat-hub-rightbar .user .defaultAvatar{width:2rem;height:2rem;font-size:.9rem;flex-shrink:0}.chat-hub-rightbar .user img.avatar{width:2rem;height:2rem;flex-shrink:0}@media(max-width: 899px){.chat-hub-rightbar{display:none}}.chat-hub-messages{flex:1;overflow-y:auto;padding:1.25rem 1.5rem;display:flex;flex-direction:column;gap:1rem}.chat-hub-messages .message{display:flex;flex-direction:column;max-width:70%;padding:.625rem .875rem;border-radius:.75rem;font-size:.925rem;line-height:1.4;word-break:break-word;box-shadow:0 1px 2px rgba(0,0,0,.05)}.chat-hub-messages .message.incoming{align-self:flex-start;align-items:flex-start;background-color:#fff;color:#1e293b;border:1px solid #e2e8f0;border-bottom-left-radius:.125rem}.chat-hub-messages .message.outgoing{align-self:flex-end;align-items:flex-end;background-color:#3ba4d7;color:#fff;border-bottom-right-radius:.125rem}.chat-hub-messages .message .username{font-size:.75rem;margin-bottom:.25rem;padding:0 .125rem;font-weight:700}.chat-hub-messages .message.incoming .username{color:#0369a1}.chat-hub-messages .message.outgoing .username{color:#e0f2fe}.chat-hub-messages .message .messagetext{white-space:break-spaces;margin:0}.chat-hub-messages .message .datetime{font-size:.7rem;margin-top:.25rem;padding:0 .125rem;opacity:.8}.chat-hub-messages .message.incoming .datetime{color:#64748b}.chat-hub-messages .message.outgoing .datetime{color:#f1f5f9}.chat-hub-input-area{padding:.75rem 1.5rem;background-color:#fff;border-top:1px solid #cbd5e1;display:flex;gap:.75rem;align-items:flex-end;flex-shrink:0}.chat-hub-input-area textarea.chat-hub-textarea{flex:1;resize:vertical;min-height:40px;max-height:250px;height:40px;padding:.5rem .75rem;border:1px solid #cbd5e1;border-radius:.375rem;font-size:.9rem;outline:none;transition:border-color .2s,box-shadow .2s;background-color:#f8fafc}.chat-hub-input-area textarea.chat-hub-textarea:focus{background-color:#fff;border-color:#3ba4d7;box-shadow:0 0 0 3px rgba(59,164,215,.15)}.chat-hub-input-area button.chat-hub-send-btn{padding:.5rem 1.25rem;font-size:.9rem;height:40px;display:flex;align-items:center;gap:.5rem;border-radius:.375rem}button.chat-hub-action-btn,label.chat-hub-action-btn,.chat-hub-action-btn{display:inline-flex !important;align-items:center !important;justify-content:center !important;width:36px !important;height:36px !important;min-width:36px !important;padding:0 !important;margin:0 !important;background:rgba(0,0,0,0) !important;border:none !important;box-shadow:none !important;color:#64748b !important;font-size:1.15rem !important;border-radius:.375rem !important;cursor:pointer !important;transition:all .15s ease !important;outline:none !important}button.chat-hub-action-btn:hover,label.chat-hub-action-btn:hover,.chat-hub-action-btn:hover{background-color:#e2e8f0 !important;color:#3b82f6 !important;box-shadow:none !important}button.chat-hub-action-btn i,label.chat-hub-action-btn i,.chat-hub-action-btn i{font-size:1.15rem !important;color:inherit !important}.chat-hub-messages.compact-container,.messages.compact-container{gap:0 !important;padding:.75rem 1rem !important;background-color:#fff !important;display:flex !important;flex-direction:column !important}.chat-hub-messages.compact-container .message.compact,.messages.compact-container .message.compact{display:block !important;max-width:100% !important;padding:.1rem 0 !important;border-radius:0 !important;background-color:rgba(0,0,0,0) !important;border:none !important;box-shadow:none !important;align-self:flex-start !important;font-size:.875rem !important;line-height:1.45 !important;margin:0 !important;white-space:nowrap !important}.chat-hub-messages.compact-container .message.compact:hover,.messages.compact-container .message.compact:hover{background-color:#f8fafc !important;overflow:visible !important;white-space:normal !important}.chat-hub-messages.compact-container .message.compact .datetime,.messages.compact-container .message.compact .datetime{color:#a0a0a0 !important;margin-right:.4rem !important;font-size:.78rem !important;font-family:monospace !important;opacity:1 !important;display:inline !important}.chat-hub-messages.compact-container .message.compact .username,.messages.compact-container .message.compact .username{font-weight:bold !important;margin-right:.2rem !important;font-size:.875rem !important;display:inline !important}.chat-hub-messages.compact-container .message.compact .messagetext,.messages.compact-container .message.compact .messagetext{color:#1e293b !important;white-space:normal !important;word-break:break-word !important;display:inline !important;margin:0 !important}.chat-create-lobby-btn{position:absolute;bottom:.5rem;right:1.25rem;background-color:#0084ff;color:#fff;border:none;border-radius:.375rem;padding:.35rem .75rem;font-size:.85rem;font-weight:600;cursor:pointer;box-shadow:0 4px 6px -1px rgba(0,132,255,.2),0 2px 4px -1px rgba(0,132,255,.1);transition:background-color .2s,transform .2s;display:flex;align-items:center;gap:.25rem}.chat-create-lobby-btn:hover{background-color:#0073e6;transform:translateY(-1px)}.chat-create-lobby-btn:active{transform:translateY(0)}.chat-hub-rightbar .user .user-name{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}.chat-hub-rightbar .rightbar-context-menu{position:absolute;right:1rem;width:210px;background-color:#fff;border:1px solid #e2e8f0;box-shadow:0 4px 10px rgba(0,0,0,.15);border-radius:.375rem;z-index:1010;padding:.25rem 0;display:flex;flex-direction:column}.chat-hub-rightbar .rightbar-context-menu .menu-item{padding:.5rem 1rem;font-size:.85rem;color:#334155;cursor:pointer;display:flex;align-items:center;transition:background-color .2s}.chat-hub-rightbar .rightbar-context-menu .menu-item:hover{background-color:#f1f5f9;color:#0f172a}.chat-emoji{font-size:1.45em;line-height:1;vertical-align:-0.15em;display:inline-block}.chat-hub-attach-btn,.chat-hub-action-btn{background-color:rgba(0,0,0,0) !important;border:none !important;font-size:1.15rem !important;color:#64748b !important;cursor:pointer !important;padding:.4rem .5rem !important;border-radius:.375rem !important;flex-shrink:0 !important;display:inline-flex !important;align-items:center !important;justify-content:center !important;transition:all .2s !important;box-shadow:none !important;margin:0 !important;line-height:1 !important;height:36px !important;width:36px !important}.chat-hub-attach-btn:hover,.chat-hub-action-btn:hover{background-color:#f1f5f9 !important;color:#3b82f6 !important;transform:none !important}.attach-modal-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:rgba(15,23,42,.4);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:2000}.attach-modal{background-color:#fff;border-radius:.5rem;width:450px;max-width:90%;padding:1.5rem;box-shadow:0 10px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);display:flex;flex-direction:column;gap:1rem}.attach-modal .attach-modal-header{display:flex;align-items:center;gap:.6rem;margin-bottom:.25rem}.attach-modal .attach-modal-icon{font-size:1.2rem;color:#3b82f6}.attach-modal h4{margin:0;font-size:1.2rem;color:#0f172a}.attach-modal p{margin:0;font-size:.9rem;color:#475569}.attach-modal .attach-path-row{display:flex;gap:.5rem;align-items:center}.attach-modal .attach-path-row input[type=text]{flex:1;padding:.75rem;border:1px solid #cbd5e1;border-radius:.375rem;font-size:.9rem;outline:none;transition:border-color .2s;min-width:0}.attach-modal .attach-path-row input[type=text]:focus{border-color:#3b82f6;box-shadow:0 0 0 3px rgba(59,130,246,.15)}.attach-browse-btn{flex-shrink:0;display:flex;align-items:center;gap:.35rem;padding:.625rem .9rem;font-size:.875rem;background-color:#f1f5f9;color:#334155;border:1px solid #cbd5e1;border-radius:.375rem;cursor:pointer;box-shadow:none;transition:background-color .2s,border-color .2s;white-space:nowrap}.attach-browse-btn:hover{background-color:#e2e8f0;border-color:#94a3b8}.attach-path-hint{display:flex;align-items:flex-start;gap:.5rem;padding:.6rem .75rem;background-color:#fffbeb;border:1px solid #fcd34d;border-left:3px solid #f59e0b;border-radius:.375rem;font-size:.825rem;color:#92400e;line-height:1.45}.attach-path-hint i{color:#f59e0b;margin-top:.1rem;flex-shrink:0}.attach-path-hint code{font-family:monospace;background-color:rgba(245,158,11,.15);padding:.05rem .25rem;border-radius:.2rem}.attach-modal .hashing-spinner{display:flex;align-items:center;gap:.5rem;font-size:.9rem;color:#3b82f6}.attach-modal .error-text{color:#ef4444;font-size:.85rem;margin:0}.attach-modal .modal-buttons{display:flex;justify-content:flex-end;gap:.75rem;margin-top:.5rem}.attach-modal .modal-buttons button{padding:.5rem 1rem;font-size:.9rem;border-radius:.25rem;border:none;cursor:pointer;transition:opacity .2s}.attach-modal .modal-buttons button:hover{opacity:.9}.chat-hub-emoji-btn{background-color:rgba(0,0,0,0);border:none;font-size:1.3rem;cursor:pointer;padding:.35rem .4rem;margin-right:.25rem;flex-shrink:0;display:flex;align-items:center;justify-content:center;border-radius:.375rem;line-height:1;transition:background-color .15s,transform .15s;box-shadow:none}.chat-hub-emoji-btn:hover{background-color:#f1f5f9;transform:scale(1.1)}.emoji-picker-wrapper{position:relative;flex-shrink:0;display:flex;align-items:center}.emoji-picker{position:absolute;bottom:calc(100% + .5rem);left:0;width:320px;background-color:#fff;border:1px solid #e2e8f0;border-radius:.625rem;box-shadow:0 8px 30px -4px rgba(0,0,0,.18),0 4px 12px -2px rgba(0,0,0,.1);z-index:3000;display:flex;flex-direction:column;overflow:hidden;animation:emoji-pop .15s ease-out}.emoji-search-row{display:flex;align-items:center;gap:.4rem;padding:.6rem .75rem .4rem;border-bottom:1px solid #f1f5f9}.emoji-search-icon{color:#94a3b8;font-size:.8rem;flex-shrink:0}.emoji-search-input{flex:1;border:1px solid #e2e8f0;border-radius:.375rem;padding:.3rem .5rem;font-size:.85rem;outline:none;background-color:#f8fafc;transition:border-color .15s}.emoji-search-input:focus{border-color:#3ba4d7;background-color:#fff}.emoji-search-clear{background:none;border:none;cursor:pointer;color:#94a3b8;padding:.2rem;font-size:.8rem;box-shadow:none;display:flex;align-items:center}.emoji-search-clear:hover{color:#475569}.emoji-categories{display:flex;gap:.1rem;padding:.35rem .5rem;border-bottom:1px solid #f1f5f9;overflow-x:auto;scrollbar-width:none}.emoji-categories::-webkit-scrollbar{display:none}.emoji-cat-btn{background:none;border:none;cursor:pointer;font-size:1.2rem;padding:.3rem .35rem;border-radius:.375rem;line-height:1;box-shadow:none;transition:background-color .1s;flex-shrink:0}.emoji-cat-btn:hover{background-color:#f1f5f9}.emoji-cat-btn.active{background-color:#e0f2fe;box-shadow:inset 0 -2px 0 #3ba4d7}.emoji-grid{display:grid;grid-template-columns:repeat(7, 1fr);gap:0;padding:.4rem .35rem;max-height:220px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:#cbd5e1 rgba(0,0,0,0)}.emoji-grid::-webkit-scrollbar{width:4px}.emoji-grid::-webkit-scrollbar-track{background:rgba(0,0,0,0)}.emoji-grid::-webkit-scrollbar-thumb{background-color:#cbd5e1;border-radius:4px}.emoji-btn{background:none;border:none;cursor:pointer;font-size:1.7rem;padding:.25rem;border-radius:.3rem;line-height:1;box-shadow:none;text-align:center;transition:background-color .1s,transform .1s;display:flex;align-items:center;justify-content:center;aspect-ratio:1}.emoji-btn:hover{background-color:#f1f5f9;transform:scale(1.2)}@keyframes emoji-pop{from{opacity:0;transform:scale(0.92) translateY(6px)}to{opacity:1;transform:scale(1) translateY(0)}}.chat-hub-messages.compact-container .message.compact,.messages.compact-container .message.compact{display:block !important;max-width:100% !important;padding:.1rem 0 !important;border-radius:0 !important;background-color:rgba(0,0,0,0) !important;border:none !important;box-shadow:none !important;align-self:flex-start !important;font-size:.875rem !important;line-height:1.45 !important;margin:0 !important;white-space:nowrap !important}.chat-hub-messages.compact-container .message.compact:hover,.messages.compact-container .message.compact:hover{background-color:#f8fafc !important;overflow:visible !important;white-space:normal !important}.chat-hub-messages.compact-container .message.compact .datetime,.messages.compact-container .message.compact .datetime{color:#a0a0a0 !important;margin-right:.4rem !important;font-size:.78rem !important;font-family:monospace !important;opacity:1 !important;display:inline !important}.chat-hub-messages.compact-container .message.compact .username,.messages.compact-container .message.compact .username{font-weight:bold !important;margin-right:.2rem !important;font-size:.875rem !important;display:inline !important}.chat-hub-messages.compact-container .message.compact .messagetext,.messages.compact-container .message.compact .messagetext{color:#1e293b !important;white-space:normal !important;word-break:break-word !important;display:inline !important;margin:0 !important}.user-tooltip{position:fixed !important;width:280px !important;background-color:#ffffe1 !important;border:1px solid #7f7f7f !important;box-shadow:2px 2px 6px rgba(0,0,0,.25) !important;padding:.5rem !important;border-radius:.25rem !important;z-index:10000 !important;white-space:normal !important;display:flex !important;gap:.5rem !important;align-items:flex-start !important;color:#000 !important;font-size:.8rem !important;text-align:left !important;pointer-events:none !important}.user-tooltip .tooltip-avatar{flex-shrink:0 !important}.user-tooltip .tooltip-avatar .jdenticon-avatar,.user-tooltip .tooltip-avatar .defaultAvatar,.user-tooltip .tooltip-avatar img.avatar{width:56px !important;height:56px !important;min-width:56px !important;min-height:56px !important;border-radius:2px !important;border:1px solid #999 !important;box-shadow:none !important;object-fit:cover !important}.user-tooltip .tooltip-details{display:flex !important;flex-direction:column !important;gap:.2rem !important;min-width:0 !important;flex:1 !important}.user-tooltip .tooltip-details .tooltip-row{line-height:1.2 !important;display:flex !important;flex-direction:row !important;align-items:baseline !important;gap:.35rem !important;white-space:normal !important;word-break:break-all !important}.user-tooltip .tooltip-details .tooltip-row .tooltip-label{font-weight:bold !important;color:#000 !important;font-size:.8rem !important;flex-shrink:0 !important}.user-tooltip .tooltip-details .tooltip-row .tooltip-value{font-weight:normal !important;color:#000 !important;font-size:.8rem !important;overflow:hidden !important;text-overflow:ellipsis !important}.user-tooltip .tooltip-details .tooltip-row .tooltip-value.tooltip-id{font-family:monospace !important;font-size:.75rem !important;color:#00b !important}.attach-modal-overlay{position:fixed;top:0;left:0;width:100vw;height:100vh;background-color:rgba(15,23,42,.5);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:9999}.attach-modal{background:#fff;border-radius:.5rem;width:480px;max-width:92vw;box-shadow:0 20px 25px -5px rgba(0,0,0,.15),0 8px 10px -6px rgba(0,0,0,.1);padding:1.5rem;display:flex;flex-direction:column;color:#1e293b;box-sizing:border-box}.attach-modal h4{margin:0 0 1rem 0;font-size:1.15rem;font-weight:700;color:#0f172a}.attach-modal .attach-modal-header{display:flex;align-items:center;gap:.5rem;margin-bottom:1rem}.attach-modal .attach-modal-header h4{margin:0}.attach-modal .attach-modal-header .attach-modal-icon{font-size:1.25rem;color:#3b82f6}.emoji-picker-wrapper{position:relative;display:inline-flex}.emoji-picker{position:absolute;bottom:48px;left:0;z-index:9999;width:320px;background:#fff;border:1px solid #cbd5e1;border-radius:12px;box-shadow:0 10px 25px -5px rgba(0,0,0,.15),0 8px 10px -6px rgba(0,0,0,.1);padding:.75rem;display:flex;flex-direction:column;gap:.5rem;font-family:inherit}.emoji-picker .emoji-search-row{position:relative;display:flex;align-items:center}.emoji-picker .emoji-search-row .emoji-search-icon{position:absolute;left:.6rem;color:#94a3b8;font-size:.85rem;pointer-events:none}.emoji-picker .emoji-search-row input.emoji-search-input{width:100%;padding:.4rem 1.8rem .4rem 1.8rem;font-size:.85rem;border:1px solid #cbd5e1;border-radius:6px;outline:none;background-color:#f8fafc;transition:border-color .15s ease}.emoji-picker .emoji-search-row input.emoji-search-input:focus{border-color:#3b82f6;background-color:#fff}.emoji-picker .emoji-search-row .emoji-search-clear{position:absolute;right:.5rem;background:rgba(0,0,0,0);border:none;color:#94a3b8;cursor:pointer;padding:.2rem;font-size:.85rem}.emoji-picker .emoji-search-row .emoji-search-clear:hover{color:#ef4444}.emoji-picker .emoji-categories{display:flex;justify-content:space-between;padding-bottom:.4rem;border-bottom:1px solid #f1f5f9;margin-bottom:.25rem}.emoji-picker .emoji-categories .emoji-cat-btn{background:rgba(0,0,0,0) !important;border:none !important;font-size:1.1rem !important;padding:.25rem .35rem !important;border-radius:6px !important;cursor:pointer !important;transition:background-color .15s ease,transform .1s ease !important;line-height:1 !important;width:auto !important;height:auto !important;min-width:unset !important}.emoji-picker .emoji-categories .emoji-cat-btn:hover{background-color:#f1f5f9 !important;transform:scale(1.15)}.emoji-picker .emoji-categories .emoji-cat-btn.active{background-color:#e0f2fe !important;border-radius:6px !important}.emoji-picker .emoji-grid{display:grid !important;grid-template-columns:repeat(8, 1fr) !important;gap:2px !important;max-height:220px !important;overflow-y:auto !important;padding-right:2px !important}.emoji-picker .emoji-grid::-webkit-scrollbar{width:5px}.emoji-picker .emoji-grid::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:3px}.emoji-picker .emoji-grid .emoji-btn{background:rgba(0,0,0,0) !important;border:none !important;font-size:1.25rem !important;padding:.35rem 0 !important;cursor:pointer !important;border-radius:6px !important;display:flex !important;align-items:center !important;justify-content:center !important;width:auto !important;height:auto !important;min-width:unset !important;transition:background-color .1s ease,transform .1s ease !important}.emoji-picker .emoji-grid .emoji-btn:hover{background-color:#e2e8f0 !important;transform:scale(1.2)}.rightbar-context-menu,.chat-msg-context-menu{z-index:9999 !important;background-color:#fff !important;border:1px solid #cbd5e1 !important;border-radius:8px !important;box-shadow:0 10px 25px -5px rgba(0,0,0,.15),0 8px 10px -6px rgba(0,0,0,.1) !important;padding:.35rem 0 !important;font-family:inherit !important;overflow:hidden !important}.rightbar-context-menu .menu-item,.rightbar-context-menu .context-menu-item,.chat-msg-context-menu .menu-item,.chat-msg-context-menu .context-menu-item{display:flex !important;align-items:center !important;padding:.55rem .85rem !important;font-size:.875rem !important;font-weight:500 !important;color:#1e293b !important;cursor:pointer !important;transition:background-color .15s ease,color .15s ease !important;user-select:none !important}.rightbar-context-menu .menu-item:hover,.rightbar-context-menu .context-menu-item:hover,.chat-msg-context-menu .menu-item:hover,.chat-msg-context-menu .context-menu-item:hover{background-color:#f1f5f9 !important;color:#0284c7 !important}.rightbar-context-menu .menu-item i,.rightbar-context-menu .context-menu-item i,.chat-msg-context-menu .menu-item i,.chat-msg-context-menu .context-menu-item i{font-size:.95rem !important;width:1.25rem !important;text-align:center !important}.rightbar-context-menu{position:absolute !important;right:10px !important;min-width:220px !important}.chat-msg-context-menu{position:fixed !important;right:auto !important;width:max-content !important;min-width:180px !important;max-width:calc(100vw - 16px) !important;box-sizing:border-box !important}.chat-msg-context-menu .context-menu-item{white-space:nowrap !important}.side-bar{display:flex;flex-direction:column;background:#fff}.side-bar .mail-compose-btn{width:96%;margin:.25rem;padding:.75rem 0}.side-bar .sidebar a,.side-bar .sidebarquickview a{display:flex;align-items:center;gap:.5rem}.side-bar .sidebar-badge{margin-left:auto;background-color:#3b82f6;color:#fff;font-size:.725rem;font-weight:700;padding:.15rem .45rem;border-radius:999px;line-height:1;display:inline-flex;align-items:center;justify-content:center;min-width:1.2rem}.compose-mail__from{display:flex;justify-content:flex-start;align-items:center;gap:.5rem;padding-bottom:.5rem;border-bottom:2px solid #eef3f6}.compose-mail__recipients{padding:.5rem 0;display:flex;flex-direction:column;gap:.5rem;border-bottom:2px solid #eef3f6}.compose-mail__recipients__container{display:flex;gap:.5rem}.compose-mail__recipients__container>label{text-transform:capitalize}.compose-mail__recipients__container .recipients{width:100%;display:flex;gap:.5rem;flex-wrap:wrap}.compose-mail__recipients__container .recipients__selected{padding:.125rem .5rem;display:flex;align-items:center;gap:.5rem;border:1px solid #eef3f6;border-radius:3px;cursor:default}.compose-mail__recipients__container .recipients__selected i{cursor:pointer;padding:.25rem}.compose-mail__recipients__container .recipients__input{display:flex;position:relative;flex-grow:1}.compose-mail__recipients__container .recipients__input-field{flex-grow:1;min-width:200px;padding:0;border:none;box-shadow:none}.compose-mail__recipients__container .recipients__input-field:focus+.recipients__input-list{display:flex}.compose-mail__recipients__container .recipients__input-list{z-index:1;position:absolute;top:1rem;padding:0;width:100%;max-height:15rem;flex-direction:column;overflow:auto;display:none;background:#fff;border-top:1px solid #eef3f6;border-bottom:1px solid #eef3f6}.compose-mail__recipients__container .recipients__input-list:hover{display:flex}.compose-mail__recipients__container .recipients__input-list li{list-style:none;padding:.25rem .5rem;cursor:pointer;background:#fff;border:1px solid #eef3f6;border-top:0px}.compose-mail__recipients__container .recipients__input-list li:hover{background:#eef3f6}.compose-mail__recipients__container .recipients__input-list li:last-child{border-bottom:0px}.compose-mail__recipients .remove-recipient{padding:.125rem .5rem}.compose-mail input[type=text].compose-mail__subject{padding:.5rem 0;border:none;box-shadow:none;border-bottom:2px solid #eef3f6;border-radius:0}.compose-mail__message{margin:.5rem 0;height:100%;display:flex;flex-direction:column;overflow:auto}.compose-mail__message-body{height:100%;outline:rgba(0,0,0,0)}.compose-mail__send-btn{display:flex;align-items:center;gap:.5rem}.compose-mail__send-btn i{transform:translateY(-1px)}.msg-view{height:100%;display:flex;flex-direction:column;gap:1rem;overflow:auto}.msg-view-nav{display:flex;justify-content:space-between;align-items:column}.msg-view-nav__action{display:flex;gap:.5rem}.msg-view__header{display:flex;flex-direction:column;gap:1rem}.msg-view__header>h3{line-height:1}.msg-view__header .msg-details{display:flex;gap:1rem}.msg-view__header .msg-details__avatar{height:max-content}.msg-view__header .msg-details__info{display:flex;flex-direction:column}.msg-view__header .msg-details__info-item{display:flex;gap:.5rem}.msg-view__body{height:100%;overflow:auto;font-size:14px !important}.msg-view__attachment{height:50%;overflow:auto;display:flex;flex-direction:column}.msg-view__attachment-items{height:100%;overflow:auto}.mail-tag{width:8rem;padding:.5rem}.msgHeader{display:flex}.msgHeaderDetails{display:flex;flex-direction:column}table.mails th:nth-child(1){width:5%;color:#fcba03}table.mails th:nth-child(2){width:5%;color:hsl(202.5,30.7692307692%,44.9019607843%)}table.mails th:nth-child(3){width:50%;text-align:start}table.mails th:nth-child(4),table.mails th:nth-child(5){width:20%;text-align:start}table.mails td:nth-child(3){text-align:start;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}table.mails td:nth-child(4),table.mails td:nth-child(5){text-align:start}table.mails tr:hover{background-color:#eef3f6;cursor:pointer}table.mails tr.unread{color:#000;background-color:#eef3f6}table.mails>tr:hover{cursor:auto;background-color:#fff}table.mails th.sortable-th{cursor:pointer;user-select:none;transition:background-color .2s,color .2s}table.mails th.sortable-th:hover{background-color:#eef3f6;color:hsl(202.5,30.7692307692%,14.9019607843%)}input.star-check{display:none}input.star-check+label.star-check{color:gray}input.star-check:checked+label.star-check{color:#fcba03}#truncate{height:6rem;overflow:auto}#truncate.truncated-view{height:1.75rem;overflow:hidden}.toggle-truncate{font-size:.75rem;padding:0 .25rem;background:#999;color:#14141b;box-shadow:none;border-radius:2px}table.attachment-container{padding:0}table.attachment-container>tr{border:0}table.attachment-container .attachment-header{width:100%;display:flex;justify-content:space-between}table.attachment-container .attachment-header th{text-align:start}table.attachment-container .attachment-header th:nth-child(1){flex-basis:45%}table.attachment-container .attachment-header th:nth-child(2){flex-basis:15%}table.attachment-container .attachment-header th:nth-child(3){flex-basis:10%}table.attachment-container .attachment-header th:nth-child(4){flex-basis:20%}table.attachment-container .attachment-header th:nth-child(5){text-align:center;flex-basis:10%}table.attachment-container .attachment{width:100%;display:flex;justify-content:space-between;text-align:start}table.attachment-container .attachment__name{flex-basis:45%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}table.attachment-container .attachment__name span{margin-left:8px}table.attachment-container .attachment__from{flex-basis:15%}table.attachment-container .attachment__size{flex-basis:10%}table.attachment-container .attachment__date{flex-basis:20%}table.attachment-container .attachment td:nth-child(5){display:flex;justify-content:center;align-items:center;flex-basis:10%}table.attachment-container .attachment td:nth-child(5) button{font-size:.875rem}.view-toggle{height:max-content;border:1px solid #019dff;border-radius:4px;display:flex}.view-toggle *{padding:4px 12px;border-radius:4px}.composePopupOverlay{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1;background-color:rgba(0,0,0,.2)}.composePopupOverlay .composePopup{position:absolute;inset:0;margin:auto;width:80%;height:90%}.composePopupOverlay .composePopup>.widget{padding:2rem}.composePopupOverlay .composePopup .close-btn{position:absolute;top:1.5rem;right:1.5rem}.attachments-wrapper{display:flex;flex-direction:column;gap:.5rem;margin-top:.5rem}.attachment-card{background:#fff;border:1px solid #e2e8f0;border-radius:.5rem;padding:.75rem 1rem;display:flex;align-items:center;gap:.75rem;box-shadow:0 1px 3px rgba(0,0,0,.02)}.attachment-card .attachment-icon{width:40px;height:40px;border-radius:.5rem;background:#eff6ff;color:#3b82f6;display:flex;align-items:center;justify-content:center;font-size:1.2rem;flex-shrink:0}.attachment-card .attachment-info{flex:1;min-width:0}.attachment-card .attachment-info .attachment-name{font-weight:600;font-size:.9rem;color:#1e293b;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.attachment-card .attachment-info .attachment-size{font-size:.75rem;color:#64748b}.attachment-card .btn-attachment-download{padding:.4rem .75rem;font-size:.8rem;height:34px}.mobile-fab-compose{display:none}@media(max-width: 768px){.side-bar{width:100% !important;flex-direction:row !important;overflow-x:auto !important;overflow-y:hidden !important;white-space:nowrap !important;border-bottom:1px solid #cbd5e1 !important;padding:.5rem !important;flex-shrink:0 !important;background:#fff !important}.side-bar .mail-compose-btn{display:none !important}.side-bar .sidebar{width:auto !important;flex-direction:row !important;gap:.25rem !important}.side-bar .sidebar a{padding:.5rem .75rem !important;font-size:.85rem !important;border-radius:.375rem !important;border-left:none !important;border-bottom:none !important;display:inline-flex !important;align-items:center !important;gap:.35rem !important}.side-bar .sidebar a .sidebar-link-text{display:none !important}.side-bar .sidebar a i{margin-right:0 !important;font-size:1.1rem !important}.side-bar .sidebar a .sidebar-badge{margin-left:0 !important;background:#3b82f6 !important;color:#fff !important;font-size:.7rem !important;font-weight:700 !important;padding:.15rem .4rem !important;border-radius:999px !important;line-height:1 !important}.side-bar .sidebar a.selected-sidebar-link{background-color:#e0f2fe !important;color:#0369a1 !important;border-left:none !important;font-weight:600 !important}.side-bar .sidebarquickview{display:none !important}select.mail-tag{padding:.45rem .75rem !important;border-radius:.5rem !important;border:1px solid #cbd5e1 !important;font-size:.85rem !important;font-weight:600 !important;background-color:#fff !important;color:#1e293b !important;outline:none !important;cursor:pointer !important;box-shadow:0 1px 2px rgba(0,0,0,.05) !important}.node-panel{width:100% !important;flex:1 !important;padding:.5rem !important;overflow:auto !important}.table-pagination-container table.mails{display:block !important;width:100% !important}.table-pagination-container table.mails tr:first-child{display:none !important}.table-pagination-container table.mails tbody{display:flex !important;flex-direction:column !important;gap:.5rem !important}.table-pagination-container table.mails tr.msgbody{display:flex !important;flex-direction:column !important;padding:.85rem !important;background:#fff !important;border:1px solid #e2e8f0 !important;border-radius:.65rem !important;box-shadow:0 1px 3px rgba(0,0,0,.03) !important;position:relative !important;margin-bottom:.25rem !important;transition:all .2s ease !important}.table-pagination-container table.mails tr.msgbody.unread{background:#f0f9ff !important;border-color:#bae6fd !important;border-left:4px solid #0284c7 !important}.table-pagination-container table.mails tr.msgbody td{display:block !important;padding:0 !important;border:none !important}.table-pagination-container table.mails tr.msgbody td.cell-from{order:1 !important;margin-bottom:.35rem !important;padding-right:5rem !important}.table-pagination-container table.mails tr.msgbody td.cell-from div{display:flex !important;align-items:center !important;gap:.5rem !important;width:100% !important}.table-pagination-container table.mails tr.msgbody td.cell-from div .jdenticon-avatar,.table-pagination-container table.mails tr.msgbody td.cell-from div .defaultAvatar,.table-pagination-container table.mails tr.msgbody td.cell-from div img.avatar{flex-shrink:0 !important;width:28px !important;height:28px !important;min-width:28px !important;min-height:28px !important;aspect-ratio:1/1 !important;object-fit:cover !important;border-radius:50% !important}.table-pagination-container table.mails tr.msgbody td.cell-from div span{font-weight:700 !important;font-size:.95rem !important;color:#1e293b !important;white-space:nowrap !important;overflow:hidden !important;text-overflow:ellipsis !important}.table-pagination-container table.mails tr.msgbody td.cell-date{order:2 !important;position:absolute !important;top:.85rem !important;right:.85rem !important;font-size:.75rem !important;font-weight:600 !important;color:#64748b !important;white-space:nowrap !important}.table-pagination-container table.mails tr.msgbody td.cell-subject{order:3 !important;margin-bottom:.25rem !important;font-size:.9rem !important;color:#334155 !important}.table-pagination-container table.mails tr.msgbody td.cell-subject span{font-weight:600 !important}.table-pagination-container table.mails tr.msgbody td.cell-star{order:4 !important;position:absolute !important;bottom:.75rem !important;right:.85rem !important}.table-pagination-container table.mails tr.msgbody td.cell-attachment{order:5 !important;position:absolute !important;bottom:.75rem !important;right:2.5rem !important;color:#64748b !important}.msg-view{padding:.5rem !important;gap:.75rem !important}.msg-view .msg-view-nav{background:#fff !important;padding:.5rem .75rem !important;border-radius:.5rem !important;border:1px solid #e2e8f0 !important}.msg-view .msg-view-nav__action button{padding:.45rem .65rem !important;min-width:38px !important;height:38px !important;justify-content:center !important}.msg-view .msg-view-nav__action button .btn-text{display:none !important}.msg-view .msg-view-nav__action button i{margin:0 !important;font-size:1rem !important}.msg-view__header{background:#fff !important;padding:1rem !important;border-radius:.5rem !important;border:1px solid #e2e8f0 !important}.msg-view__header h3{font-size:1.25rem !important;font-weight:800 !important;color:#0f172a !important;line-height:1.3 !important}.msg-view__body{background:#fff !important;padding:1rem !important;border-radius:.5rem !important;border:1px solid #e2e8f0 !important;font-size:.95rem !important;line-height:1.6 !important;color:#1e293b !important}.msg-view__attachment{height:auto !important}.mobile-fab-compose{position:fixed !important;bottom:1.75rem !important;right:1.5rem !important;width:54px !important;height:54px !important;border-radius:50% !important;background:linear-gradient(135deg, #3b82f6, #2563eb) !important;color:#fff !important;display:flex !important;align-items:center !important;justify-content:center !important;font-size:1.35rem !important;box-shadow:0 4px 14px rgba(37,99,235,.45) !important;border:none !important;z-index:1000 !important;cursor:pointer !important;transition:transform .2s ease !important}.mobile-fab-compose:active{transform:scale(0.92) !important}.composePopupOverlay .composePopup{width:95% !important;height:95% !important}}.mail-mobile-nav-toggle{display:none}.mail-nav-drawer{display:contents}.mail-mobile-box-title{display:none}@media(max-width: 768px){.side-bar{height:44px !important;padding:.25rem !important;overflow:visible !important}.mail-mobile-nav-toggle{display:inline-flex !important;width:40px;height:40px;align-items:center;justify-content:center;border:0;border-radius:6px;background:#fff;color:#0f172a;cursor:pointer;font-size:1.15rem}.mail-nav-drawer{position:fixed;top:0;left:0;z-index:1100;display:flex !important;flex-direction:column;width:min(82vw,300px);height:100dvh;padding:1rem 0;overflow-y:auto;background:#fff;border-right:1px solid #e2e8f0;box-shadow:8px 0 24px rgba(15,23,42,.16);transform:translateX(-105%);transition:transform 180ms ease}.mail-nav-drawer--open{transform:translateX(0)}.mail-mobile-box-title{display:flex !important;align-items:center;gap:.55rem;margin:.35rem 0 .55rem;color:#0f172a;font-size:1.45rem;font-weight:600}.mail-mobile-box-title i{color:#3b82f6;font-size:1.35rem}.mail-box-content>.widget__heading{display:none}.mail-nav-drawer .mail-compose-btn{display:flex !important;margin:0 .85rem .75rem !important}.mail-nav-drawer .sidebar,.mail-nav-drawer .sidebarquickview{display:flex !important;flex-direction:column !important;width:100% !important}.mail-nav-drawer .sidebarquickview{margin-top:.5rem;border-top:1px solid #e2e8f0;padding-top:.5rem}.mail-nav-drawer .sidebarquickview h6{display:block !important;margin:0 .9rem .35rem;color:#64748b}.mail-nav-drawer .sidebar a,.mail-nav-drawer .sidebarquickview a{display:flex !important;width:auto;padding:.7rem 1rem !important;border-radius:0 !important}.mail-nav-drawer .sidebar a .sidebar-link-text{display:inline !important}.mail-nav-drawer .sidebar a i{margin-right:.75rem !important;font-size:1.1rem !important}}.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:1.5rem;padding-left:.25rem;padding-right:0}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;padding-left:.25rem}table.friendsfiles td:nth-child(1){width:1.5rem;padding-left:.25rem;padding-right:0}.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}.file-search-container{align-items:stretch;min-height:16rem;padding:1rem;background:#fff;box-shadow:0 1px 3px rgba(15,23,42,.06)}.file-search-container__keywords{flex:0 0 13rem;padding:0 1rem 0 0}.file-search-container__keywords .keywords-header{display:flex;align-items:center;justify-content:space-between;gap:.75rem}.file-search-container__keywords .keywords-header h5{margin:0;font-size:1rem}.file-search-container__keywords .clear-btn{padding:.35rem .7rem}.file-search-container__keywords .keywords-container a{padding:.45rem .55rem;border-radius:.35rem;font-size:.95rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.file-search-container__keywords .keywords-container a:hover,.file-search-container__keywords .keywords-container a.selected{background:rgba(0,154,235,.1);color:#019dff}.file-search-container__results{flex:1 1 auto;min-width:0;overflow:visible}.file-search-container__results>h5{margin:0;color:#64748b}.results-container{width:100%;border:1px solid #dbe3ec;border-radius:.5rem;overflow:hidden}.results-row{display:grid;grid-template-columns:minmax(12rem, 2fr) minmax(5.5rem, 0.6fr) minmax(12rem, 1.6fr) auto;gap:1rem;align-items:center}.results-header{background:#f1f5f9;border-bottom:1px solid #dbe3ec;color:#475569;font-size:.78rem;font-weight:700;letter-spacing:.02em;text-transform:uppercase}.results-header .results-row{padding:.65rem .85rem}.results-list .file-item{padding:.75rem .85rem;border-bottom:1px solid #edf2f7;transition:background-color .15s ease}.results-list .file-item:last-child{border-bottom:0}.results-list .file-item:hover{background:#f8fafc}.results-cell{min-width:0}.results-cell.name-col{display:flex;align-items:center;gap:.55rem;color:#0f172a;font-weight:600}.results-cell.name-col i{color:#0284c7;font-size:1.1rem}.results-cell.name-col span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.results-cell.size-col{color:#475569;white-space:nowrap}.results-cell.hash-col{overflow:hidden;color:#64748b;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:.78rem;text-overflow:ellipsis;white-space:nowrap}.download-btn-v65{padding:.45rem .75rem;white-space:nowrap}.shareManagerPopupOverlay{position:fixed;width:100%;height:100%;top:0;left:0;z-index:1;background-color:rgba(0,0,0,.2)}.shareManagerPopupOverlay .shareManagerPopup{position:absolute;inset:0;margin:auto;width:80%;height:90%}.shareManagerPopupOverlay .shareManagerPopup>.widget{padding:1.5rem}.shareManagerPopupOverlay .shareManagerPopup .close-btn{position:absolute;top:1.5rem;right:1.5rem}.share-manager{display:flex;flex-direction:column;justify-content:space-between}.share-manager__table{margin:1rem 0 auto}.share-manager__table thead{font-weight:bold;text-align:left}.share-manager__table thead td:nth-child(1),.share-manager__table thead td:nth-child(2){padding-left:.5rem}.share-manager__table thead td:nth-child(3) .tooltip,.share-manager__table thead td:nth-child(4) .tooltip{font-weight:normal;font-size:1rem}.share-manager__table tbody{text-align:left}.share-manager__table tbody td:nth-child(4){font-size:1rem}.share-manager__table td input{border:0 !important}.share-manager__table td input[type=text]{width:100%}.share-manager__table td:nth-child(1){width:45%}.share-manager__table td:nth-child(2){width:20%}.share-manager__table td:nth-child(3){width:10%}.share-manager__table td:nth-child(4){width:25%}.share-manager__actions{display:flex;justify-content:space-between}.share-manager__form{display:flex;flex-direction:column;gap:.5rem}.share-manager__form_input{display:flex;flex-direction:column;gap:.5rem}.share-manager__form_input input{flex-grow:1}.share-manager .share-flags input.share-flags-check{display:none}.share-manager .share-flags input.share-flags-check+label.share-flags-label{color:gray;margin-right:.25rem;padding:.25rem .25rem .125rem;border:1px solid #6d6d6d;border-radius:.5rem}.share-manager .share-flags input.share-flags-check:checked+label.share-flags-label{color:#118fcc}.share-manager label span{display:inline-block;width:1.125rem}.manage-visibility label{width:100%;cursor:pointer}.manage-visibility{display:flex;justify-content:space-between}@media(max-width: 700px){.file-view__body-details{flex-direction:column;align-items:flex-start;gap:1rem}.file-view__body-details-stat{grid-template-columns:1fr;gap:.5rem}.file-view__body-details-stat span{display:flex;align-items:center}.share-manager__table,.share-manager__table thead,.share-manager__table tbody,.share-manager__table tr,.share-manager__table td{display:block;width:100% !important}.share-manager__table thead{display:none}.share-manager__table tr{border:1px solid #ccc;border-radius:8px;margin-bottom:1rem;padding:.5rem;background:#fff}.share-manager__table td{margin-bottom:.5rem;border:none !important;padding-left:0 !important}table.myfiles,table.myfiles tr,table.myfiles td,table.friendsfiles,table.friendsfiles tr,table.friendsfiles td{display:block;width:100% !important}table.myfiles th,table.friendsfiles th{display:none}table.myfiles tr,table.friendsfiles tr{border:1px solid #ccc;border-radius:8px;margin-bottom:1rem;padding:.5rem;background:#fff}table.friendsfiles tr{display:grid;grid-template-columns:1.25rem minmax(0, 1fr) auto;align-items:center;column-gap:.35rem;margin-bottom:.5rem;padding:.65rem .5rem}table.friendsfiles td{display:block;width:auto !important;margin:0;padding:0 !important;border:0 !important}table.friendsfiles td:nth-child(1){grid-column:1}table.friendsfiles td:nth-child(2){grid-column:2;left:0 !important;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}table.friendsfiles td:nth-child(3){grid-column:3;white-space:nowrap}table.friendsfiles td:nth-child(4){grid-column:2/-1;margin-top:.4rem}table.myfiles tr{display:grid;grid-template-columns:1.25rem minmax(0, 1fr) auto;align-items:center;column-gap:.35rem;margin-bottom:.5rem;padding:.65rem .5rem}table.myfiles td{display:block;width:auto !important;margin:0;padding:0 !important;border:0 !important}table.myfiles td:nth-child(1){grid-column:1}table.myfiles td:nth-child(2){grid-column:2;left:0 !important;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}table.myfiles td:nth-child(3){grid-column:3;white-space:nowrap}.my-files__configure-shares{width:2.4rem;min-width:2.4rem;padding:.55rem !important}.my-files__configure-shares span{display:none}.file-search-container{flex-direction:column}.file-search-container__keywords{flex-basis:auto;width:100%;border-right:none;border-bottom:1px solid rgba(20,20,27,.1);padding-bottom:1rem;margin-bottom:1rem}.results-container,.results-container thead,.results-container tbody,.results-container tr,.results-container td{display:block;width:100% !important}.results-container thead{display:none}.results-container tr{border-bottom:1px solid #eee;padding:1rem 0}.results-container td{margin-bottom:.5rem;word-break:break-all}.search-form{width:auto;flex:1;max-width:18rem}.file-search-container{gap:.75rem;padding:.75rem}.file-search-container__keywords{padding:0 0 .75rem;margin:0}.file-search-container__results{width:100%}.results-header{display:none}.results-list .file-item{display:grid;grid-template-columns:minmax(0, 1fr) auto;grid-template-areas:"name action" "size hash";gap:.45rem .75rem;padding:.8rem}.results-cell.name-col{grid-area:name}.results-cell.size-col{grid-area:size;font-size:.82rem}.results-cell.hash-col{grid-area:hash;max-width:10rem;text-align:right}.results-cell.action-col{grid-area:action}.download-btn-v65{padding:.4rem .6rem}}.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}.posts-container-card .channel-post__placeholder{flex:1 1 auto;min-height:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.35rem;color:#64748b;background:linear-gradient(135deg, #f8fafc, #dbe5f1)}.channel-post__placeholder i{font-size:1.35rem;color:#64748b}.channel-post__placeholder span{font-size:2rem;font-weight:700;color:#2563eb}.channel-post__placeholder small{font-size:.72rem;font-weight:600}.post-description{margin:1rem 0;padding:.85rem 1rem;border-radius:10px;background:#f1f5f9;color:#1e293b}.post-description__text{overflow-wrap:anywhere;line-height:1.5}.post-description__text>:first-child{margin-top:0}.post-description__text>:last-child{margin-bottom:0}.post-description__text--collapsed{display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical;-webkit-line-clamp:3}.post-description__toggle{margin-top:.35rem;padding:0 !important;border:0 !important;box-shadow:none !important;background:rgba(0,0,0,0) !important;color:#0f172a !important;font-size:.85rem !important;font-weight:700 !important}.post-description__toggle:hover{color:#2563eb !important;text-decoration:underline}.posts-container{align-content:start;grid-auto-rows:240px}.posts-container-card{height:240px;min-height:0;overflow:hidden}.posts-container-card>img,.posts-container-card>.channel-post__placeholder{min-height:0}@media(max-width: 700px){.posts-container{grid-auto-rows:210px;gap:1rem}.posts-container-card{height:210px}}@media(max-width: 700px){.file-section{margin-top:1.25rem}table.channel-files,table.channel-files tbody,table.channel-files tr,table.channel-files td{display:block;width:100% !important;box-sizing:border-box}table.channel-files{padding:0;table-layout:auto}table.channel-files thead{display:none}table.channel-files tr{margin:0 0 .7rem;padding:.7rem;border:1px solid #dbe3ef;border-radius:8px;background:#fff}table.channel-files td{min-width:0;padding:.25rem 0;border:0;text-align:left}table.channel-files td::before{display:block;margin-bottom:.1rem;color:#64748b;content:attr(data-label);font-size:.72rem;font-weight:700;text-transform:uppercase}table.channel-files .channel-file__name{overflow-wrap:anywhere;color:#0f172a;font-weight:600;line-height:1.35}table.channel-files .channel-file__size{color:#475569}table.channel-files .channel-file__action{padding-top:.5rem}table.channel-files .channel-file__action>button{min-width:116px}table.channel-files .channel-file__action .file-view{margin-top:.6rem}}#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}#popupmessage{position:fixed !important;top:0 !important;left:0 !important;right:0 !important;bottom:0 !important;width:100vw !important;height:100vh !important;background-color:rgba(15,23,42,.75) !important;backdrop-filter:blur(4px) !important;z-index:999999 !important;display:none;align-items:center !important;justify-content:center !important;box-sizing:border-box !important}.popup{position:fixed !important;top:50% !important;left:50% !important;transform:translate(-50%, -50%) !important;z-index:1000000 !important;max-width:90vw !important;max-height:90vh !important;display:flex !important;flex-direction:column !important;box-sizing:border-box !important}.popup-content{position:relative !important;background:#fff !important;border-radius:12px !important;padding:1rem !important;box-shadow:0 20px 25px -5px rgba(0,0,0,.4),0 10px 10px -5px rgba(0,0,0,.2) !important;max-width:90vw !important;max-height:90vh !important;overflow:auto !important;box-sizing:border-box !important}.popup-content span.close{position:absolute !important;top:.5rem !important;right:.75rem !important;font-size:1.75rem !important;font-weight:700 !important;color:#64748b !important;cursor:pointer !important;line-height:1 !important;z-index:10 !important;transition:color .15s ease !important}.popup-content span.close:hover{color:#ef4444 !important}.board-view-container{display:flex !important;flex-direction:column !important;gap:1rem !important;width:100% !important;max-width:100% !important;overflow-x:hidden !important;padding:.5rem 0 !important;box-sizing:border-box !important}.board-table{width:100% !important;border-collapse:collapse !important}.board-table th,.board-table td{text-align:left !important;padding:.65rem .85rem !important}.board-toolbar{display:flex !important;flex-direction:row !important;align-items:center !important;justify-content:space-between !important;gap:1rem !important;width:100% !important;padding:.5rem .85rem !important;background-color:#fff !important;border:1px solid #cbd5e1 !important;border-radius:8px !important;box-shadow:0 1px 3px rgba(0,0,0,.05) !important;box-sizing:border-box !important}.board-toolbar__left{display:flex !important;flex-direction:row !important;align-items:center !important;gap:.75rem !important;flex:1 !important;min-width:0 !important}.board-toolbar__right{display:flex !important;flex-direction:row !important;align-items:center !important;gap:.65rem !important;flex-shrink:0 !important}.board-toolbar__count-badge{display:inline-flex !important;align-items:center !important;gap:.4rem !important;padding:.3rem .65rem !important;background-color:#f1f5f9 !important;color:#475569 !important;font-size:.825rem !important;font-weight:600 !important;border-radius:16px !important;border:1px solid #e2e8f0 !important;white-space:nowrap !important}.board-toolbar__count-badge i{color:#007bff !important}.board-toolbar__search{position:relative !important;display:flex !important;align-items:center !important;flex:1 !important;max-width:380px !important;min-width:160px !important}.board-toolbar__search-icon{position:absolute !important;left:.75rem !important;color:#94a3b8 !important;font-size:.85rem !important;pointer-events:none !important}.board-toolbar__search-input{width:100% !important;padding:.35rem .65rem .35rem 2.1rem !important;border:1px solid #cbd5e1 !important;border-radius:6px !important;font-size:.85rem !important;background-color:#fff !important;color:#1e293b !important}.board-toolbar__search-input:focus{outline:none !important;border-color:#007bff !important;box-shadow:0 0 0 3px rgba(0,123,255,.15) !important}.board-toolbar__voter{display:inline-flex !important;align-items:center !important;gap:.4rem !important;color:#475569 !important;font-size:.8rem !important;font-weight:600 !important;white-space:nowrap !important}.board-toolbar__voter select{max-width:180px !important;min-width:110px !important;padding:.3rem .45rem !important;border:1px solid #cbd5e1 !important;border-radius:6px !important;background:#fff !important;color:#1e293b !important;font-size:.8rem !important}.board-toolbar__voter select:focus{outline:none !important;border-color:#007bff !important;box-shadow:0 0 0 3px rgba(0,123,255,.15) !important}.board-toolbar__view-toggle{display:inline-flex !important;flex-direction:row !important;align-items:center !important;background-color:#f1f5f9 !important;padding:2px !important;border-radius:6px !important;border:1px solid #cbd5e1 !important}.board-toolbar__toggle-btn{display:inline-flex !important;align-items:center !important;justify-content:center !important;gap:.35rem !important;padding:.3rem .65rem !important;border:none !important;background:rgba(0,0,0,0) !important;color:#64748b !important;font-size:.825rem !important;font-weight:500 !important;border-radius:4px !important;cursor:pointer !important;white-space:nowrap !important;transition:all .2s ease !important}.board-toolbar__toggle-btn i{font-size:.85rem !important}.board-toolbar__toggle-btn:hover{color:#1e293b !important;background-color:hsla(0,0%,100%,.6) !important}.board-toolbar__toggle-btn--active{background-color:#007bff !important;color:#fff !important;font-weight:600 !important;box-shadow:0 1px 2px rgba(0,0,0,.1) !important}.board-toolbar__toggle-btn--active i{color:#fff !important}@media(max-width: 768px){.board-toolbar{flex-wrap:wrap !important}.board-toolbar__left{flex-basis:100% !important}.board-toolbar__toggle-btn span{display:none !important}.board-toolbar__toggle-btn{padding:.35rem .55rem !important}}.board-comments{margin-top:1.5rem;max-width:900px;color:#0f172a}.board-comments__heading{display:flex;align-items:center;gap:1rem;margin-bottom:1.25rem}.board-comments__heading h3{margin:0;font-size:1.15rem}.board-comments__heading span{color:#64748b;font-size:.8rem;font-weight:600}.board-comments__heading i{margin-right:.35rem}.board-comments__voter{display:inline-flex;align-items:center;gap:.4rem;margin-left:auto;color:#64748b;font-size:.75rem;font-weight:600;white-space:nowrap}.board-comments__voter select{max-width:180px;padding:.25rem .4rem;font-size:.78rem}.board-post-voting{display:flex;align-items:center;justify-content:space-between;gap:1rem;margin:.85rem 0;padding:.65rem .75rem;background:#f8fafc;border:1px solid #e2e8f0;border-radius:.5rem}.board-post-voting__identity{display:flex;align-items:center;gap:.5rem;color:#64748b;font-size:.8rem;font-weight:600}.board-post-voting__identity select{max-width:220px;padding:.3rem .45rem;font-size:.8rem}.board-post-voting__buttons{display:flex;align-items:center;gap:.35rem}.board-post-voting__buttons button{min-width:52px;padding:.3rem .55rem;box-shadow:none}.board-post-voting__score{min-width:2rem;color:#334155;font-weight:700;text-align:center}.board-comment-composer,.board-comment{display:flex;gap:.8rem}.board-comment-avatar{display:flex;flex:0 0 38px;width:38px;height:38px;align-items:center;justify-content:center;border-radius:50%;background:linear-gradient(135deg, #2563eb, #7c3aed);color:#fff;font-size:.78rem;font-weight:700}.board-comment-composer__body,.board-comment__content{min-width:0;flex:1}.board-comment-composer__identity{max-width:240px;margin-bottom:.45rem;font-size:.8rem}.board-comment-composer__input{width:100%;min-height:36px;padding:.45rem 0;resize:vertical;border:0;border-bottom:1px solid #94a3b8;border-radius:0;background:rgba(0,0,0,0);color:#0f172a;font:inherit;line-height:1.4;box-sizing:border-box}.board-comment-composer__input:focus{outline:0;border-bottom:2px solid #2563eb}.board-comment-composer__input:disabled{cursor:not-allowed;opacity:.6}.board-comment-composer__actions,.board-comment__actions{display:flex;align-items:center;gap:.45rem;margin-top:.55rem}.board-comment-composer__actions{justify-content:flex-end}.board-comment-composer__actions button,.board-comment__actions button,.board-comment__like,.board-comment-composer__replying button{border:0;background:rgba(0,0,0,0);color:#475569;cursor:pointer;font-size:.78rem;font-weight:700}.board-comment-composer__submit{padding:.45rem .85rem !important;border-radius:999px !important;background:#2563eb !important;color:#fff !important}.board-comment-composer__submit:disabled{background:#dbe3ef !important;color:#94a3b8 !important;cursor:not-allowed}.board-comment-composer__cancel:hover,.board-comment__actions button:hover{color:#2563eb}.board-comment-composer__replying{display:flex;align-items:center;gap:.25rem;margin-bottom:.35rem;color:#64748b;font-size:.8rem}.board-comment-composer__replying button{margin-left:.3rem}.board-comment-composer__hint,.board-comment-composer__error{margin:.4rem 0 0;font-size:.78rem}.board-comment-composer__hint{color:#64748b}.board-comment-composer__error{color:#dc2626}.board-comments__list{margin-top:1.8rem}.board-comment{margin-top:1.35rem}.board-comment__header{display:flex;align-items:center;justify-content:space-between;min-height:18px}.board-comment__meta{display:flex;align-items:baseline;gap:.55rem;font-size:.8rem}.board-comment__meta b{color:#1e293b}.board-comment__meta span{color:#64748b;font-size:.75rem}.board-comment__menu{width:28px;height:28px;padding:0;border:0;border-radius:50%;background:rgba(0,0,0,0);color:#0f172a;cursor:pointer;opacity:0}.board-comment:hover .board-comment__menu,.board-comment__menu:focus{opacity:1}.board-comment__menu:hover{background:#f1f5f9}.board-comment__text{margin:.2rem 0 0;white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.45}.board-comment__actions{gap:.65rem;margin-top:.35rem}.board-comment__actions button{padding:.25rem .2rem;color:#0f172a}.board-comment__like{padding:.25rem .35rem;color:#94a3b8}.board-comment__actions i{margin-right:.2rem}.board-comment__replies-toggle{position:relative;margin-top:.3rem;padding:.25rem .35rem;border:0;background:rgba(0,0,0,0);color:#2563eb;cursor:pointer;font-size:.78rem;font-weight:700}.board-comment__replies-toggle:hover{background:#eff6ff;border-radius:4px}.board-comment__replies-toggle i{margin-left:.15rem}.board-comment__replies-toggle::before{content:"";position:absolute;left:-3.15rem;bottom:.8rem;width:1.5rem;height:2.2rem;border-left:1px solid #e2e8f0;border-bottom:1px solid #e2e8f0;border-radius:0 0 0 .75rem;pointer-events:none}.board-comment__replies{margin-top:.2rem;padding-left:1rem;border-left:2px solid #e2e8f0}.board-comments__status,.board-comments__empty{margin:2rem 0;color:#64748b;text-align:center}.board-comments__empty i{font-size:1.5rem}@media(max-width: 560px){.board-comments__heading{flex-wrap:wrap;justify-content:space-between;gap:.5rem}.board-comments__voter{width:100%;margin-left:0}.board-comments__voter select{flex:1;max-width:none}.board-post-voting{align-items:stretch;flex-direction:column}.board-post-voting__identity select{flex:1;min-width:0;max-width:none}.board-post-voting__buttons{justify-content:center}.board-comment-composer,.board-comment{gap:.6rem}.board-comment-avatar{flex-basis:32px;width:32px;height:32px;font-size:.68rem}.board-comment__replies{padding-left:.6rem}.board-comment__replies-toggle::before{left:-2.65rem;width:1.2rem}.board-card__notes-btn span{display:none}.board-card__notes-btn{width:30px;height:30px;justify-content:center;padding:0 !important}.board-card__notes-btn i{margin:0 !important}}.board-pagination{display:inline-flex !important;flex-direction:row !important;align-items:center !important;gap:.25rem !important;background-color:#fff !important;padding:2px 4px !important;border-radius:6px !important;border:1px solid #cbd5e1 !important}.board-pagination__btn{display:inline-flex !important;align-items:center !important;justify-content:center !important;width:26px !important;height:26px !important;min-width:26px !important;min-height:26px !important;padding:0 !important;margin:0 !important;border:1px solid #cbd5e1 !important;border-radius:4px !important;background:#fff !important;color:#007bff !important;font-size:.825rem !important;cursor:pointer !important;transition:all .15s ease !important}.board-pagination__btn i{font-size:.825rem !important;color:#007bff !important}.board-pagination__btn:hover:not(:disabled){background-color:#007bff !important;color:#fff !important;border-color:#007bff !important}.board-pagination__btn:hover:not(:disabled) i{color:#fff !important}.board-pagination__btn:disabled{opacity:.4 !important;cursor:not-allowed !important;color:#94a3b8 !important;border-color:#e2e8f0 !important;background-color:#f1f5f9 !important}.board-pagination__btn:disabled i{color:#94a3b8 !important}.board-pagination__label{font-size:.825rem !important;font-weight:700 !important;color:#334155 !important;padding:0 .35rem !important;white-space:nowrap !important;user-select:none !important}.board-view-footer{display:flex !important;justify-content:center !important;align-items:center !important;padding:1rem 0 .5rem 0 !important;width:100% !important}.board-grid{display:flex !important;flex-direction:column !important;width:100% !important;box-sizing:border-box !important}.board-grid--compact{display:flex !important;flex-direction:column !important;gap:.5rem !important;width:100% !important}.board-grid--card{display:grid !important;grid-template-columns:repeat(auto-fill, minmax(280px, 1fr)) !important;gap:1.25rem !important;width:100% !important}.board-grid__empty{display:flex !important;flex-direction:column !important;align-items:center !important;justify-content:center !important;padding:4rem 2rem !important;text-align:center !important;background:#fff !important;border:2px dashed #cbd5e1 !important;border-radius:12px !important;width:100% !important;box-sizing:border-box !important}.board-grid__empty-icon{font-size:3rem !important;color:#cbd5e1 !important;margin-bottom:1rem !important}.board-grid__empty-title{font-size:1.15rem !important;font-weight:600 !important;color:#475569 !important;margin:0 0 .5rem 0 !important}.board-grid__empty-desc{font-size:.9rem !important;color:#94a3b8 !important;margin:0 !important}.board-card{box-sizing:border-box !important}.board-card--compact{display:flex !important;flex-direction:row !important;align-items:center !important;width:100% !important;min-height:70px !important;padding:.45rem .75rem !important;background-color:#eef2f5 !important;border:1px solid #d1d5db !important;border-radius:4px !important;gap:.75rem !important;box-sizing:border-box !important;margin-bottom:.35rem !important}.board-card--compact:hover{background-color:#e2e8f0 !important;border-color:#9ca3af !important}.board-card__vote-pill{display:inline-flex !important;align-items:center !important;gap:.4rem !important;padding:.2rem .55rem !important;background-color:#f1f5f9 !important;border:1px solid #cbd5e1 !important;border-radius:20px !important;box-sizing:border-box !important}button.board-card__vote-btn,.board-card__vote-pill .board-card__vote-btn{background:rgba(0,0,0,0) !important;border:none !important;box-shadow:none !important;padding:.1rem .2rem !important;margin:0 !important;cursor:pointer !important;display:flex !important;align-items:center !important;justify-content:center !important;line-height:1 !important;border-radius:4px !important;transition:background-color .15s ease !important;outline:none !important}.board-card__vote-pill .board-card__vote-btn:hover{background-color:#e2e8f0 !important;box-shadow:none !important}.board-card__vote-pill .board-card__vote-btn--up i{color:#16a34a !important;font-size:1.15rem !important}.board-card__vote-pill .board-card__vote-btn--down i{color:#dc2626 !important;font-size:1.15rem !important}.board-card__vote-pill .board-card__vote-score{font-size:.9rem !important;font-weight:700 !important;color:#1e293b !important;padding:0 .15rem !important;line-height:1 !important;min-width:1rem !important;text-align:center !important}.board-card--compact .board-card__image-container{width:110px !important;height:62px !important;flex-shrink:0 !important;border-radius:4px !important;overflow:hidden !important;background-color:#cbd5e1 !important}.board-card--compact .board-card__image{width:100% !important;height:100% !important;object-fit:cover !important;display:block !important}.board-card--compact .board-card__placeholder-wrapper{width:100% !important;height:100% !important;display:flex !important;align-items:center !important;justify-content:center !important;background:linear-gradient(135deg, #e2e8f0 0%, #cbd5e1 100%) !important}.board-card__placeholder-content{display:flex;flex-direction:column;align-items:center;gap:.3rem;color:#64748b;font-size:.72rem;font-weight:600}.board-card__placeholder-content i{font-size:1.35rem}.board-card--compact .board-card__placeholder-img{width:24px !important;height:24px !important;color:#64748b !important}.board-card--compact .board-card__content{display:flex !important;flex-direction:column !important;justify-content:center !important;flex:1 !important;min-width:0 !important;padding:0 !important}.board-card--compact .board-card__title{font-size:1.05rem !important;font-weight:700 !important;color:#25a !important;text-decoration:underline !important;font-style:italic !important;white-space:nowrap !important;overflow:hidden !important;text-overflow:ellipsis !important;margin:0 0 .15rem 0 !important;cursor:pointer !important}.board-card--compact .board-card__title:hover{color:#1d4ed8 !important}.board-card--compact .board-card__meta{font-size:.8rem !important;color:#475569 !important;margin-bottom:.2rem !important}.board-card--compact .board-card__meta b{color:#1e293b !important}.board-card--compact .board-card__footer{display:flex !important;align-items:center !important;gap:.5rem !important;padding:0 !important;border:none !important;margin:0 !important}button.board-card__comments-btn,.board-card__comments-btn,.board-card--compact .board-card__comments-btn{display:inline-flex !important;align-items:center !important;gap:.35rem !important;padding:.2rem .5rem !important;background:rgba(0,0,0,0) !important;border:none !important;box-shadow:none !important;color:#64748b !important;font-size:.85rem !important;font-weight:500 !important;cursor:pointer !important;outline:none !important}button.board-card__comments-btn:hover,.board-card__comments-btn:hover,.board-card--compact .board-card__comments-btn:hover{color:#007bff !important;text-decoration:underline !important;box-shadow:none !important}button.board-card__notes-btn,.board-card__notes-btn{display:inline-flex !important;align-items:center !important;gap:.35rem !important;padding:.2rem .5rem !important;background:rgba(0,0,0,0) !important;border:none !important;box-shadow:none !important;color:#64748b !important;font-size:.85rem !important;font-weight:500 !important;cursor:pointer !important}.board-card__notes-btn:hover{color:#007bff !important;text-decoration:underline !important}.board-card--card{display:flex !important;flex-direction:column !important;background-color:#fff !important;border:1px solid #e2e8f0 !important;border-radius:10px !important;overflow:hidden !important;transition:transform .2s ease,box-shadow .2s ease !important;box-shadow:0 2px 4px rgba(0,0,0,.04) !important}.board-card--card:hover{transform:translateY(-3px) !important;box-shadow:0 8px 16px rgba(0,0,0,.08) !important;border-color:#cbd5e1 !important}.board-card--card .board-card__vote-col{display:none !important}.board-card--card .board-card__image-container{width:100% !important;height:170px !important;overflow:hidden !important;background-color:#f1f5f9 !important}.board-card--card .board-card__image{width:100% !important;height:100% !important;object-fit:cover !important;display:block !important}.board-card--card .board-card__placeholder-wrapper{width:100% !important;height:100% !important;display:flex !important;align-items:center !important;justify-content:center !important;background:linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%) !important}.board-card--card .board-card__placeholder-img{width:36px !important;height:36px !important;color:#94a3b8 !important}.board-card--card .board-card__content{display:flex !important;flex-direction:column !important;flex:1 !important;padding:1rem !important}.board-card--card .board-card__title{font-size:1.05rem !important;font-weight:700 !important;color:#0f172a !important;margin:0 0 .4rem 0 !important;white-space:nowrap !important;overflow:hidden !important;text-overflow:ellipsis !important;cursor:pointer !important}.board-card--card .board-card__title:hover{color:#007bff !important}.board-card--card .board-card__meta{font-size:.8rem !important;color:#64748b !important;margin-bottom:.5rem !important}.board-card--card .board-card__notes-wrapper{display:flex !important;flex-direction:column !important;gap:.35rem !important;margin-bottom:.75rem !important}.board-card--card .board-card__notes{font-size:.85rem !important;line-height:1.45 !important;color:#475569 !important;background-color:#f8fafc !important;border-left:3px solid #cbd5e1 !important;padding:.4rem .6rem !important;word-break:break-word !important}.board-card--card .board-card__notes--clamped{display:-webkit-box !important;-webkit-line-clamp:3 !important;-webkit-box-orient:vertical !important;overflow:hidden !important;white-space:pre-line !important}.board-card--card .board-card__notes--expanded{display:block !important;white-space:pre-line !important}.board-card--card .board-card__notes-toggle{align-self:flex-start !important;background:none !important;border:none !important;padding:.1rem .3rem !important;color:#007bff !important;font-size:.775rem !important;font-weight:600 !important;cursor:pointer !important}.board-card--card .board-card__notes-toggle:hover{text-decoration:underline !important}.board-card--card .board-card__footer{display:flex !important;align-items:center !important;justify-content:flex-end !important;margin-top:auto !important;padding-top:.65rem !important;border-top:1px solid #f1f5f9 !important}.board-card--card .board-card__comments-btn{display:inline-flex !important;align-items:center !important;gap:.4rem !important;padding:.3rem .6rem !important;background-color:#f1f5f9 !important;color:#475569 !important;border:1px solid #e2e8f0 !important;border-radius:6px !important;font-size:.8rem !important;font-weight:500 !important;cursor:pointer !important}.board-card--card .board-card__comments-btn:hover{background-color:#007bff !important;color:#fff !important;border-color:#007bff !important}.board-card--card .board-card__comments-btn:hover i{color:#fff !important}.board-card--card .board-card__notes-btn{display:inline-flex !important;align-items:center !important;gap:.4rem !important;padding:.3rem .6rem !important;background-color:#f8fafc !important;color:#475569 !important;border:1px solid #e2e8f0 !important;border-radius:6px !important;font-size:.8rem !important;font-weight:500 !important}.board-card--card .board-card__notes-btn:hover{background-color:#e0f2fe !important;color:#0369a1 !important;border-color:#7dd3fc !important;text-decoration:none !important}.board-notes-dialog{min-width:min(560px,75vw);max-width:75vw}.board-notes-dialog h3{margin:0 2rem .8rem 0;color:#0f172a}.board-notes-dialog__label{margin:0 0 .35rem;color:#64748b;font-size:.78rem;font-weight:700;text-transform:uppercase}.board-notes-dialog__content{margin:0;white-space:pre-wrap;overflow-wrap:anywhere;color:#1e293b;line-height:1.55}#photo-view-overlay{display:none;position:fixed;inset:0;z-index:999999;background-color:rgba(0,0,0,.85);align-items:center;justify-content:center}.photo-view-dialog{background-color:#f8fafc !important;border-radius:8px !important;box-shadow:0 20px 25px -5px rgba(0,0,0,.5) !important;display:flex !important;flex-direction:column !important;max-width:90vw !important;max-height:90vh !important;width:820px !important;overflow:hidden !important;position:relative !important;z-index:1 !important}.photo-view-header{display:flex !important;align-items:center !important;justify-content:space-between !important;padding:.75rem 1rem !important;background-color:#fff !important;border-bottom:1px solid #e2e8f0 !important}.photo-view-title{font-size:1.05rem !important;font-style:italic !important;font-weight:700 !important;color:#1e293b !important;margin:0 !important;flex:1 !important;overflow:hidden !important;text-overflow:ellipsis !important;white-space:nowrap !important}.photo-view-close-btn{background:none !important;border:none !important;font-size:1.6rem !important;line-height:1 !important;color:#64748b !important;cursor:pointer !important;padding:0 .4rem !important}.photo-view-close-btn:hover{color:#ef4444 !important}.photo-view-body{display:flex !important;flex-direction:row !important;align-items:stretch !important;background-color:#0f172a !important;flex:1 !important;min-height:380px !important;max-height:68vh !important;overflow:hidden !important}.photo-view-nav-col{width:56px !important;flex-shrink:0 !important;display:flex !important;align-items:center !important;justify-content:center !important;background-color:rgba(0,0,0,.25) !important}.photo-view-img-wrap{flex:1 !important;display:flex !important;align-items:center !important;justify-content:center !important;min-width:0 !important;padding:.75rem !important}.photo-view-img{max-width:100% !important;max-height:65vh !important;object-fit:contain !important;display:block !important;border-radius:4px !important}.photo-view-no-img{color:#94a3b8 !important;font-size:.95rem !important}.photo-view-nav-btn{width:38px !important;height:38px !important;background-color:#fff !important;border:1px solid #cbd5e1 !important;border-radius:6px !important;color:#007bff !important;display:flex !important;align-items:center !important;justify-content:center !important;cursor:pointer !important;box-shadow:0 4px 10px rgba(0,0,0,.3) !important;transition:all .15s ease !important;flex-shrink:0 !important}.photo-view-nav-btn i{font-size:1rem !important;color:#007bff !important}.photo-view-nav-btn:hover{background-color:#007bff !important;color:#fff !important;border-color:#007bff !important}.photo-view-nav-btn:hover i{color:#fff !important}.photo-view-footer{display:flex !important;align-items:center !important;justify-content:space-between !important;padding:.75rem 1rem !important;background-color:#fff !important;border-top:1px solid #e2e8f0 !important}.photo-view-meta{font-size:.875rem !important;color:#475569 !important}.photo-view-meta b{color:#0f172a !important}@media(max-width: 768px){.media-item__desc{display:none !important}.media-item__details{flex-basis:100% !important;width:100% !important}}.feedreader-page{height:100%;display:flex;flex-direction:column;color:#333}.feedreader-toolbar,.feedreader-section-title,.feedreader-article-actions{display:flex;align-items:center;gap:.6rem}.feedreader-toolbar{padding:1rem;border-bottom:1px solid #ddd}.feedreader-toolbar h2{margin-right:auto}.feedreader-columns{min-height:0;flex:1;display:grid;grid-template-columns:minmax(190px, 0.8fr) minmax(260px, 1fr) minmax(320px, 1.7fr)}.feedreader-tree,.feedreader-messages,.feedreader-reader{min-width:0;overflow:auto;border-right:1px solid #ddd}.feedreader-tree-item{display:flex;gap:.55rem;align-items:center;padding-block:.65rem;cursor:pointer}.feedreader-tree-item:hover,.feedreader-tree-item.selected{background:#e9f4fa}.feedreader-tree-item span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.feedreader-icon-button{margin-left:auto;border:0;background:rgba(0,0,0,0)}.feedreader-section-title{padding:.8rem 1rem;border-bottom:1px solid #ddd;justify-content:space-between}.feedreader-message-row{display:flex;flex-direction:column;gap:.2rem;padding:.8rem 1rem;border-bottom:1px solid #eee;cursor:pointer}.feedreader-message-row.unread{border-left:4px solid #3ba4d7}.feedreader-message-row.read{opacity:.72}.feedreader-message-row.selected,.feedreader-message-row:hover{background:#f2f8fb}.feedreader-message-row time,.feedreader-message-row span{color:#777;font-size:.85rem}.feedreader-reader{padding:1.25rem;border-right:0}.feedreader-reader img{max-width:100%;height:auto}.feedreader-article-meta{color:#777;font-size:.85rem}.feedreader-article-body{line-height:1.55;margin-top:1.5rem;overflow-wrap:anywhere}.feedreader-article-actions{margin-top:1.5rem}.feedreader-placeholder{padding:1.25rem;color:#777}.feedreader-error{padding:.7rem 1rem;color:#a00;background:#fee}.feedreader-add{padding:.8rem 1rem;display:flex;gap:.6rem;align-items:center;border-bottom:1px solid #ddd}.feedreader-add input{min-width:12rem}@media(max-width: 900px){.feedreader-columns{grid-template-columns:1fr;overflow:auto}.feedreader-tree,.feedreader-messages,.feedreader-reader{min-height:14rem;border-right:0;border-bottom:1px solid #ddd}.feedreader-toolbar,.feedreader-add{flex-wrap:wrap}}.mail .permission-flag{margin-bottom:1rem;display:flex;gap:1rem}.mail-tags{padding:.5rem;border:1px solid rgba(20,20,27,.2);border-radius:6px}.mail-tags__container{display:flex;flex-direction:column}.mail-tags__container .tag-item{display:flex;align-items:center;gap:4px;border-bottom:1px solid rgba(20,20,27,.1);padding:2px 0}.mail-tags__container .tag-item:last-child{border:none}.mail-tags__container .tag-item__color{width:1.25rem;height:1.25rem;aspect-ratio:1}.mail-tags__container .tag-item__name{font-size:1.125rem}.mail-tags__container .tag-item__modify{margin-left:auto;font-size:.75rem;display:flex;gap:4px}.mail-tags__container .tag-item:hover{background-color:#eef3f6}.mail-tags__container .tag-item button,.mail-tags__container .tag-item button.red{padding:.25rem .6rem}.mail-tags-form .input-field{margin-bottom:.5rem}.mail-tags-form .input-field label{margin-right:.5rem}.external-address{margin:0;padding-left:1rem;height:100px;overflow:hidden auto}.external-address::-webkit-scrollbar{display:none}.proxy-server{display:flex;flex-direction:column;gap:4px}.proxy-server__tor>h4,.proxy-server__i2p>h4{margin-bottom:.25rem}.proxy-server__tor>input,.proxy-server__i2p>input{margin-right:.5rem}.proxy-server__tor .proxy-outgoing,.proxy-server__i2p .proxy-outgoing{display:inline-flex;align-items:center;gap:.5rem}.proxy-server__tor .proxy-outgoing__status,.proxy-server__i2p .proxy-outgoing__status{width:1rem;height:1rem;aspect-ratio:1;border:1px solid #000;border-radius:50%}.config-files{display:flex;flex-direction:column;gap:1rem}.proxy-server-container{width:100%;display:flex;flex-direction:column;gap:1rem}.proxy-description{color:#334155;font-size:.95rem;margin-bottom:.5rem}.proxy-rows-container{display:flex;flex-direction:column;gap:.75rem;width:100%}.proxy-row{display:grid;grid-template-columns:160px 220px 220px auto;gap:.75rem;align-items:center;width:100%}.proxy-label{font-size:.95rem;font-weight:500;color:#1e293b}.proxy-addr-input,.proxy-port-input{width:100% !important;max-width:none !important}.proxy-status-container{display:flex;align-items:center;gap:.5rem}.proxy-status-bullet{width:14px;height:14px;border-radius:50%;display:inline-block;border:1px solid #475569}.proxy-status-text{font-size:.95rem;color:#1e293b}@media(max-width: 700px){.config-network{min-width:0;overflow-x:hidden}.config-network .widget{min-width:0;padding:.8rem}.config-network .nw-config-row{display:flex !important;flex-direction:column !important;align-items:stretch !important;gap:.35rem !important;min-width:0}.config-network .nw-config-row>label,.config-network .nw-config-row>p{margin:0 !important}.config-network .nw-mode-group,.config-network .nat-control-group,.config-network .addr-control-group,.config-network .proxy-control-group,.config-network .addr-port-group{width:100%;min-width:0;gap:.5rem !important}.config-network input[type=text],.config-network input[type=number],.config-network select{width:100% !important;max-width:none !important;min-width:0 !important;box-sizing:border-box}.config-network .port-group,.config-network .status-indicator{margin-left:0 !important}.config-network .port-group input[type=number]{width:90px !important}.config-network .external-address{width:100%;height:auto;max-height:9rem;padding-left:1.25rem;overflow:auto;overflow-wrap:anywhere;word-break:break-word;box-sizing:border-box}}@media(max-width: 700px){.node-config .config-grid{display:flex !important;flex-direction:column !important;align-items:stretch !important;gap:.6rem !important;min-width:0;box-sizing:border-box}.node-config .default-id-selector{width:100%;min-width:0}.node-config .default-id-selector select,.node-config .config-grid>select{width:100% !important;min-width:0 !important;max-width:none !important;box-sizing:border-box}.node-config .storage-input-group{justify-content:flex-start}.node-config .table-container{overflow:visible !important}.node-config .history-config-table,.node-config .history-config-table tbody,.node-config .history-config-table tr,.node-config .history-config-table td{display:block;width:100% !important;box-sizing:border-box}.node-config .history-config-table{table-layout:auto}.node-config .history-config-table thead{display:none}.node-config .history-config-table tr{margin:0;padding:.75rem;border-bottom:1px solid #e2e8f0 !important}.node-config .history-config-table tr:last-child{border-bottom:0 !important}.node-config .history-config-table td{padding:.25rem 0 !important;text-align:left !important}.node-config .history-config-table td:nth-child(2),.node-config .history-config-table td:nth-child(3){display:flex;align-items:center;justify-content:space-between;gap:.75rem}.node-config .history-config-table td:nth-child(2)::before{content:"Enable history";color:#64748b;font-size:.8rem;font-weight:600}.node-config .history-config-table td:nth-child(3)::before{content:"Max saved messages";color:#64748b;font-size:.8rem;font-weight:600}}