diff --git a/main.js b/main.js index 7cfeff0..c3d1c01 100644 --- a/main.js +++ b/main.js @@ -67,9 +67,153 @@ console.log('[Oasis] Launching Oasis Social Network Utopia...'); console.log('[Oasis] Backend path:', backendPath); console.log('[Oasis] HOME:', process.env.HOME); -try { +const http = require('http'); +const keylock = require('./src/models/keylock'); +const configPath = path.join(process.env.HOME, '.ssb'); + +let backendStarted = false; +function startBackend() { + if (backendStarted) return; + backendStarted = true; + try { require(backendPath); console.log('[Oasis] Backend started on port 3000'); -} catch (error) { + } catch (error) { console.error('[Oasis] Error starting backend:', error); + } } + +function unlockAndStart(keys) { + global.__OASIS_KEYS__ = keys; + for (const s of sockets) { try { s.destroy(); } catch (_) {} } + try { lockServer.close(() => startBackend()); } catch (_) { startBackend(); } +} + +const MODE = keylock.isConfigured(configPath) ? 'unlock' : 'setup'; + +const LOCK_LANGS = ['en','es','fr','eu','de','it','pt','zh','ar','hi','ru']; +const LOCK_I18N = { + en: { name:"English", t_setup:"Create your password", t_unlock:"Unlock Oasis", s_setup:"It encrypts your identity and keys on this device. If you forget it, you lose the account (no recovery).", s_unlock:"Enter your password to decrypt your keys and start Oasis.", ph:"Password", ph2:"Repeat password", b_setup:"Create and enter", b_unlock:"Enter", e_min:"Minimum 6 characters", e_mism:"Passwords do not match", e_wrong:"Incorrect password", e_pre:"Error: ", m_unlock:"Unlocking…" }, + es: { name:"Español", t_setup:"Crea tu contraseña", t_unlock:"Desbloquea Oasis", s_setup:"Cifrará tu identidad y tus claves en este dispositivo. Si la olvidas, pierdes la cuenta (no hay recuperación).", s_unlock:"Introduce tu contraseña para descifrar tus claves y arrancar Oasis.", ph:"Contraseña", ph2:"Repite la contraseña", b_setup:"Crear y entrar", b_unlock:"Entrar", e_min:"Mínimo 6 caracteres", e_mism:"Las contraseñas no coinciden", e_wrong:"Contraseña incorrecta", e_pre:"Error: ", m_unlock:"Desbloqueando…" }, + fr: { name:"Français", t_setup:"Créez votre mot de passe", t_unlock:"Déverrouiller Oasis", s_setup:"Il chiffre votre identité et vos clés sur cet appareil. Si vous l'oubliez, vous perdez le compte (aucune récupération).", s_unlock:"Saisissez votre mot de passe pour déchiffrer vos clés et démarrer Oasis.", ph:"Mot de passe", ph2:"Répétez le mot de passe", b_setup:"Créer et entrer", b_unlock:"Entrer", e_min:"6 caractères minimum", e_mism:"Les mots de passe ne correspondent pas", e_wrong:"Mot de passe incorrect", e_pre:"Erreur : ", m_unlock:"Déverrouillage…" }, + eu: { name:"Euskara", t_setup:"Sortu zure pasahitza", t_unlock:"Desblokeatu Oasis", s_setup:"Zure identitatea eta gakoak gailu honetan zifratzen ditu. Ahazten baduzu, kontua galtzen duzu (ez dago berreskuratzerik).", s_unlock:"Sartu zure pasahitza gakoak deszifratu eta Oasis abiarazteko.", ph:"Pasahitza", ph2:"Errepikatu pasahitza", b_setup:"Sortu eta sartu", b_unlock:"Sartu", e_min:"Gutxienez 6 karaktere", e_mism:"Pasahitzak ez datoz bat", e_wrong:"Pasahitz okerra", e_pre:"Errorea: ", m_unlock:"Desblokeatzen…" }, + de: { name:"Deutsch", t_setup:"Erstelle dein Passwort", t_unlock:"Oasis entsperren", s_setup:"Es verschlüsselt deine Identität und Schlüssel auf diesem Gerät. Wenn du es vergisst, verlierst du das Konto (keine Wiederherstellung).", s_unlock:"Gib dein Passwort ein, um deine Schlüssel zu entschlüsseln und Oasis zu starten.", ph:"Passwort", ph2:"Passwort wiederholen", b_setup:"Erstellen und eintreten", b_unlock:"Eintreten", e_min:"Mindestens 6 Zeichen", e_mism:"Passwörter stimmen nicht überein", e_wrong:"Falsches Passwort", e_pre:"Fehler: ", m_unlock:"Entsperren…" }, + it: { name:"Italiano", t_setup:"Crea la tua password", t_unlock:"Sblocca Oasis", s_setup:"Cifra la tua identità e le tue chiavi su questo dispositivo. Se la dimentichi, perdi l'account (nessun recupero).", s_unlock:"Inserisci la password per decifrare le chiavi e avviare Oasis.", ph:"Password", ph2:"Ripeti la password", b_setup:"Crea ed entra", b_unlock:"Entra", e_min:"Minimo 6 caratteri", e_mism:"Le password non coincidono", e_wrong:"Password errata", e_pre:"Errore: ", m_unlock:"Sblocco…" }, + pt: { name:"Português", t_setup:"Crie a sua palavra-passe", t_unlock:"Desbloquear o Oasis", s_setup:"Cifra a sua identidade e chaves neste dispositivo. Se a esquecer, perde a conta (sem recuperação).", s_unlock:"Introduza a sua palavra-passe para decifrar as chaves e iniciar o Oasis.", ph:"Palavra-passe", ph2:"Repita a palavra-passe", b_setup:"Criar e entrar", b_unlock:"Entrar", e_min:"Mínimo 6 caracteres", e_mism:"As palavras-passe não coincidem", e_wrong:"Palavra-passe incorreta", e_pre:"Erro: ", m_unlock:"A desbloquear…" }, + zh: { name:"中文", t_setup:"创建你的密码", t_unlock:"解锁 Oasis", s_setup:"它会在此设备上加密你的身份和密钥。如果忘记,你将失去账户(无法恢复)。", s_unlock:"输入密码以解密你的密钥并启动 Oasis。", ph:"密码", ph2:"重复密码", b_setup:"创建并进入", b_unlock:"进入", e_min:"至少 6 个字符", e_mism:"密码不一致", e_wrong:"密码错误", e_pre:"错误:", m_unlock:"正在解锁…" }, + ar: { name:"العربية", t_setup:"أنشئ كلمة المرور", t_unlock:"فتح Oasis", s_setup:"يشفّر هويتك ومفاتيحك على هذا الجهاز. إذا نسيتها، تفقد الحساب (لا استعادة).", s_unlock:"أدخل كلمة المرور لفك تشفير مفاتيحك وتشغيل Oasis.", ph:"كلمة المرور", ph2:"أعد كلمة المرور", b_setup:"إنشاء ودخول", b_unlock:"دخول", e_min:"6 أحرف على الأقل", e_mism:"كلمتا المرور غير متطابقتين", e_wrong:"كلمة مرور غير صحيحة", e_pre:"خطأ: ", m_unlock:"جارٍ الفتح…" }, + hi: { name:"हिन्दी", t_setup:"अपना पासवर्ड बनाएं", t_unlock:"Oasis अनलॉक करें", s_setup:"यह इस डिवाइस पर आपकी पहचान और कुंजियाँ एन्क्रिप्ट करता है। भूल गए तो खाता खो देंगे (कोई रिकवरी नहीं)।", s_unlock:"अपनी कुंजियाँ डिक्रिप्ट करने और Oasis शुरू करने के लिए पासवर्ड दर्ज करें।", ph:"पासवर्ड", ph2:"पासवर्ड दोहराएँ", b_setup:"बनाएँ और प्रवेश करें", b_unlock:"प्रवेश करें", e_min:"कम से कम 6 अक्षर", e_mism:"पासवर्ड मेल नहीं खाते", e_wrong:"गलत पासवर्ड", e_pre:"त्रुटि: ", m_unlock:"अनलॉक हो रहा है…" }, + ru: { name:"Русский", t_setup:"Создайте пароль", t_unlock:"Разблокировать Oasis", s_setup:"Он шифрует вашу личность и ключи на этом устройстве. Если забудете, потеряете аккаунт (без восстановления).", s_unlock:"Введите пароль, чтобы расшифровать ключи и запустить Oasis.", ph:"Пароль", ph2:"Повторите пароль", b_setup:"Создать и войти", b_unlock:"Войти", e_min:"Минимум 6 символов", e_mism:"Пароли не совпадают", e_wrong:"Неверный пароль", e_pre:"Ошибка: ", m_unlock:"Разблокировка…" }, +}; + +function lockPage(mode) { + const confirmField = mode === 'setup' + ? '' + : ''; + return '' + + '' + + 'Oasis' + + '' + + '' + + '

' + + '
' + + confirmField + + '' + + '
' + + ''; +} + +function readBody(req) { + return new Promise((resolve) => { + let b = ''; + req.on('data', (d) => { b += d; if (b.length > 1e5) req.destroy(); }); + req.on('end', () => { try { resolve(JSON.parse(b || '{}')); } catch (_) { resolve({}); } }); + req.on('error', () => resolve({})); + }); +} + +const sockets = new Set(); +const lockServer = http.createServer(async (req, res) => { + try { + if (req.method === 'POST' && req.url === '/__keylock/setup') { + const { password } = await readBody(req); + if (!password || String(password).length < 6) { res.writeHead(400, { 'Connection': 'close' }); return res.end('{"ok":false}'); } + keylock.setPassword(configPath, password); + const keys = keylock.loadOrCreateSecret(configPath); + res.writeHead(200, { 'Content-Type': 'application/json', 'Connection': 'close' }); + res.end('{"ok":true}'); + return unlockAndStart(keys); + } + if (req.method === 'POST' && req.url === '/__keylock/unlock') { + const { password } = await readBody(req); + const kek = password ? keylock.verifyPassword(configPath, password) : null; + if (!kek) { res.writeHead(401, { 'Connection': 'close' }); return res.end('{"ok":false}'); } + const keys = keylock.loadOrCreateSecret(configPath); + res.writeHead(200, { 'Content-Type': 'application/json', 'Connection': 'close' }); + res.end('{"ok":true}'); + return unlockAndStart(keys); + } + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Connection': 'close' }); + res.end(lockPage(MODE)); + } catch (e) { + res.writeHead(500, { 'Connection': 'close' }); + res.end('{"ok":false,"error":"' + String(e && e.message).replace(/"/g, '') + '"}'); + } +}); +lockServer.on('connection', (s) => { sockets.add(s); s.on('close', () => sockets.delete(s)); }); +lockServer.listen(3000, '127.0.0.1', () => { + console.log('[Oasis] keylock gate escuchando en 127.0.0.1:3000 (modo: ' + MODE + ')'); +}); diff --git a/src/backend/backend.js b/src/backend/backend.js index a2cebc6..44492f0 100644 --- a/src/backend/backend.js +++ b/src/backend/backend.js @@ -7887,6 +7887,28 @@ router saveConfig(cfg); ctx.redirect(`/modules`); }) + .get("/settings/bottombar", async (ctx) => { + ctx.body = require("../views/main_views").bottomBarPickerView(); + }) + .post("/settings/bottombar", koaBody(), async (ctx) => { + const cfg = getConfig(); + const MAX = 4; + const body = ctx.request.body || {}; + const action = String(body.action || '').trim(); + const key = String(body.key || '').trim(); + const catalog = require("../views/main_views").PINNABLE_MODULES || []; + let pins = Array.isArray(cfg.bottomBarPins) ? cfg.bottomBarPins.slice() : []; + if (action === 'add') { + if (key && catalog.some(m => m.key === key) && !pins.includes(key) && pins.length < MAX) pins.push(key); + } else if (action === 'remove') { + pins = pins.filter(k => k !== key); + } else if (action === 'clear') { + pins = []; + } + cfg.bottomBarPins = pins.slice(0, MAX); + saveConfig(cfg); + ctx.redirect('/settings/bottombar'); + }) .post("/settings/ai", koaBody(), async (ctx) => { const aiPrompt = String(ctx.request.body.ai_prompt || "").trim(); if (aiPrompt.length > 128) { sendErrorPage(ctx, "Prompt too long. Must be 128 characters or fewer.", { status: 400 }); return; } diff --git a/src/client/assets/styles/mobile.css b/src/client/assets/styles/mobile.css index 97dca01..18c1720 100644 --- a/src/client/assets/styles/mobile.css +++ b/src/client/assets/styles/mobile.css @@ -914,620 +914,3 @@ a.create-button { .peers-technical-block .block-info-table td[data-label=""]::before { content: none !important; } .peers-list .block-info-table td .peer-key, .peers-technical-block .block-info-table td .peer-key { text-align: right !important; } - -.oasis-bottombar { - display: flex !important; - position: fixed !important; - left: 0 !important; right: 0 !important; bottom: 0 !important; - z-index: 1000 !important; - justify-content: space-around !important; - align-items: center !important; - gap: 4px !important; - background: #1F1F1F !important; - border-top: 1px solid #333 !important; - padding: 8px 6px calc(8px + env(safe-area-inset-bottom, 0px)) !important; -} -.oasis-bottombar .bb-item { - position: relative !important; - display: flex !important; - flex-direction: column !important; - align-items: center !important; - gap: 3px !important; - flex: 1 1 0 !important; - min-width: 0 !important; - padding: 2px 0 !important; - text-decoration: none !important; -} -.oasis-bottombar .bb-ico { font-size: 18px !important; line-height: 1 !important; color: #FFA500 !important; } -.oasis-bottombar .bb-lbl { font-size: 9px !important; color: #ccc !important; white-space: nowrap !important; } -.oasis-bottombar .bb-badge { - position: absolute !important; - top: -4px !important; - left: calc(50% + 6px) !important; - min-width: 15px !important; height: 15px !important; - padding: 0 3px !important; - border-radius: 8px !important; - background: #FFA500 !important; - color: #121212 !important; - font-size: 9px !important; font-weight: bold !important; - display: flex !important; align-items: center !important; justify-content: center !important; -} - -body { padding-bottom: 64px !important; } - -.oasis-mobile-topbar { - display: flex !important; - align-items: center !important; - justify-content: space-between !important; - width: 100% !important; - padding: 8px 12px !important; - background: transparent !important; - box-shadow: none !important; border: 0 !important; -} - -.oasis-mobile-topbar .omt-logo img, -.oasis-mobile-topbar .omt-avatar img { - width: 32px !important; height: 32px !important; - border-radius: 50% !important; - border: 2px solid #FFA500 !important; - object-fit: cover !important; display: block !important; -} - -.oasis-mobile-topbar .omt-title { - flex: 1 1 auto !important; - text-align: center !important; - font-size: 26px !important; font-weight: bold !important; - letter-spacing: 10px !important; text-indent: 10px !important; - color: #FFD700 !important; text-decoration: none !important; - white-space: nowrap !important; - background: transparent !important; border: 0 !important; box-shadow: none !important; padding: 0 !important; -} - -.header .top-bar-left > a.logo-icon { display: none !important; } - -.oasis-mobile-menus { display: flex !important; gap: 8px !important; padding: 0 10px 6px !important; } -.oasis-mobile-menus .oasis-menu-ico { display: none !important; } -.oasis-mobile-menus .oasis-menu-btn { - flex: 1 1 0 !important; - display: flex !important; align-items: center !important; justify-content: center !important; - padding: 12px !important; - border: 1.5px solid #333 !important; border-radius: 10px !important; - background: #121212 !important; color: #ccc !important; - font-weight: bold !important; letter-spacing: 1px !important; cursor: pointer !important; -} - -#nav-left-toggle:checked ~ .header .oasis-menu-btn[for="nav-left-toggle"] { - background: #FFA500 !important; color: #121212 !important; border-color: #FFA500 !important; -} - -#nav-right-toggle:checked ~ .header .oasis-menu-btn[for="nav-right-toggle"] { - background: #FFD700 !important; color: #121212 !important; border-color: #FFD700 !important; -} - -:root { - --space-1: 4px; --space-2: 8px; --space-3: 16px; --space-4: 24px; --space-5: 32px; - --fs-caption: 12px; --fs-body: 14px; --fs-body-lg: 16px; --fs-title: 20px; - --lh-body: 1.5; --measure: 66ch; - --c-muted: #8c8c8c; --radius-md: 12px; -} - -.oasis-bottombar .bb-fab .bb-ico { - width: 46px !important; height: 46px !important; - margin-top: -22px !important; - border-radius: 50% !important; - background: #FFA500 !important; color: #121212 !important; - display: flex !important; align-items: center !important; justify-content: center !important; - font-size: 22px !important; - box-shadow: 0 2px 8px rgba(0,0,0,.4) !important; -} -.oasis-bottombar .bb-fab .bb-lbl { color: #FFA500 !important; font-weight: bold !important; } - -.main-column, .main-column p, .main-column li, .main-column a, -.main-column span, .main-column td, .main-column h1, .main-column h2, .main-column h3 { - overflow-wrap: anywhere !important; - word-break: break-word !important; -} -.main-column { padding: var(--space-3) !important; } -.main-column p { line-height: var(--lh-body) !important; } - -.main-column > * + * { margin-top: var(--space-3) !important; } - -.main-column img, .main-column video, .main-column table, .main-column pre, .main-column iframe { - max-width: 100% !important; height: auto !important; -} -.main-column pre { white-space: pre-wrap !important; } - -.oasis-nav-group { - list-style: none !important; - background: #1c1c1c !important; - border: 1px solid #333 !important; - border-radius: var(--radius-md) !important; - overflow: hidden !important; - margin-bottom: var(--space-2) !important; -} -.oasis-nav-header { - display: flex !important; align-items: center !important; gap: var(--space-2) !important; - min-height: 48px !important; padding: 0 var(--space-3) !important; - cursor: pointer !important; - color: #FFD700 !important; font-weight: bold !important; - text-transform: uppercase !important; letter-spacing: .04em !important; - font-size: var(--fs-caption) !important; -} -.oasis-nav-header .nav-text { flex: 1 1 auto !important; } -.oasis-nav-header .oasis-nav-arrow { color: var(--c-muted) !important; transition: transform 150ms ease !important; } -.oasis-nav-toggle:checked + .oasis-nav-header .oasis-nav-arrow { transform: rotate(180deg) !important; } -.oasis-nav-header:active { background: #262626 !important; } -.oasis-nav-list { list-style: none !important; padding: 0 !important; margin: 0 !important; } -.oasis-nav-list li a { - display: flex !important; align-items: center !important; gap: var(--space-3) !important; - min-height: 48px !important; padding: 0 var(--space-3) !important; - color: #e8e8e8 !important; text-decoration: none !important; - border-top: 1px solid #262626 !important; -} -.oasis-nav-list li a .emoji { flex: 0 0 auto !important; width: 22px !important; text-align: center !important; color: #FFA500 !important; } -.oasis-nav-list li a .nav-text { font-size: var(--fs-body) !important; } -.oasis-nav-list li a:active { background: #262626 !important; } -.oasis-nav-list li a.current { color: #FFA500 !important; font-weight: bold !important; } - -.main-column h1 { font-size: var(--fs-title) !important; font-weight: 700 !important; line-height: 1.25 !important; margin: 0 0 var(--space-2) !important; } -.main-column h2 { font-size: 18px !important; font-weight: 700 !important; line-height: 1.3 !important; margin: 0 0 var(--space-2) !important; } -.main-column h3 { font-size: var(--fs-body-lg) !important; font-weight: 600 !important; margin: 0 0 var(--space-1) !important; } -.main-column p { font-size: var(--fs-body-lg) !important; } -.main-column .created-at, .main-column .small, .main-column .time, -.main-column .meta, .main-column .author-meta, .main-column .subtitle { - color: var(--c-muted) !important; font-size: var(--fs-caption) !important; -} - -.main-column article, -.main-column [class*="-card"] { - background: #1c1c1c !important; - border: 1px solid #333 !important; - border-radius: var(--radius-md) !important; - padding: var(--space-3) !important; -} -.main-column article:active, -.main-column [class*="-card"]:active { background: #202020 !important; } - -.btn, button, input[type="submit"], input[type="button"], .submit-button, -.filter-btn, .create-button, .edit-btn, .delete-btn, .join-btn, .leave-btn, .buy-btn { - min-height: 48px !important; -} -button:active, input[type="submit"]:active, .submit-button:active, -.filter-btn:active, .create-button:active, .edit-btn:active, -.delete-btn:active, .join-btn:active, .leave-btn:active, .buy-btn:active { - filter: brightness(.85) !important; -} -.main-column a:active { opacity: .6 !important; } - -* { -webkit-tap-highlight-color: transparent !important; } -a, button, label, .tap, .oasis-menu-btn, .bb-item, .oasis-nav-header, .oasis-nav-list li a { - touch-action: manipulation !important; -} -body { overscroll-behavior: none !important; } - -.header .top-bar-left nav, -.header .top-bar-right nav { display: none !important; } - -.oasis-mobile-topbar { padding-top: calc(env(safe-area-inset-top, 0px) + 10px) !important; } - -.oasis-bottombar .bb-pair { - display: flex !important; align-items: center !important; - flex: 0 0 auto !important; - gap: 2px !important; padding: 3px !important; - background: #262626 !important; - border: 1px solid #3a3a3a !important; - border-radius: 999px !important; -} -.oasis-bottombar .bb-pair .bb-item { flex: 0 0 auto !important; padding: 2px 8px !important; } -.oasis-bottombar .bb-pair .bb-lbl { font-size: 8px !important; } - -.oasis-bottombar a.bb-item.current .bb-ico { - background: #FFA500 !important; color: #121212 !important; - border-radius: 999px !important; padding: 4px 12px !important; -} -.oasis-bottombar a.bb-item.current .bb-lbl { color: #FFA500 !important; font-weight: bold !important; } -.oasis-bottombar a.bb-item.current .bb-badge { background: #121212 !important; color: #FFA500 !important; } - -.main-column, .main-column p, .main-column li, .main-column a, -.main-column span, .main-column td, .main-column th, .main-column label, -.main-column h1, .main-column h2, .main-column h3, .main-column h4, -.main-column strong, .main-column em, .main-column small, -.created-at, .post-meta, .feed-post-meta, -.feed-row .small, .feed-row .time, .feed-row .created-at, -.profile-sensors-box > *, -.card-field .card-value, .card-field .card-label { - overflow-wrap: break-word !important; - word-break: normal !important; - -webkit-hyphens: none !important; - hyphens: none !important; -} - -.main-column pre, .main-column code, kbd, samp, -.peer-key, .user-link.peer-key, .oasis-id-copy, .search-oasis-id, -.pm-sent-key-value, .kpi__value, -.block-info-table td .peer-key, .block-info-table td .user-link, -.pm-info-table td .user-link, -.main-column a[href*="ed25519"], .main-column a[href^="ssb:"], -.main-column a[href^="/author/"] { - overflow-wrap: anywhere !important; - word-break: break-all !important; -} - -button, input[type="submit"], input[type="button"], -.filter-btn, a.filter-btn, .create-button, .edit-btn, .delete-btn, -.join-btn, .leave-btn, .buy-btn, .submit-button, .btn, -.conn-actions button, .peers-conn-actions button, -.mode-buttons button, .mode-buttons .filter-btn, -.filter-group button, .filter-group .filter-btn, -.main-column button, .main-column input[type="submit"] { - white-space: normal !important; - word-break: normal !important; - overflow-wrap: break-word !important; - -webkit-hyphens: none !important; - hyphens: none !important; -} - -.conn-actions, .peers-conn-actions { - display: flex !important; - flex-direction: row !important; - flex-wrap: wrap !important; - justify-content: center !important; - align-items: stretch !important; - gap: 8px !important; - width: 100% !important; - max-width: 100% !important; - overflow-x: hidden !important; -} -.conn-actions > form, .peers-conn-actions > form { - flex: 0 1 auto !important; - width: auto !important; - min-width: 0 !important; - margin: 0 !important; - display: flex !important; -} - -.pm-info-table, .pm-info-table tbody, -.block-info-table, .block-info-table tbody { - display: block !important; - width: 100% !important; - max-width: 100% !important; - border: 0 !important; - background: transparent !important; - box-shadow: none !important; - table-layout: auto !important; -} -.pm-info-table thead, .block-info-table thead { display: none !important; } -.pm-info-table tr, .block-info-table tr { - display: block !important; - width: 100% !important; - box-sizing: border-box !important; - border: 0 !important; - border-bottom: 1px solid rgba(255,165,0,0.18) !important; - border-radius: 0 !important; - margin: 0 0 8px 0 !important; - padding: 6px 0 !important; - background: transparent !important; -} -.pm-info-table td, .block-info-table td { - display: block !important; - width: 100% !important; - max-width: 100% !important; - box-sizing: border-box !important; - border: 0 !important; - background: transparent !important; - padding: 1px 0 !important; - text-align: left !important; - overflow-wrap: break-word !important; - word-break: normal !important; -} - -.pm-info-table td.card-label, .block-info-table td.card-label { - color: var(--c-muted, #8c8c8c) !important; - font-size: 11px !important; - font-weight: 700 !important; - text-transform: uppercase !important; - letter-spacing: .03em !important; - margin-bottom: 2px !important; - padding-bottom: 0 !important; -} -.pm-info-table td.card-value, .block-info-table td.card-value { - font-size: 15px !important; - line-height: 1.5 !important; - margin-bottom: 6px !important; -} - -.block-info-table tr:first-child:has(> td.card-label) { display: none !important; } -.block-info-table td[data-label]:not([data-label=""])::before { - content: attr(data-label); - display: block !important; - color: var(--c-muted, #8c8c8c) !important; - font-size: 10px !important; - font-weight: 700 !important; - text-transform: uppercase !important; - letter-spacing: .03em !important; - margin-bottom: 1px !important; - white-space: nowrap !important; - word-break: normal !important; -} -.block-info-table td[data-label=""]::before { content: none !important; } - -.peers-list .block-info-table td, -.peers-technical-block .block-info-table td, -.gov-overview tbody td { - word-break: normal !important; - overflow-wrap: break-word !important; -} - -.pm-info-table td .user-link, .pm-info-table td .peer-key, .pm-info-table td a[href], -.block-info-table td .peer-key, .block-info-table td .user-link, .block-info-table td a[href] { - display: inline-block !important; - max-width: 100% !important; - overflow-wrap: anywhere !important; - word-break: break-all !important; -} - -.pm-info-table td form, .block-info-table td form, -.pm-info-table td button, .block-info-table td button { - width: 100% !important; - margin: 0 !important; -} - -.main-content { width: 100% !important; max-width: 100% !important; } -.main-column { - width: 100% !important; max-width: 100% !important; - padding: 16px !important; box-sizing: border-box !important; -} -.main-column .card-field { - flex-direction: column !important; - align-items: stretch !important; - gap: 2px !important; -} -.main-column .card-field .card-value { - width: 100% !important; max-width: 100% !important; font-size: 15px !important; -} -.main-column .card-field .card-label { - color: var(--c-muted, #8c8c8c) !important; font-size: 11px !important; -} -.main-column img, .main-column video, .main-column iframe, .main-column canvas { - max-width: 100% !important; height: auto !important; -} -.main-column hr { - width: 100% !important; border: 0 !important; - border-top: 1px solid #333 !important; margin: 16px 0 !important; -} - -.activity-filter-acc { width: 100% !important; margin-bottom: 8px !important; } - -.activity-quick-modes { - display: flex !important; flex-wrap: nowrap !important; gap: 6px !important; margin-bottom: 8px !important; -} -.activity-quick-modes form { flex: 1 1 0 !important; min-width: 0 !important; margin: 0 !important; display: flex !important; } -.activity-quick-modes .filter-btn { - flex: 1 1 0 !important; width: 100% !important; min-height: 44px !important; - white-space: nowrap !important; word-break: keep-all !important; overflow-wrap: normal !important; - padding-left: 6px !important; padding-right: 6px !important; font-size: 14px !important; -} - -.activity-groups { list-style: none !important; padding: 0 !important; margin: 0 !important; } - -.activity-groups .oasis-nav-header .emoji { display: none !important; } -.activity-groups .oasis-nav-header .nav-text { flex: 1 1 auto !important; } -.activity-groups .act-count { - flex: 0 0 auto !important; min-width: 20px !important; text-align: center !important; - font-size: 11px !important; font-weight: 700 !important; color: #121212 !important; - background: #FFA500 !important; border-radius: 999px !important; padding: 1px 7px !important; - margin: 0 8px 0 0 !important; -} - -.activity-groups .oasis-nav-list li { list-style: none !important; margin: 0 !important; padding: 0 !important; } -.activity-groups .oasis-nav-list form { margin: 0 !important; display: block !important; width: 100% !important; } -.activity-groups .oasis-nav-list .filter-btn { - display: block !important; width: 100% !important; text-align: left !important; - min-height: 44px !important; margin: 2px 0 !important; box-sizing: border-box !important; -} -.activity-groups .oasis-nav-list .filter-btn.active { - outline: 2px solid #FFA500 !important; outline-offset: -2px !important; -} - -.message-list { display: flex !important; flex-direction: column !important; gap: 4px !important; width: 100% !important; } -.message-list .pm-thread { display: flex !important; flex-direction: column !important; width: 100% !important; align-self: stretch !important; } -.pm-thread-replies { display: flex !important; flex-direction: column !important; gap: 4px !important; } - -.pm-card.pm-bubble { - max-width: 82% !important; width: fit-content !important; min-width: 0 !important; - box-sizing: border-box !important; padding: 8px 12px !important; margin: 2px 0 !important; - border-radius: 16px !important; border: 1px solid #333 !important; - background: #1e1e1e !important; box-shadow: none !important; cursor: default !important; -} - -.pm-card.pm-bubble.pm-recv { - align-self: flex-start !important; margin-left: 0 !important; margin-right: auto !important; - border-bottom-left-radius: 4px !important; background: #1e1e1e !important; border-color: #343434 !important; -} - -.pm-card.pm-bubble.pm-sent { - align-self: flex-end !important; margin-left: auto !important; margin-right: 0 !important; - border-bottom-right-radius: 4px !important; background: #2a2413 !important; border-color: #5c4c1f !important; -} - -.pm-bubble .pm-bubble-sender { font-size: 11px !important; font-weight: 700 !important; color: #FFA500 !important; margin-bottom: 3px !important; } -.pm-bubble .pm-bubble-sender a { color: #FFA500 !important; } - -.pm-bubble .message-text { font-size: 15px !important; line-height: 1.42 !important; color: #ececec !important; margin: 0 !important; } -.pm-bubble .message-text a { color: #FFD700 !important; } - -.pm-bubble .pm-bubble-meta { - display: flex !important; flex-wrap: wrap !important; align-items: center !important; - gap: 6px !important; margin-top: 4px !important; font-size: 10px !important; color: var(--c-muted, #8c8c8c) !important; -} -.pm-card.pm-sent .pm-bubble-meta { justify-content: flex-end !important; } -.pm-card.pm-recv .pm-bubble-meta { justify-content: flex-start !important; } -.pm-bubble .pm-bubble-subject { font-style: italic !important; opacity: .75 !important; max-width: 50% !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } -.pm-bubble .pm-bubble-lock { opacity: .85 !important; } -.pm-bubble .pm-bubble-eco { font-size: 9px !important; opacity: .9 !important; } -.pm-bubble .pm-bubble-eco * { font-size: 9px !important; } - -.pm-bubble .pm-actions { margin-top: 6px !important; display: flex !important; gap: 6px !important; } -.pm-card.pm-sent .pm-actions { justify-content: flex-end !important; } -.pm-bubble .pm-actions .pm-btn { font-size: 10px !important; padding: 3px 8px !important; min-height: 28px !important; } -.pm-bubble .pm-actions form { margin: 0 !important; } - -.pm-bubble .pm-info-table { display: none !important; } - -.pm-thread .pm-thread-details { width: 100% !important; } -.pm-thread .pm-thread-toggle { font-size: 12px !important; color: var(--c-muted, #8c8c8c) !important; padding: 4px 0 !important; cursor: pointer !important; } - -.main-column h1, .main-column h2, .main-column h3, .main-column h4, -.tags-header h2, .tags-header h3, .title-with-chip h2, .pm-title, h2.pm-title, -.cn-name, h1.cn-name, .cn-section, h2.cn-section, .profile-side-name, h2.profile-side-name { - text-wrap: balance !important; - overflow-wrap: break-word !important; - word-break: keep-all !important; - -webkit-hyphens: none !important; hyphens: none !important; - text-align: left !important; -} -.main-column h1, .tags-header h2, .cn-name { - font-size: var(--fs-title, 20px) !important; font-weight: 700 !important; - line-height: 1.22 !important; letter-spacing: -0.01em !important; - margin: 0 0 var(--space-2, 8px) !important; -} -.main-column h2, .pm-title, .title-with-chip h2, .cn-section { - font-size: 18px !important; font-weight: 700 !important; - line-height: 1.28 !important; margin: var(--space-1, 4px) 0 var(--space-2, 8px) !important; -} -.main-column h3 { - font-size: var(--fs-body-lg, 16px) !important; font-weight: 600 !important; - line-height: 1.35 !important; margin: var(--space-1, 4px) 0 !important; -} -.main-column h1, .main-column h2, .main-column h3, -.tags-header h2, .pm-title, .cn-name, .cn-section { - max-width: 40ch !important; width: auto !important; min-width: 0 !important; -} - -.main-column h1 code, .main-column h2 code, .main-column h3 code, -.tags-header h2 code, .pm-title code, -.main-column h1 a[href*="ed25519"], .main-column h2 a[href*="ed25519"], -.main-column h1 a[href^="ssb:"], .main-column h2 a[href^="ssb:"] { - word-break: break-all !important; overflow-wrap: anywhere !important; -} - -.main-column .tags-header { - display: flex !important; flex-direction: column !important; align-items: flex-start !important; - gap: var(--space-1, 4px) !important; width: 100% !important; max-width: 100% !important; - margin: 0 0 var(--space-2, 8px) !important; -} -.main-column .tags-header h2 { margin: 0 !important; } - -.main-column .title-with-chip { - display: flex !important; flex-wrap: wrap !important; align-items: center !important; - gap: var(--space-2, 8px) !important; width: 100% !important; max-width: 100% !important; -} -.main-column .title-with-chip h2 { flex: 1 1 auto !important; min-width: 0 !important; margin: 0 !important; max-width: none !important; } -.main-column .title-with-chip > :not(h2) { flex: 0 0 auto !important; } - -.main-column p, .main-column li, -.tags-header p, .tags-header .prefs-help, .indexing-note, .indexing-percent, -.error-page-message, .welcome-text, .message-text, .pm-bubble .message-text { - text-wrap: pretty !important; - line-height: 1.55 !important; - max-width: min(66ch, 100%) !important; - overflow-wrap: anywhere !important; - word-break: normal !important; - min-width: 0 !important; - -webkit-hyphens: none !important; hyphens: none !important; -} -.main-column p + p { margin-top: var(--space-3, 16px) !important; } -.tags-header p, .tags-header .prefs-help, .indexing-note { line-height: 1.5 !important; margin: 4px 0 0 0 !important; } -.pm-bubble .message-text { line-height: 1.45 !important; max-width: 100% !important; } - -[style*="auto-fill"], -[style*="minmax(240px"], [style*="minmax(280px"], [style*="minmax(300px"], -[style*="minmax( 240px"], [style*="minmax( 280px"], [style*="minmax( 300px"], -.trending-container, .cn-hub-grid, .cn-products { - display: grid !important; grid-template-columns: 1fr !important; gap: 12px !important; - width: 100% !important; max-width: 100% !important; -} -.trending-container > *, .games-grid > *, .market-grid > *, .tribe-grid > *, -.feed-container, .feed-card, .trending-card, -.main-column article, .main-column [class*="-card"], .main-column [class*="-item"], -.main-column [class*="-entry"], .main-column .card { - width: 100% !important; max-width: 100% !important; min-width: 0 !important; - box-sizing: border-box !important; margin-left: 0 !important; margin-right: 0 !important; -} -.feed-card .feed-text, .trending-card .feed-text, -.main-column article p, .main-column [class*="-card"] p { max-width: var(--measure, 66ch) !important; } -.main-column form, .comment-form, .comment-form-wrapper, .market-form, -.new-message-form, .project-control-form, .filter-box { - display: block !important; width: 100% !important; max-width: 100% !important; - min-width: 0 !important; box-sizing: border-box !important; -} -.main-column input:not([type="checkbox"]):not([type="radio"]):not(.oasis-nav-toggle):not(.oasis-nav-side-toggle), -.main-column textarea, .main-column select { - width: 100% !important; max-width: 100% !important; min-width: 0 !important; box-sizing: border-box !important; -} -.main-column form > button:only-of-type, .main-column form > .create-button, -.comment-form button[type="submit"], .market-form button[type="submit"], -.new-message-form button[type="submit"] { - display: block !important; width: 100% !important; max-width: 100% !important; - box-sizing: border-box !important; margin-top: 8px !important; -} -[style*="width:33%"], [style*="width: 33%"], [style*="width:25%"], [style*="width: 25%"], -[style*="width:60%"], [style*="width: 60%"], [style*="width:40%"], [style*="width: 40%"] { - width: 100% !important; max-width: 100% !important; -} -.main-column [style*="max-width: 800px"], .main-column [style*="max-width:800px"], -.ai-response, .ai-train-bar { - max-width: 100% !important; width: 100% !important; margin-left: 0 !important; margin-right: 0 !important; box-sizing: border-box !important; -} -.message-list, .pm-thread, .pm-thread-replies { width: 100% !important; max-width: 100% !important; box-sizing: border-box !important; } - -.main-column p .user-link, .main-column p a[href^="/author/"], -.main-column li .user-link, .main-column span .user-link, -.main-column .message-text .user-link, .main-column .message-text a[href^="/author/"], -.pm-bubble .message-text .user-link, .pm-bubble .message-text a[href^="/author/"], -.pm-quote a[href^="/author/"] { - display: inline !important; white-space: normal !important; - overflow-wrap: anywhere !important; word-break: keep-all !important; - -webkit-hyphens: none !important; hyphens: none !important; -} - -.peer-key, .user-link.peer-key, .block-info-table td .peer-key, .pm-info-table td .peer-key, -.pm-info-table td .user-link, .block-info-table td .user-link, -.search-oasis-id, .pm-sent-key-value, .kpi__value { - display: inline-block !important; max-width: 100% !important; - overflow-wrap: anywhere !important; word-break: break-all !important; - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace !important; - font-variant-numeric: tabular-nums !important; line-height: 1.45 !important; -} - -.oasis-id-copy { - display: block !important; width: 100% !important; white-space: nowrap !important; - overflow: hidden !important; text-overflow: ellipsis !important; - font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace !important; -} -.invites-list .user-link.peer-key, .peers-list .block-info-table td .peer-key { - display: block !important; max-width: 100% !important; white-space: nowrap !important; - overflow: hidden !important; text-overflow: ellipsis !important; word-break: normal !important; -} - -.main-column .message-text a[href^="http"], .main-column p a[href^="http"], -.pm-bubble .message-text a[href^="http"], .main-column .message-text a.oasis-path-link, -.main-column .message-text a.job-link, .main-column .message-text a.project-link, -.main-column .message-text a.market-link { - overflow-wrap: anywhere !important; word-break: keep-all !important; - -webkit-hyphens: none !important; hyphens: none !important; -} -.main-column code:not(pre code), .pm-bubble .message-text code { - display: inline !important; max-width: 100% !important; - overflow-wrap: anywhere !important; word-break: break-word !important; white-space: pre-wrap !important; -} - -.main-column .tags-header { margin-bottom: 6px !important; gap: 2px !important; } -.main-column .tags-header h2, .tags-header h2 { - font-size: 17px !important; line-height: 1.2 !important; letter-spacing: 0 !important; -} -.main-column .tags-header p { font-size: 13px !important; line-height: 1.35 !important; margin: 0 !important; } - -.main-column { padding: 10px 12px 16px 12px !important; } -.activity-filter-acc { margin-bottom: 4px !important; } - diff --git a/src/client/assets/themes/OasisMobileUX.css b/src/client/assets/themes/OasisMobileUX.css new file mode 100644 index 0000000..a43279c --- /dev/null +++ b/src/client/assets/themes/OasisMobileUX.css @@ -0,0 +1,777 @@ +html { overflow-x: clip !important; overflow-y: visible !important; height: auto !important; } +body { overflow-x: clip !important; overflow-y: visible !important; } + + +.oasis-bottombar { + display: flex !important; + position: fixed !important; + left: 0 !important; right: 0 !important; bottom: 0 !important; + z-index: 1000 !important; + justify-content: space-around !important; + align-items: center !important; + gap: 4px !important; + background: #1F1F1F !important; + border-top: 1px solid #333 !important; + padding: 8px 6px calc(8px + env(safe-area-inset-bottom, 0px)) !important; +} +.oasis-bottombar .bb-item { + position: relative !important; + display: flex !important; + flex-direction: column !important; + align-items: center !important; + gap: 3px !important; + flex: 1 1 0 !important; + min-width: 0 !important; + padding: 2px 0 !important; + text-decoration: none !important; +} +.oasis-bottombar .bb-ico { font-size: 18px !important; line-height: 1 !important; color: #FFA500 !important; } +.oasis-bottombar .bb-lbl { font-size: 9px !important; color: #ccc !important; white-space: nowrap !important; } +.oasis-bottombar .bb-badge { + position: absolute !important; + top: -4px !important; + left: calc(50% + 6px) !important; + min-width: 15px !important; height: 15px !important; + padding: 0 3px !important; + border-radius: 8px !important; + background: #FFA500 !important; + color: #121212 !important; + font-size: 9px !important; font-weight: bold !important; + display: flex !important; align-items: center !important; justify-content: center !important; +} + +body { padding-bottom: calc(64px + env(safe-area-inset-bottom, 0px)) !important; } + +.oasis-mobile-topbar { + display: flex !important; + align-items: center !important; + justify-content: space-between !important; + width: 100% !important; + padding: 8px 12px !important; + background: transparent !important; + box-shadow: none !important; border: 0 !important; +} + +.oasis-mobile-topbar .omt-logo img, +.oasis-mobile-topbar .omt-avatar img { + width: 32px !important; height: 32px !important; + border-radius: 50% !important; + border: 2px solid #FFA500 !important; + object-fit: cover !important; display: block !important; +} + +.oasis-mobile-topbar .omt-title { + flex: 1 1 auto !important; + text-align: center !important; + font-size: 26px !important; font-weight: bold !important; + letter-spacing: 10px !important; text-indent: 10px !important; + color: #FFD700 !important; text-decoration: none !important; + white-space: nowrap !important; + background: transparent !important; border: 0 !important; box-shadow: none !important; padding: 0 !important; +} + +.header .top-bar-left > a.logo-icon { display: none !important; } + +.oasis-mobile-menus { display: flex !important; gap: 8px !important; padding: 0 10px 6px !important; } +.oasis-mobile-menus .oasis-menu-ico { display: none !important; } +.oasis-mobile-menus .oasis-menu-btn { + flex: 1 1 0 !important; + display: flex !important; align-items: center !important; justify-content: center !important; + padding: 12px !important; + border: 1.5px solid #333 !important; border-radius: 10px !important; + background: #121212 !important; color: #ccc !important; + font-weight: bold !important; letter-spacing: 1px !important; cursor: pointer !important; +} + +#nav-left-toggle:checked ~ .header .oasis-menu-btn[for="nav-left-toggle"] { + background: #FFA500 !important; color: #121212 !important; border-color: #FFA500 !important; +} + +#nav-right-toggle:checked ~ .header .oasis-menu-btn[for="nav-right-toggle"] { + background: #FFD700 !important; color: #121212 !important; border-color: #FFD700 !important; +} + +:root { + --space-1: 4px; --space-2: 8px; --space-3: 16px; --space-4: 24px; --space-5: 32px; + --fs-caption: 12px; --fs-body: 14px; --fs-body-lg: 16px; --fs-title: 20px; + --lh-body: 1.5; --measure: 66ch; + --c-muted: #8c8c8c; --radius-md: 12px; +} + +.oasis-bottombar .bb-fab .bb-ico { + width: 46px !important; height: 46px !important; + margin-top: -22px !important; + border-radius: 50% !important; + background: #FFA500 !important; color: #121212 !important; + display: flex !important; align-items: center !important; justify-content: center !important; + font-size: 22px !important; + box-shadow: 0 2px 8px rgba(0,0,0,.4) !important; +} +.oasis-bottombar .bb-fab .bb-lbl { color: #FFA500 !important; font-weight: bold !important; } + +.main-column, .main-column p, .main-column li, .main-column a, +.main-column span, .main-column td, .main-column h1, .main-column h2, .main-column h3 { + overflow-wrap: anywhere !important; + word-break: break-word !important; +} +.main-column { padding: var(--space-3) !important; } +.main-column p { line-height: var(--lh-body) !important; } + +.main-column > * + * { margin-top: var(--space-3) !important; } + +.main-column img, .main-column video, .main-column table, .main-column pre, .main-column iframe { + max-width: 100% !important; height: auto !important; +} +.main-column pre { white-space: pre-wrap !important; } + +.oasis-nav-group { + list-style: none !important; + background: #1c1c1c !important; + border: 1px solid #333 !important; + border-radius: var(--radius-md) !important; + overflow: hidden !important; + margin-bottom: var(--space-2) !important; +} +.oasis-nav-header { + display: flex !important; align-items: center !important; gap: var(--space-2) !important; + min-height: 48px !important; padding: 0 var(--space-3) !important; + cursor: pointer !important; + color: #FFD700 !important; font-weight: bold !important; + text-transform: uppercase !important; letter-spacing: .04em !important; + font-size: var(--fs-caption) !important; +} +.oasis-nav-header .nav-text { flex: 1 1 auto !important; } +.oasis-nav-header .oasis-nav-arrow { color: var(--c-muted) !important; transition: transform 150ms ease !important; } +.oasis-nav-toggle:checked + .oasis-nav-header .oasis-nav-arrow { transform: rotate(180deg) !important; } +.oasis-nav-header:active { background: #262626 !important; } +.oasis-nav-list { list-style: none !important; padding: 0 !important; margin: 0 !important; } +.oasis-nav-list li a { + display: flex !important; align-items: center !important; gap: var(--space-3) !important; + min-height: 48px !important; padding: 0 var(--space-3) !important; + color: #e8e8e8 !important; text-decoration: none !important; + border-top: 1px solid #262626 !important; +} +.oasis-nav-list li a .emoji { flex: 0 0 auto !important; width: 22px !important; text-align: center !important; color: #FFA500 !important; } +.oasis-nav-list li a .nav-text { font-size: var(--fs-body) !important; } +.oasis-nav-list li a:active { background: #262626 !important; } +.oasis-nav-list li a.current { color: #FFA500 !important; font-weight: bold !important; } + +.main-column h1 { font-size: var(--fs-title) !important; font-weight: 700 !important; line-height: 1.25 !important; margin: 0 0 var(--space-2) !important; } +.main-column h2 { font-size: 18px !important; font-weight: 700 !important; line-height: 1.3 !important; margin: 0 0 var(--space-2) !important; } +.main-column h3 { font-size: var(--fs-body-lg) !important; font-weight: 600 !important; margin: 0 0 var(--space-1) !important; } +.main-column p { font-size: var(--fs-body-lg) !important; } +.main-column .created-at, .main-column .small, .main-column .time, +.main-column .meta, .main-column .author-meta, .main-column .subtitle { + color: var(--c-muted) !important; font-size: var(--fs-caption) !important; +} + +.main-column article, +.main-column [class*="-card"] { + background: #1c1c1c !important; + border: 1px solid #333 !important; + border-radius: var(--radius-md) !important; + padding: var(--space-3) !important; +} +.main-column article:active, +.main-column [class*="-card"]:active { background: #202020 !important; } + +.btn, button, input[type="submit"], input[type="button"], .submit-button, +.filter-btn, .create-button, .edit-btn, .delete-btn, .join-btn, .leave-btn, .buy-btn { + min-height: 48px !important; +} +button:active, input[type="submit"]:active, .submit-button:active, +.filter-btn:active, .create-button:active, .edit-btn:active, +.delete-btn:active, .join-btn:active, .leave-btn:active, .buy-btn:active { + filter: brightness(.85) !important; +} +.main-column a:active { opacity: .6 !important; } + +* { -webkit-tap-highlight-color: transparent !important; } +a, button, label, .tap, .oasis-menu-btn, .bb-item, .oasis-nav-header, .oasis-nav-list li a { + touch-action: manipulation !important; +} +body { overscroll-behavior: none !important; } + +.main-content, .main-column, .header, +.sidebar-left, .sidebar-right { overflow: visible !important; } + +.sidebar-left, .sidebar-right { display: none !important; } +#nav-left-toggle:checked ~ .main-content .sidebar-left { display: block !important; } +#nav-right-toggle:checked ~ .main-content .sidebar-right { display: block !important; } + +.bb-manage { margin: 8px 0 16px !important; } +.bb-manage h3, .bb-manage-add h3 { + font-size: 13px !important; text-transform: uppercase !important; letter-spacing: .05em !important; + color: #FFA500 !important; margin: 14px 0 8px !important; +} +.bb-manage-list { display: flex !important; flex-direction: column !important; gap: 8px !important; } +.bb-manage-form { margin: 0 !important; width: 100% !important; } +.bb-manage-btn { + width: 100% !important; + display: flex !important; align-items: center !important; gap: 12px !important; + min-height: 52px !important; padding: 0 14px !important; + background: #1c1c1c !important; border: 1px solid #333 !important; border-radius: 10px !important; + color: #eee !important; cursor: pointer !important; text-align: left !important; +} +.bb-manage-ico { font-size: 20px !important; color: #FFA500 !important; width: 26px !important; text-align: center !important; } +.bb-manage-lbl { flex: 1 1 auto !important; font-size: 15px !important; } +.bb-manage-x { font-size: 16px !important; color: #ff6b6b !important; font-weight: bold !important; } +.bb-manage-empty, .bb-manage-full { color: #999 !important; font-size: 14px !important; padding: 8px 2px !important; } +.bb-manage-done { + display: block !important; text-align: center !important; + margin-top: 16px !important; min-height: 48px !important; line-height: 48px !important; +} + +.hive-nav { --hx: min(64px, calc((100vw - 40px) / 4)); --hxm: min(58px, calc((100vw - 36px) / 5)); margin: 8px 0 16px !important; overflow: visible !important; background: transparent !important; padding: 0 !important; border: 0 !important; box-shadow: none !important; } +.hive-radio { position: absolute !important; opacity: 0 !important; width: 0 !important; height: 0 !important; pointer-events: none !important; } +.hive-grid { display: flex !important; flex-direction: column !important; align-items: center !important; overflow: visible !important; background: transparent !important; padding: 0 !important; margin: 0 !important; box-shadow: none !important; } +.hive-row { display: flex !important; justify-content: center !important; gap: 3px !important; overflow: visible !important; background: transparent !important; padding: 0 !important; margin: 0 !important; box-shadow: none !important; } +.hive-cats .hive-row + .hive-row { margin-top: calc(var(--hx) * -0.30) !important; } +.hex-cat, .hex-mod { + position: relative !important; + -webkit-clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%) !important; + clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%) !important; + display: flex !important; flex-direction: column !important; align-items: center !important; justify-content: center !important; gap: 2px !important; + text-decoration: none !important; cursor: pointer !important; box-sizing: border-box !important; + background: #D9A400 !important; + filter: drop-shadow(0 1px 2px rgba(0,0,0,.6)) !important; +} +.hex-cat::before, .hex-mod::before { + content: '' !important; position: absolute !important; inset: 1.5px !important; z-index: 0 !important; + -webkit-clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%) !important; + clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%) !important; + background: #161616 !important; +} +.hex-cat { width: var(--hx) !important; height: calc(var(--hx) * 1.15) !important; } +.hex-mod { width: var(--hxm) !important; height: calc(var(--hxm) * 1.15) !important; } +.hex-cat .hx-ico, .hex-mod .hx-ico { color: #FFA500 !important; line-height: 1 !important; position: relative !important; z-index: 1 !important; } +.hex-cat .hx-ico { font-size: 17px !important; } +.hex-mod .hx-ico { font-size: 16px !important; } +.hex-cat .hx-lbl, .hex-mod .hx-lbl { color: #d8d8d8 !important; text-align: center !important; line-height: 1 !important; font-weight: bold !important; overflow: hidden !important; position: relative !important; z-index: 1 !important; } +.hex-cat .hx-lbl { font-size: 8px !important; max-width: 88% !important; } +.hex-mod .hx-lbl { font-size: 8px !important; max-width: 92% !important; } +.hex-cat:active::before, .hex-mod:active::before { background: #242424 !important; } +#hive-personal:checked ~ .hive-grid .hex-cat[for="hive-personal"], +#hive-governance:checked ~ .hive-grid .hex-cat[for="hive-governance"], +#hive-office:checked ~ .hive-grid .hex-cat[for="hive-office"], +#hive-economy:checked ~ .hive-grid .hex-cat[for="hive-economy"], +#hive-tools:checked ~ .hive-grid .hex-cat[for="hive-tools"], +#hive-network:checked ~ .hive-grid .hex-cat[for="hive-network"], +#hive-blogs:checked ~ .hive-grid .hex-cat[for="hive-blogs"], +#hive-creative:checked ~ .hive-grid .hex-cat[for="hive-creative"], +#hive-media:checked ~ .hive-grid .hex-cat[for="hive-media"], +#hive-fediverse:checked ~ .hive-grid .hex-cat[for="hive-fediverse"] { background: #FFA500 !important; } +#hive-personal:checked ~ .hive-grid .hex-cat[for="hive-personal"]::before, +#hive-governance:checked ~ .hive-grid .hex-cat[for="hive-governance"]::before, +#hive-office:checked ~ .hive-grid .hex-cat[for="hive-office"]::before, +#hive-economy:checked ~ .hive-grid .hex-cat[for="hive-economy"]::before, +#hive-tools:checked ~ .hive-grid .hex-cat[for="hive-tools"]::before, +#hive-network:checked ~ .hive-grid .hex-cat[for="hive-network"]::before, +#hive-blogs:checked ~ .hive-grid .hex-cat[for="hive-blogs"]::before, +#hive-creative:checked ~ .hive-grid .hex-cat[for="hive-creative"]::before, +#hive-media:checked ~ .hive-grid .hex-cat[for="hive-media"]::before, +#hive-fediverse:checked ~ .hive-grid .hex-cat[for="hive-fediverse"]::before { background: #FFA500 !important; } +#hive-personal:checked ~ .hive-grid .hex-cat[for="hive-personal"] :is(.hx-ico,.hx-lbl), +#hive-governance:checked ~ .hive-grid .hex-cat[for="hive-governance"] :is(.hx-ico,.hx-lbl), +#hive-office:checked ~ .hive-grid .hex-cat[for="hive-office"] :is(.hx-ico,.hx-lbl), +#hive-economy:checked ~ .hive-grid .hex-cat[for="hive-economy"] :is(.hx-ico,.hx-lbl), +#hive-tools:checked ~ .hive-grid .hex-cat[for="hive-tools"] :is(.hx-ico,.hx-lbl), +#hive-network:checked ~ .hive-grid .hex-cat[for="hive-network"] :is(.hx-ico,.hx-lbl), +#hive-blogs:checked ~ .hive-grid .hex-cat[for="hive-blogs"] :is(.hx-ico,.hx-lbl), +#hive-creative:checked ~ .hive-grid .hex-cat[for="hive-creative"] :is(.hx-ico,.hx-lbl), +#hive-media:checked ~ .hive-grid .hex-cat[for="hive-media"] :is(.hx-ico,.hx-lbl), +#hive-fediverse:checked ~ .hive-grid .hex-cat[for="hive-fediverse"] :is(.hx-ico,.hx-lbl) { color: #121212 !important; } +.hive-panel { display: none !important; margin: 12px auto 0 !important; max-width: 340px !important; background: #101010 !important; border: 1px solid #242424 !important; border-radius: 16px !important; padding: 12px 8px !important; overflow: visible !important; box-shadow: none !important; } +#hive-personal:checked ~ #hive-panel-personal, +#hive-governance:checked ~ #hive-panel-governance, +#hive-office:checked ~ #hive-panel-office, +#hive-economy:checked ~ #hive-panel-economy, +#hive-tools:checked ~ #hive-panel-tools, +#hive-network:checked ~ #hive-panel-network, +#hive-blogs:checked ~ #hive-panel-blogs, +#hive-creative:checked ~ #hive-panel-creative, +#hive-media:checked ~ #hive-panel-media, +#hive-fediverse:checked ~ #hive-panel-fediverse { display: block !important; } +.hive-mods { display: flex !important; flex-wrap: wrap !important; justify-content: center !important; gap: 4px !important; margin: 0 !important; padding: 0 !important; background: transparent !important; overflow: visible !important; } + +.header .top-bar-left nav, +.header .top-bar-right nav { display: none !important; } + +.oasis-mobile-topbar { padding-top: calc(env(safe-area-inset-top, 0px) + 10px) !important; } + +.oasis-bottombar .bb-pair { + display: flex !important; align-items: center !important; + flex: 0 0 auto !important; + gap: 2px !important; padding: 3px !important; + background: #262626 !important; + border: 1px solid #3a3a3a !important; + border-radius: 999px !important; +} +.oasis-bottombar .bb-pair .bb-item { flex: 0 0 auto !important; padding: 2px 8px !important; } +.oasis-bottombar .bb-pair .bb-lbl { font-size: 8px !important; } + +.oasis-bottombar a.bb-item.current .bb-ico { + background: #FFA500 !important; color: #121212 !important; + border-radius: 999px !important; padding: 4px 12px !important; +} +.oasis-bottombar a.bb-item.current .bb-lbl { color: #FFA500 !important; font-weight: bold !important; } +.oasis-bottombar a.bb-item.current .bb-badge { background: #121212 !important; color: #FFA500 !important; } + +.main-column, .main-column p, .main-column li, .main-column a, +.main-column span, .main-column td, .main-column th, .main-column label, +.main-column h1, .main-column h2, .main-column h3, .main-column h4, +.main-column strong, .main-column em, .main-column small, +.created-at, .post-meta, .feed-post-meta, +.feed-row .small, .feed-row .time, .feed-row .created-at, +.profile-sensors-box > *, +.card-field .card-value, .card-field .card-label { + overflow-wrap: break-word !important; + word-break: normal !important; + -webkit-hyphens: none !important; + hyphens: none !important; +} + +.main-column pre, .main-column code, kbd, samp, +.peer-key, .user-link.peer-key, .oasis-id-copy, .search-oasis-id, +.pm-sent-key-value, .kpi__value, +.block-info-table td .peer-key, .block-info-table td .user-link, +.pm-info-table td .user-link, +.main-column a[href*="ed25519"], .main-column a[href^="ssb:"], +.main-column a[href^="/author/"] { + overflow-wrap: anywhere !important; + word-break: break-all !important; +} + +button, input[type="submit"], input[type="button"], +.filter-btn, a.filter-btn, .create-button, .edit-btn, .delete-btn, +.join-btn, .leave-btn, .buy-btn, .submit-button, .btn, +.conn-actions button, .peers-conn-actions button, +.mode-buttons button, .mode-buttons .filter-btn, +.filter-group button, .filter-group .filter-btn, +.main-column button, .main-column input[type="submit"] { + white-space: normal !important; + word-break: normal !important; + overflow-wrap: break-word !important; + -webkit-hyphens: none !important; + hyphens: none !important; +} + +.conn-actions, .peers-conn-actions { + display: flex !important; + flex-direction: row !important; + flex-wrap: wrap !important; + justify-content: center !important; + align-items: stretch !important; + gap: 8px !important; + width: 100% !important; + max-width: 100% !important; + overflow-x: hidden !important; +} +.conn-actions > form, .peers-conn-actions > form { + flex: 0 1 auto !important; + width: auto !important; + min-width: 0 !important; + margin: 0 !important; + display: flex !important; +} + +.pm-info-table, .pm-info-table tbody, +.block-info-table, .block-info-table tbody { + display: block !important; + width: 100% !important; + max-width: 100% !important; + border: 0 !important; + background: transparent !important; + box-shadow: none !important; + table-layout: auto !important; +} +.pm-info-table thead, .block-info-table thead { display: none !important; } +.pm-info-table tr, .block-info-table tr { + display: block !important; + width: 100% !important; + box-sizing: border-box !important; + border: 0 !important; + border-bottom: 1px solid rgba(255,165,0,0.18) !important; + border-radius: 0 !important; + margin: 0 0 8px 0 !important; + padding: 6px 0 !important; + background: transparent !important; +} +.pm-info-table td, .block-info-table td { + display: block !important; + width: 100% !important; + max-width: 100% !important; + box-sizing: border-box !important; + border: 0 !important; + background: transparent !important; + padding: 1px 0 !important; + text-align: left !important; + overflow-wrap: break-word !important; + word-break: normal !important; +} + +.pm-info-table td.card-label, .block-info-table td.card-label { + color: var(--c-muted, #8c8c8c) !important; + font-size: 11px !important; + font-weight: 700 !important; + text-transform: uppercase !important; + letter-spacing: .03em !important; + margin-bottom: 2px !important; + padding-bottom: 0 !important; +} +.pm-info-table td.card-value, .block-info-table td.card-value { + font-size: 15px !important; + line-height: 1.5 !important; + margin-bottom: 6px !important; +} + +.block-info-table tr:first-child:has(> td.card-label) { display: none !important; } +.block-info-table td[data-label]:not([data-label=""])::before { + content: attr(data-label); + display: block !important; + color: var(--c-muted, #8c8c8c) !important; + font-size: 10px !important; + font-weight: 700 !important; + text-transform: uppercase !important; + letter-spacing: .03em !important; + margin-bottom: 1px !important; + white-space: nowrap !important; + word-break: normal !important; +} +.block-info-table td[data-label=""]::before { content: none !important; } + +.peers-list .block-info-table td, +.peers-technical-block .block-info-table td, +.gov-overview tbody td { + word-break: normal !important; + overflow-wrap: break-word !important; +} + +.pm-info-table td .user-link, .pm-info-table td .peer-key, .pm-info-table td a[href], +.block-info-table td .peer-key, .block-info-table td .user-link, .block-info-table td a[href] { + display: inline-block !important; + max-width: 100% !important; + overflow-wrap: anywhere !important; + word-break: break-all !important; +} + +.pm-info-table td form, .block-info-table td form, +.pm-info-table td button, .block-info-table td button { + width: 100% !important; + margin: 0 !important; +} + +.main-content { width: 100% !important; max-width: 100% !important; } +.main-column { + width: 100% !important; max-width: 100% !important; + padding: 16px !important; box-sizing: border-box !important; +} +.main-column .card-field { + flex-direction: column !important; + align-items: stretch !important; + gap: 2px !important; +} +.main-column .card-field .card-value { + width: 100% !important; max-width: 100% !important; font-size: 15px !important; +} +.main-column .card-field .card-label { + color: var(--c-muted, #8c8c8c) !important; font-size: 11px !important; +} +.main-column img, .main-column video, .main-column iframe, .main-column canvas { + max-width: 100% !important; height: auto !important; +} +.main-column hr { + width: 100% !important; border: 0 !important; + border-top: 1px solid #333 !important; margin: 16px 0 !important; +} + +.activity-filter-acc { width: 100% !important; margin-bottom: 8px !important; } + +.activity-quick-modes { + display: flex !important; flex-wrap: nowrap !important; gap: 6px !important; margin-bottom: 8px !important; +} +.activity-quick-modes form { flex: 1 1 0 !important; min-width: 0 !important; margin: 0 !important; display: flex !important; } +.activity-quick-modes .filter-btn { + flex: 1 1 0 !important; width: 100% !important; min-height: 44px !important; + white-space: nowrap !important; word-break: keep-all !important; overflow-wrap: normal !important; + padding-left: 6px !important; padding-right: 6px !important; font-size: 14px !important; +} + +.activity-groups { list-style: none !important; padding: 0 !important; margin: 0 !important; } + +.activity-groups .oasis-nav-header .emoji { display: none !important; } +.activity-groups .oasis-nav-header .nav-text { flex: 1 1 auto !important; } +.activity-groups .act-count { + flex: 0 0 auto !important; min-width: 20px !important; text-align: center !important; + font-size: 11px !important; font-weight: 700 !important; color: #121212 !important; + background: #FFA500 !important; border-radius: 999px !important; padding: 1px 7px !important; + margin: 0 8px 0 0 !important; +} + +.activity-groups .oasis-nav-list li { list-style: none !important; margin: 0 !important; padding: 0 !important; } +.activity-groups .oasis-nav-list form { margin: 0 !important; display: block !important; width: 100% !important; } +.activity-groups .oasis-nav-list .filter-btn { + display: block !important; width: 100% !important; text-align: left !important; + min-height: 44px !important; margin: 2px 0 !important; box-sizing: border-box !important; +} +.activity-groups .oasis-nav-list .filter-btn.active { + outline: 2px solid #FFA500 !important; outline-offset: -2px !important; +} + +.message-list { display: flex !important; flex-direction: column !important; gap: 4px !important; width: 100% !important; } +.message-list .pm-thread { display: flex !important; flex-direction: column !important; width: 100% !important; align-self: stretch !important; } +.pm-thread-replies { display: flex !important; flex-direction: column !important; gap: 4px !important; } + +.pm-card.pm-bubble { + max-width: 82% !important; width: fit-content !important; min-width: 0 !important; + box-sizing: border-box !important; padding: 8px 12px !important; margin: 2px 0 !important; + border-radius: 16px !important; border: 1px solid #333 !important; + background: #1e1e1e !important; box-shadow: none !important; cursor: default !important; +} + +.pm-card.pm-bubble.pm-recv { + align-self: flex-start !important; margin-left: 0 !important; margin-right: auto !important; + border-bottom-left-radius: 4px !important; background: #1e1e1e !important; border-color: #343434 !important; +} + +.pm-card.pm-bubble.pm-sent { + align-self: flex-end !important; margin-left: auto !important; margin-right: 0 !important; + border-bottom-right-radius: 4px !important; background: #2a2413 !important; border-color: #5c4c1f !important; +} + +.pm-bubble .pm-bubble-sender { font-size: 11px !important; font-weight: 700 !important; color: #FFA500 !important; margin-bottom: 3px !important; } +.pm-bubble .pm-bubble-sender a { color: #FFA500 !important; } + +.pm-bubble .message-text { font-size: 15px !important; line-height: 1.42 !important; color: #ececec !important; margin: 0 !important; } +.pm-bubble .message-text a { color: #FFD700 !important; } + +.pm-bubble .pm-bubble-meta { + display: flex !important; flex-wrap: wrap !important; align-items: center !important; + gap: 6px !important; margin-top: 4px !important; font-size: 10px !important; color: var(--c-muted, #8c8c8c) !important; +} +.pm-card.pm-sent .pm-bubble-meta { justify-content: flex-end !important; } +.pm-card.pm-recv .pm-bubble-meta { justify-content: flex-start !important; } +.pm-bubble .pm-bubble-subject { font-style: italic !important; opacity: .75 !important; max-width: 50% !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } +.pm-bubble .pm-bubble-lock { opacity: .85 !important; } +.pm-bubble .pm-bubble-eco { font-size: 9px !important; opacity: .9 !important; } +.pm-bubble .pm-bubble-eco * { font-size: 9px !important; } + +.pm-bubble .pm-actions { margin-top: 6px !important; display: flex !important; gap: 6px !important; } +.pm-card.pm-sent .pm-actions { justify-content: flex-end !important; } +.pm-bubble .pm-actions .pm-btn { font-size: 10px !important; padding: 3px 8px !important; min-height: 28px !important; } +.pm-bubble .pm-actions form { margin: 0 !important; } + +.pm-bubble .pm-info-table { display: none !important; } + +.pm-thread .pm-thread-details { width: 100% !important; } +.pm-thread .pm-thread-toggle { font-size: 12px !important; color: var(--c-muted, #8c8c8c) !important; padding: 4px 0 !important; cursor: pointer !important; } + +.main-column h1, .main-column h2, .main-column h3, .main-column h4, +.tags-header h2, .tags-header h3, .title-with-chip h2, .pm-title, h2.pm-title, +.cn-name, h1.cn-name, .cn-section, h2.cn-section, .profile-side-name, h2.profile-side-name { + text-wrap: balance !important; + overflow-wrap: break-word !important; + word-break: keep-all !important; + -webkit-hyphens: none !important; hyphens: none !important; + text-align: left !important; +} +.main-column h1, .tags-header h2, .cn-name { + font-size: var(--fs-title, 20px) !important; font-weight: 700 !important; + line-height: 1.22 !important; letter-spacing: -0.01em !important; + margin: 0 0 var(--space-2, 8px) !important; +} +.main-column h2, .pm-title, .title-with-chip h2, .cn-section { + font-size: 18px !important; font-weight: 700 !important; + line-height: 1.28 !important; margin: var(--space-1, 4px) 0 var(--space-2, 8px) !important; +} +.main-column h3 { + font-size: var(--fs-body-lg, 16px) !important; font-weight: 600 !important; + line-height: 1.35 !important; margin: var(--space-1, 4px) 0 !important; +} +.main-column h1, .main-column h2, .main-column h3, +.tags-header h2, .pm-title, .cn-name, .cn-section { + max-width: 40ch !important; width: auto !important; min-width: 0 !important; +} + +.main-column h1 code, .main-column h2 code, .main-column h3 code, +.tags-header h2 code, .pm-title code, +.main-column h1 a[href*="ed25519"], .main-column h2 a[href*="ed25519"], +.main-column h1 a[href^="ssb:"], .main-column h2 a[href^="ssb:"] { + word-break: break-all !important; overflow-wrap: anywhere !important; +} + +.main-column .tags-header { + display: flex !important; flex-direction: column !important; align-items: flex-start !important; + gap: var(--space-1, 4px) !important; width: 100% !important; max-width: 100% !important; + margin: 0 0 var(--space-2, 8px) !important; +} +.main-column .tags-header h2 { margin: 0 !important; } + +.main-column .title-with-chip { + display: flex !important; flex-wrap: wrap !important; align-items: center !important; + gap: var(--space-2, 8px) !important; width: 100% !important; max-width: 100% !important; +} +.main-column .title-with-chip h2 { flex: 1 1 auto !important; min-width: 0 !important; margin: 0 !important; max-width: none !important; } +.main-column .title-with-chip > :not(h2) { flex: 0 0 auto !important; } + +.main-column p, .main-column li, +.tags-header p, .tags-header .prefs-help, .indexing-note, .indexing-percent, +.error-page-message, .welcome-text, .message-text, .pm-bubble .message-text { + text-wrap: pretty !important; + line-height: 1.55 !important; + max-width: min(66ch, 100%) !important; + overflow-wrap: anywhere !important; + word-break: normal !important; + min-width: 0 !important; + -webkit-hyphens: none !important; hyphens: none !important; +} +.main-column p + p { margin-top: var(--space-3, 16px) !important; } +.tags-header p, .tags-header .prefs-help, .indexing-note { line-height: 1.5 !important; margin: 4px 0 0 0 !important; } +.pm-bubble .message-text { line-height: 1.45 !important; max-width: 100% !important; } + +[style*="auto-fill"], +[style*="minmax(240px"], [style*="minmax(280px"], [style*="minmax(300px"], +[style*="minmax( 240px"], [style*="minmax( 280px"], [style*="minmax( 300px"], +.trending-container, .cn-hub-grid, .cn-products { + display: grid !important; grid-template-columns: 1fr !important; gap: 12px !important; + width: 100% !important; max-width: 100% !important; +} +.trending-container > *, .games-grid > *, .market-grid > *, .tribe-grid > *, +.feed-container, .feed-card, .trending-card, +.main-column article, .main-column [class*="-card"], .main-column [class*="-item"], +.main-column [class*="-entry"], .main-column .card { + width: 100% !important; max-width: 100% !important; min-width: 0 !important; + box-sizing: border-box !important; margin-left: 0 !important; margin-right: 0 !important; +} +.feed-card .feed-text, .trending-card .feed-text, +.main-column article p, .main-column [class*="-card"] p { max-width: var(--measure, 66ch) !important; } +.main-column form, .comment-form, .comment-form-wrapper, .market-form, +.new-message-form, .project-control-form, .filter-box { + display: block !important; width: 100% !important; max-width: 100% !important; + min-width: 0 !important; box-sizing: border-box !important; +} +.main-column input:not([type="checkbox"]):not([type="radio"]):not(.oasis-nav-toggle):not(.oasis-nav-side-toggle), +.main-column textarea, .main-column select { + width: 100% !important; max-width: 100% !important; min-width: 0 !important; box-sizing: border-box !important; +} +.main-column form > button:only-of-type, .main-column form > .create-button, +.comment-form button[type="submit"], .market-form button[type="submit"], +.new-message-form button[type="submit"] { + display: block !important; width: 100% !important; max-width: 100% !important; + box-sizing: border-box !important; margin-top: 8px !important; +} +[style*="width:33%"], [style*="width: 33%"], [style*="width:25%"], [style*="width: 25%"], +[style*="width:60%"], [style*="width: 60%"], [style*="width:40%"], [style*="width: 40%"] { + width: 100% !important; max-width: 100% !important; +} +.main-column [style*="max-width: 800px"], .main-column [style*="max-width:800px"], +.ai-response, .ai-train-bar { + max-width: 100% !important; width: 100% !important; margin-left: 0 !important; margin-right: 0 !important; box-sizing: border-box !important; +} +.message-list, .pm-thread, .pm-thread-replies { width: 100% !important; max-width: 100% !important; box-sizing: border-box !important; } + +.main-column p .user-link, .main-column p a[href^="/author/"], +.main-column li .user-link, .main-column span .user-link, +.main-column .message-text .user-link, .main-column .message-text a[href^="/author/"], +.pm-bubble .message-text .user-link, .pm-bubble .message-text a[href^="/author/"], +.pm-quote a[href^="/author/"] { + display: inline !important; white-space: normal !important; + overflow-wrap: anywhere !important; word-break: keep-all !important; + -webkit-hyphens: none !important; hyphens: none !important; +} + +.peer-key, .user-link.peer-key, .block-info-table td .peer-key, .pm-info-table td .peer-key, +.pm-info-table td .user-link, .block-info-table td .user-link, +.search-oasis-id, .pm-sent-key-value, .kpi__value { + display: inline-block !important; max-width: 100% !important; + overflow-wrap: anywhere !important; word-break: break-all !important; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace !important; + font-variant-numeric: tabular-nums !important; line-height: 1.45 !important; +} + +.oasis-id-copy { + display: block !important; width: 100% !important; white-space: nowrap !important; + overflow: hidden !important; text-overflow: ellipsis !important; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace !important; +} +.invites-list .user-link.peer-key, .peers-list .block-info-table td .peer-key { + display: block !important; max-width: 100% !important; white-space: nowrap !important; + overflow: hidden !important; text-overflow: ellipsis !important; word-break: normal !important; +} + +.main-column .message-text a[href^="http"], .main-column p a[href^="http"], +.pm-bubble .message-text a[href^="http"], .main-column .message-text a.oasis-path-link, +.main-column .message-text a.job-link, .main-column .message-text a.project-link, +.main-column .message-text a.market-link { + overflow-wrap: anywhere !important; word-break: keep-all !important; + -webkit-hyphens: none !important; hyphens: none !important; +} +.main-column code:not(pre code), .pm-bubble .message-text code { + display: inline !important; max-width: 100% !important; + overflow-wrap: anywhere !important; word-break: break-word !important; white-space: pre-wrap !important; +} + +.main-column .tags-header { margin-bottom: 6px !important; gap: 2px !important; } +.main-column .tags-header h2, .tags-header h2 { + font-size: 17px !important; line-height: 1.2 !important; letter-spacing: 0 !important; +} +.main-column .tags-header p { font-size: 13px !important; line-height: 1.35 !important; margin: 0 !important; } + +.main-column { padding: 10px 12px 16px 12px !important; } +.activity-filter-acc { margin-bottom: 4px !important; } + +.empty-state { + display: flex !important; flex-direction: column !important; align-items: center !important; + justify-content: center !important; text-align: center !important; + padding: 40px 16px !important; gap: 8px !important; width: 100% !important; box-sizing: border-box !important; +} +.empty-state-icon { font-size: 44px !important; line-height: 1 !important; opacity: 0.55 !important; } +.empty-state-title { font-size: 16px !important; font-weight: 700 !important; margin: 4px 0 0 0 !important; color: #eee !important; } +.empty-state-text { color: var(--c-muted, #8c8c8c) !important; font-size: 14px !important; line-height: 1.4 !important; max-width: 40ch !important; margin: 0 !important; } +.empty-state-cta { margin-top: 10px !important; min-height: 48px !important; display: inline-flex !important; align-items: center !important; } + +.activity-filter-collapsed { + display: flex !important; align-items: center !important; gap: 10px !important; + flex-wrap: wrap !important; margin-bottom: 8px !important; width: 100% !important; +} +.activity-back { + display: inline-flex !important; align-items: center !important; gap: 4px !important; + min-height: 44px !important; padding: 6px 14px 6px 10px !important; flex: 0 0 auto !important; +} +.activity-back-arrow { font-size: 22px !important; line-height: 1 !important; font-weight: bold !important; } +.activity-active-filter { + font-weight: 700 !important; color: #FFA500 !important; text-transform: uppercase !important; + font-size: 13px !important; letter-spacing: .03em !important; +} + +.oasis-bottombar .bb-item { min-height: 48px !important; } +.oasis-bottombar .bb-pair .bb-item { min-height: 44px !important; padding: 4px 10px !important; } +.oasis-mobile-menus .oasis-menu-btn { min-height: 48px !important; } +.pm-bubble .pm-actions .pm-btn { min-height: 40px !important; padding: 6px 12px !important; } +.score-btn, .vote-btn, .feed-action-btn { + min-height: 44px !important; min-width: 44px !important; + display: inline-flex !important; align-items: center !important; justify-content: center !important; +} +.score-btn:active, .vote-btn:active, .feed-action-btn:active { filter: brightness(.85) !important; } + +.oasis-bottombar .bb-add .bb-ico { + background: #FFA500 !important; color: #121212 !important; border-radius: 999px !important; + width: 30px !important; height: 30px !important; display: inline-flex !important; + align-items: center !important; justify-content: center !important; + font-weight: bold !important; font-size: 20px !important; line-height: 1 !important; +} +.bb-pick-grid { + display: grid !important; grid-template-columns: repeat(3, 1fr) !important; + gap: 8px !important; width: 100% !important; margin-top: 8px !important; +} +.bb-pick-form { margin: 0 !important; display: block !important; } +.bb-pick-btn { + display: flex !important; flex-direction: column !important; align-items: center !important; + justify-content: center !important; gap: 6px !important; width: 100% !important; + min-height: 76px !important; padding: 10px 6px !important; box-sizing: border-box !important; + background: #1c1c1c !important; border: 1px solid #333 !important; border-radius: 12px !important; color: #eee !important; +} +.bb-pick-btn.active { border-color: #FFA500 !important; background: #2a2412 !important; } +.bb-pick-ico { font-size: 26px !important; color: #FFA500 !important; line-height: 1 !important; } +.bb-pick-lbl { font-size: 11px !important; text-align: center !important; line-height: 1.2 !important; } + diff --git a/src/client/assets/translations/oasis_ar.js b/src/client/assets/translations/oasis_ar.js index af7e1dd..3319b82 100644 --- a/src/client/assets/translations/oasis_ar.js +++ b/src/client/assets/translations/oasis_ar.js @@ -1,6 +1,20 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { ar: { + bbAdd: "إضافة", + bbQuickActions: "إجراءات سريعة", + bbPickTitle: "تثبيت وحدة", + bbPickDesc: "اختر وحدة لخانة + في الشريط السفلي.", + bbPickNone: "لا شيء", + bbManage: "تعديل", + bbYourBar: "شريطك", + bbRemove: "إزالة", + bbFull: "تم بلوغ الحد الأقصى", + bbDone: "تم", + activityChangeFilter: "تغيير التصفية", + emptyConnectHint: "اتصل بـ pub للمزامنة مع الشبكة.", + emptyConnectCta: "الاتصال بـ pub", + menuCommunity: "المجتمع", activityGroupCommunity: "المجتمع والحوكمة", activityGroupOrg: "التنظيم", activityGroupComm: "التواصل", diff --git a/src/client/assets/translations/oasis_de.js b/src/client/assets/translations/oasis_de.js index 4ce472a..0cc9ae8 100644 --- a/src/client/assets/translations/oasis_de.js +++ b/src/client/assets/translations/oasis_de.js @@ -1,6 +1,20 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { de: { + bbAdd: "Hinzufügen", + bbQuickActions: "Schnellaktionen", + bbPickTitle: "Modul anheften", + bbPickDesc: "Wähle ein Modul für den +-Platz in der unteren Leiste.", + bbPickNone: "Keins", + bbManage: "Bearbeiten", + bbYourBar: "Deine Leiste", + bbRemove: "Entfernen", + bbFull: "Maximum erreicht", + bbDone: "Fertig", + activityChangeFilter: "Filter ändern", + emptyConnectHint: "Verbinde dich mit einem Pub, um dich mit dem Netzwerk zu synchronisieren.", + emptyConnectCta: "Mit einem Pub verbinden", + menuCommunity: "Gemeinschaft", activityGroupCommunity: "Gemeinschaft & Governance", activityGroupOrg: "Organisation", activityGroupComm: "Kommunikation", diff --git a/src/client/assets/translations/oasis_en.js b/src/client/assets/translations/oasis_en.js index ae5e327..e14c720 100644 --- a/src/client/assets/translations/oasis_en.js +++ b/src/client/assets/translations/oasis_en.js @@ -1,6 +1,20 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { en: { + bbAdd: "Add", + bbQuickActions: "Quick actions", + bbPickTitle: "Pin a module", + bbPickDesc: "Choose a module for the + slot in the bottom bar.", + bbPickNone: "None", + bbManage: "Edit", + bbYourBar: "Your bar", + bbRemove: "Remove", + bbFull: "Maximum reached", + bbDone: "Done", + activityChangeFilter: "Change filter", + emptyConnectHint: "Connect to a pub to sync with the network.", + emptyConnectCta: "Connect to a pub", + menuCommunity: "Community", activityGroupCommunity: "Community & Governance", activityGroupOrg: "Organization", activityGroupComm: "Communication", diff --git a/src/client/assets/translations/oasis_es.js b/src/client/assets/translations/oasis_es.js index 46f3f48..0faa594 100644 --- a/src/client/assets/translations/oasis_es.js +++ b/src/client/assets/translations/oasis_es.js @@ -1,6 +1,20 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { es: { + bbAdd: "Añadir", + bbQuickActions: "Acciones rápidas", + bbPickTitle: "Fijar un módulo", + bbPickDesc: "Elige un módulo para el hueco + de la barra inferior.", + bbPickNone: "Ninguno", + bbManage: "Editar", + bbYourBar: "Tu barra", + bbRemove: "Quitar", + bbFull: "Máximo alcanzado", + bbDone: "Hecho", + activityChangeFilter: "Cambiar filtro", + emptyConnectHint: "Conéctate a un pub para sincronizar con la red.", + emptyConnectCta: "Conéctate a un pub", + menuCommunity: "Comunidad", activityGroupCommunity: "Comunidad y Gobernanza", activityGroupOrg: "Organización", activityGroupComm: "Comunicación", diff --git a/src/client/assets/translations/oasis_eu.js b/src/client/assets/translations/oasis_eu.js index 251748d..f67b698 100644 --- a/src/client/assets/translations/oasis_eu.js +++ b/src/client/assets/translations/oasis_eu.js @@ -1,6 +1,20 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { eu: { + bbAdd: "Gehitu", + bbQuickActions: "Ekintza azkarrak", + bbPickTitle: "Ainguratu modulu bat", + bbPickDesc: "Aukeratu modulu bat beheko barrako + hutsunerako.", + bbPickNone: "Bat ere ez", + bbManage: "Editatu", + bbYourBar: "Zure barra", + bbRemove: "Kendu", + bbFull: "Gehienekora iritsi da", + bbDone: "Eginda", + activityChangeFilter: "Aldatu iragazkia", + emptyConnectHint: "Konektatu pub batera sarearekin sinkronizatzeko.", + emptyConnectCta: "Konektatu pub batera", + menuCommunity: "Komunitatea", activityGroupCommunity: "Komunitatea eta gobernantza", activityGroupOrg: "Antolakuntza", activityGroupComm: "Komunikazioa", diff --git a/src/client/assets/translations/oasis_fr.js b/src/client/assets/translations/oasis_fr.js index 3d6f340..af3da9d 100644 --- a/src/client/assets/translations/oasis_fr.js +++ b/src/client/assets/translations/oasis_fr.js @@ -1,6 +1,20 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { fr: { + bbAdd: "Ajouter", + bbQuickActions: "Actions rapides", + bbPickTitle: "Épingler un module", + bbPickDesc: "Choisis un module pour l'emplacement + de la barre inférieure.", + bbPickNone: "Aucun", + bbManage: "Modifier", + bbYourBar: "Ta barre", + bbRemove: "Retirer", + bbFull: "Maximum atteint", + bbDone: "Terminé", + activityChangeFilter: "Changer de filtre", + emptyConnectHint: "Connecte-toi à un pub pour synchroniser avec le réseau.", + emptyConnectCta: "Se connecter à un pub", + menuCommunity: "Communauté", activityGroupCommunity: "Communauté et gouvernance", activityGroupOrg: "Organisation", activityGroupComm: "Communication", diff --git a/src/client/assets/translations/oasis_hi.js b/src/client/assets/translations/oasis_hi.js index 0108885..4b66a12 100644 --- a/src/client/assets/translations/oasis_hi.js +++ b/src/client/assets/translations/oasis_hi.js @@ -1,6 +1,20 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { hi: { + bbAdd: "जोड़ें", + bbQuickActions: "त्वरित क्रियाएँ", + bbPickTitle: "एक मॉड्यूल पिन करें", + bbPickDesc: "नीचे की बार में + स्लॉट के लिए एक मॉड्यूल चुनें।", + bbPickNone: "कोई नहीं", + bbManage: "संपादित करें", + bbYourBar: "आपकी बार", + bbRemove: "हटाएँ", + bbFull: "अधिकतम पहुँच गया", + bbDone: "हो गया", + activityChangeFilter: "फ़िल्टर बदलें", + emptyConnectHint: "नेटवर्क से सिंक करने के लिए किसी pub से जुड़ें।", + emptyConnectCta: "pub से जुड़ें", + menuCommunity: "समुदाय", activityGroupCommunity: "समुदाय और शासन", activityGroupOrg: "संगठन", activityGroupComm: "संचार", diff --git a/src/client/assets/translations/oasis_it.js b/src/client/assets/translations/oasis_it.js index fcd8d21..a71a93f 100644 --- a/src/client/assets/translations/oasis_it.js +++ b/src/client/assets/translations/oasis_it.js @@ -1,6 +1,20 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { it: { + bbAdd: "Aggiungi", + bbQuickActions: "Azioni rapide", + bbPickTitle: "Fissa un modulo", + bbPickDesc: "Scegli un modulo per lo spazio + nella barra inferiore.", + bbPickNone: "Nessuno", + bbManage: "Modifica", + bbYourBar: "La tua barra", + bbRemove: "Rimuovi", + bbFull: "Massimo raggiunto", + bbDone: "Fatto", + activityChangeFilter: "Cambia filtro", + emptyConnectHint: "Connettiti a un pub per sincronizzarti con la rete.", + emptyConnectCta: "Connettiti a un pub", + menuCommunity: "Comunità", activityGroupCommunity: "Comunità e governance", activityGroupOrg: "Organizzazione", activityGroupComm: "Comunicazione", diff --git a/src/client/assets/translations/oasis_pt.js b/src/client/assets/translations/oasis_pt.js index 0a84578..07a52ad 100644 --- a/src/client/assets/translations/oasis_pt.js +++ b/src/client/assets/translations/oasis_pt.js @@ -1,6 +1,20 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { pt: { + bbAdd: "Adicionar", + bbQuickActions: "Ações rápidas", + bbPickTitle: "Fixar um módulo", + bbPickDesc: "Escolhe um módulo para o espaço + da barra inferior.", + bbPickNone: "Nenhum", + bbManage: "Editar", + bbYourBar: "Sua barra", + bbRemove: "Remover", + bbFull: "Máximo atingido", + bbDone: "Concluído", + activityChangeFilter: "Mudar filtro", + emptyConnectHint: "Liga-te a um pub para sincronizar com a rede.", + emptyConnectCta: "Ligar a um pub", + menuCommunity: "Comunidade", activityGroupCommunity: "Comunidade e governança", activityGroupOrg: "Organização", activityGroupComm: "Comunicação", diff --git a/src/client/assets/translations/oasis_ru.js b/src/client/assets/translations/oasis_ru.js index a69a349..d422c1d 100644 --- a/src/client/assets/translations/oasis_ru.js +++ b/src/client/assets/translations/oasis_ru.js @@ -1,6 +1,20 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { ru: { + bbAdd: "Добавить", + bbQuickActions: "Быстрые действия", + bbPickTitle: "Закрепить модуль", + bbPickDesc: "Выберите модуль для слота + в нижней панели.", + bbPickNone: "Нет", + bbManage: "Изменить", + bbYourBar: "Ваша панель", + bbRemove: "Убрать", + bbFull: "Достигнут максимум", + bbDone: "Готово", + activityChangeFilter: "Сменить фильтр", + emptyConnectHint: "Подключитесь к пабу для синхронизации с сетью.", + emptyConnectCta: "Подключиться к пабу", + menuCommunity: "Сообщество", activityGroupCommunity: "Сообщество и управление", activityGroupOrg: "Организация", activityGroupComm: "Коммуникация", diff --git a/src/client/assets/translations/oasis_zh.js b/src/client/assets/translations/oasis_zh.js index 8ea0cf1..18367fd 100644 --- a/src/client/assets/translations/oasis_zh.js +++ b/src/client/assets/translations/oasis_zh.js @@ -1,6 +1,20 @@ const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); module.exports = { zh: { + bbAdd: "添加", + bbQuickActions: "快捷操作", + bbPickTitle: "固定一个模块", + bbPickDesc: "为底部栏的 + 位置选择一个模块。", + bbPickNone: "无", + bbManage: "编辑", + bbYourBar: "你的工具栏", + bbRemove: "移除", + bbFull: "已达上限", + bbDone: "完成", + activityChangeFilter: "更改筛选", + emptyConnectHint: "连接到一个 pub 以与网络同步。", + emptyConnectCta: "连接到 pub", + menuCommunity: "社区", activityGroupCommunity: "社区与治理", activityGroupOrg: "组织", activityGroupComm: "沟通", diff --git a/src/configs/config-manager.js b/src/configs/config-manager.js index 71f3574..c2aa18e 100644 --- a/src/configs/config-manager.js +++ b/src/configs/config-manager.js @@ -78,6 +78,7 @@ if (!fs.existsSync(configFilePath)) { "limit": 2000 }, "homePage": "activity", + "bottomBarPins": ["search", "inbox", "publish"], "language": "en", "wish": "whole", "pmVisibility": "whole", @@ -96,6 +97,15 @@ const getConfig = () => { if (cfg.ux.current === 'menus') cfg.ux.current = 'blocks'; if (cfg.ux.current !== 'blocks' && cfg.ux.current !== 'ainav') cfg.ux.current = 'blocks'; if (cfg.ux.current === 'ainav' && cfg.modules && cfg.modules.aiNavMod !== 'on') cfg.ux.current = 'blocks'; + let pins = Array.isArray(cfg.bottomBarPins) + ? cfg.bottomBarPins + : (typeof cfg.bottomBarPin === 'string' + ? (cfg.bottomBarPin ? [cfg.bottomBarPin] : []) + : ['search', 'inbox', 'publish']); + pins = pins.filter(k => typeof k === 'string' && k); + pins = pins.filter((k, i) => pins.indexOf(k) === i).slice(0, 4); + cfg.bottomBarPins = pins; + delete cfg.bottomBarPin; return cfg; }; diff --git a/src/models/crypto.js b/src/models/crypto.js index bdd9321..10d7085 100644 --- a/src/models/crypto.js +++ b/src/models/crypto.js @@ -1,6 +1,7 @@ const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); +const keylock = require('./keylock'); const SENSITIVE_FIELDS = [ 'title', 'description', 'location', 'price', 'salary', 'options', 'votes', @@ -45,7 +46,14 @@ module.exports = (configPath, namespace = 'tribes') => { const loadKeyring = () => { try { - keyring = JSON.parse(fs.readFileSync(keyringPath, 'utf8')); + const raw = fs.readFileSync(keyringPath); + if (keylock.isWrapped(raw)) { + if (!keylock.getKEK()) throw new Error('keyring cifrado pero keylock bloqueado'); + keyring = JSON.parse(keylock.unwrap(raw).toString('utf8')); + } else { + keyring = JSON.parse(raw.toString('utf8')); + if (keylock.getKEK()) { try { saveKeyring(); } catch (_) {} } + } try { fs.chmodSync(keyringPath, 0o600); } catch (_) {} } catch (e) { if (e.code !== 'ENOENT') throw e; @@ -55,8 +63,11 @@ module.exports = (configPath, namespace = 'tribes') => { }; const saveKeyring = () => { + const json = JSON.stringify(keyring, null, 2); + const kek = keylock.getKEK(); + const out = kek ? keylock.wrap(Buffer.from(json, 'utf8'), kek) : Buffer.from(json, 'utf8'); const tmp = keyringPath + '.tmp.' + process.pid + '.' + Date.now(); - fs.writeFileSync(tmp, JSON.stringify(keyring, null, 2), { encoding: 'utf8', mode: 0o600 }); + fs.writeFileSync(tmp, out, { mode: 0o600 }); fs.renameSync(tmp, keyringPath); try { fs.chmodSync(keyringPath, 0o600); } catch (_) {} }; diff --git a/src/models/keylock.js b/src/models/keylock.js new file mode 100644 index 0000000..a50d7e8 --- /dev/null +++ b/src/models/keylock.js @@ -0,0 +1,129 @@ +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); +const ssbKeys = require('../server/node_modules/ssb-keys'); + +const MAGIC = Buffer.from('OAES1\n', 'utf8'); +const KEYLEN = 32; +const DEFAULT_SCRYPT = { N: 16384, r: 8, p: 1 }; +const VERIFIER_PLAINTEXT = Buffer.from('oasis-keylock-v1', 'utf8'); + +let KEK = null; // SOLO en memoria + +const maxmemFor = (s) => Math.max(64 * 1024 * 1024, 256 * s.N * s.r); + +const deriveKEK = (password, salt, scrypt = DEFAULT_SCRYPT) => + crypto.scryptSync(Buffer.from(String(password), 'utf8'), salt, KEYLEN, + { N: scrypt.N, r: scrypt.r, p: scrypt.p, maxmem: maxmemFor(scrypt) }); + +const isWrapped = (buf) => + Buffer.isBuffer(buf) && buf.length >= MAGIC.length && buf.subarray(0, MAGIC.length).equals(MAGIC); + +const wrap = (buf, kek = KEK) => { + if (!kek) throw new Error('keylock: locked (no KEK)'); + const iv = crypto.randomBytes(12); + const c = crypto.createCipheriv('aes-256-gcm', kek, iv); + const ct = Buffer.concat([c.update(buf), c.final()]); + const tag = c.getAuthTag(); + return Buffer.concat([MAGIC, iv, tag, ct]); +}; + +const unwrap = (blob, kek = KEK) => { + if (!kek) throw new Error('keylock: locked (no KEK)'); + if (!isWrapped(blob)) throw new Error('keylock: not a wrapped blob'); + let o = MAGIC.length; + const iv = blob.subarray(o, o += 12); + const tag = blob.subarray(o, o += 16); + const ct = blob.subarray(o); + const d = crypto.createDecipheriv('aes-256-gcm', kek, iv); + d.setAuthTag(tag); + return Buffer.concat([d.update(ct), d.final()]); +}; + +const lockPath = (configPath) => path.join(configPath, 'keylock.json'); + +const isConfigured = (configPath) => { + try { return fs.existsSync(lockPath(configPath)); } catch (_) { return false; } +}; + +const setPassword = (configPath, password) => { + const salt = crypto.randomBytes(16); + const scrypt = DEFAULT_SCRYPT; + const kek = deriveKEK(password, salt, scrypt); + const verifier = wrap(VERIFIER_PLAINTEXT, kek); + const data = { + v: 1, kdf: 'scrypt', N: scrypt.N, r: scrypt.r, p: scrypt.p, + salt: salt.toString('hex'), verifier: verifier.toString('base64'), + }; + fs.mkdirSync(configPath, { recursive: true, mode: 0o700 }); + const tmp = lockPath(configPath) + '.tmp'; + fs.writeFileSync(tmp, JSON.stringify(data), { mode: 0o600 }); + fs.renameSync(tmp, lockPath(configPath)); + KEK = kek; + return kek; +}; + +const verifyPassword = (configPath, password) => { + let data; + try { data = JSON.parse(fs.readFileSync(lockPath(configPath), 'utf8')); } catch (_) { return null; } + const salt = Buffer.from(data.salt, 'hex'); + const scrypt = { N: data.N || DEFAULT_SCRYPT.N, r: data.r || DEFAULT_SCRYPT.r, p: data.p || DEFAULT_SCRYPT.p }; + const kek = deriveKEK(password, salt, scrypt); + try { + if (unwrap(Buffer.from(data.verifier, 'base64'), kek).equals(VERIFIER_PLAINTEXT)) { + KEK = kek; + return kek; + } + } catch (_) {} + return null; +}; + +const getKEK = () => KEK; +const setKEK = (k) => { KEK = k; }; +const isUnlocked = () => KEK != null; +const lock = () => { try { if (KEK) KEK.fill(0); } catch (_) {} KEK = null; }; + +const secretEncPath = (configPath) => path.join(configPath, 'secret.enc'); +const legacySecretPath = (configPath) => path.join(configPath, 'secret'); + +const shred = (p) => { + try { fs.writeFileSync(p, crypto.randomBytes(Math.max(1, fs.statSync(p).size))); } catch (_) {} + try { fs.unlinkSync(p); } catch (_) {} +}; + +const writeSecret = (configPath, keys) => { + const blob = wrap(Buffer.from(JSON.stringify(keys), 'utf8')); + fs.mkdirSync(configPath, { recursive: true, mode: 0o700 }); + const tmp = secretEncPath(configPath) + '.tmp'; + fs.writeFileSync(tmp, blob, { mode: 0o600 }); + fs.renameSync(tmp, secretEncPath(configPath)); +}; + +const loadOrCreateSecret = (configPath) => { + if (!KEK) throw new Error('keylock: locked'); + fs.mkdirSync(configPath, { recursive: true, mode: 0o700 }); + const encP = secretEncPath(configPath); + if (fs.existsSync(encP)) { + return JSON.parse(unwrap(fs.readFileSync(encP)).toString('utf8')); + } + const legP = legacySecretPath(configPath); + if (fs.existsSync(legP)) { + const keys = ssbKeys.loadSync(legP); + writeSecret(configPath, keys); + shred(legP); + return keys; + } + const keys = ssbKeys.generate('ed25519'); // primer arranque + writeSecret(configPath, keys); + return keys; +}; + +const hasIdentity = (configPath) => + fs.existsSync(secretEncPath(configPath)) || fs.existsSync(legacySecretPath(configPath)); + +module.exports = { + MAGIC, deriveKEK, wrap, unwrap, isWrapped, + isConfigured, setPassword, verifyPassword, + getKEK, setKEK, isUnlocked, lock, + loadOrCreateSecret, writeSecret, hasIdentity, +}; diff --git a/src/server/ssb_config.js b/src/server/ssb_config.js index 6e9e36b..0299f68 100644 --- a/src/server/ssb_config.js +++ b/src/server/ssb_config.js @@ -11,9 +11,14 @@ const i = argv.indexOf('--'); const conf = argv.slice(i + 1); const cliArgs = ~i ? argv.slice(0, i) : argv; -let config = Config('ssb', minimist(conf)); +const override = minimist(conf); +if (global.__OASIS_KEYS__) override.keys = global.__OASIS_KEYS__; + +let config = Config('ssb', override); config = { ...config, ...configData }; +if (global.__OASIS_KEYS__) config.keys = global.__OASIS_KEYS__; + const debug = process.argv.includes('--debug') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true'; if (debug) { config.logging = { ...(config.logging || {}), level: 'debug' }; diff --git a/src/views/activity_view.js b/src/views/activity_view.js index fe9fb95..f594a71 100644 --- a/src/views/activity_view.js +++ b/src/views/activity_view.js @@ -1,5 +1,5 @@ const { div, h2, p, section, button, form, a, input, img, textarea, br, span, video: videoHyperaxe, audio: audioHyperaxe, table, tr, td, th, details, summary, ul, li, label } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, userLinkLabel, renderSpreadButton, renderContentActions } = require('./main_views'); +const { template, i18n, userLink, userLinkLabel, renderSpreadButton, renderContentActions, emptyState, renderHiveNav } = require('./main_views'); const opinionCategories = require('../backend/opinion_categories'); const OPINION_TYPES = new Set(['bookmark','votes','feed','image','audio','video','document','torrent','transfer']); @@ -285,7 +285,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { : validActions; if (!deduped.length) { - return div({ class: "no-actions" }, p(i18n.noActions)); + return emptyState({ icon: "📭", title: i18n.noActions, text: i18n.emptyConnectHint, ctaHref: "/invites", ctaLabel: i18n.emptyConnectCta }); } const seenDocumentTitles = new Set(); @@ -1596,7 +1596,7 @@ function renderActionCards(actions, userId, allActions, spreadMap = new Map()) { const filteredCards = cards.filter(Boolean); if (!filteredCards.length) { - return div({ class: "no-actions" }, p(i18n.noActions)); + return emptyState({ icon: "📭", title: i18n.noActions, text: i18n.emptyConnectHint, ctaHref: "/invites", ctaLabel: i18n.emptyConnectCta }); } return filteredCards; } @@ -1841,6 +1841,7 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { h2(i18n.activityList), p(desc) ), + renderHiveNav(), (() => { const labelOf = {}; activityTypes.forEach(t => { labelOf[t.type] = t.label; }); @@ -1849,33 +1850,18 @@ exports.activityView = (actions, filter, userId, q = '', extras = {}) => { button({ type: 'submit', class: filter === type ? 'filter-btn active' : 'filter-btn' }, labelOf[type] || type) ); const QUICK = ['recent', 'all', 'mine']; - const GROUPS = [ - { id: 'act-comunidad', emoji: '👥', title: i18n.activityGroupCommunity || 'Community & Governance', types: ['inhabitants', 'tribe', 'larp', 'parliament', 'courts', 'votes', 'report'] }, - { id: 'act-organizacion', emoji: '🗓', title: i18n.activityGroupOrg || 'Organization', types: ['event', 'calendar', 'task', 'project', 'job'] }, - { id: 'act-comunicacion', emoji: '💬', title: i18n.activityGroupComm || 'Communication', types: ['post', 'feed', 'chat', 'pad', 'forum'] }, - { id: 'act-economia', emoji: '💰', title: i18n.activityGroupEconomy || 'Economy', types: ['banking', 'market', 'shop', 'transfer'] }, - { id: 'act-media', emoji: '🎬', title: i18n.activityGroupMedia || 'Content & Media', types: ['audio', 'bookmark', 'document', 'image', 'torrent', 'video'] }, - { id: 'act-otros', emoji: '⋯', title: i18n.activityGroupOther || 'Others', types: ['map', 'curriculum'] }, - ]; + const isTypeFilter = filter && !QUICK.includes(filter) && labelOf[filter]; + if (isTypeFilter) { + return div({ class: 'activity-filter-collapsed' }, + a({ class: 'activity-back filter-btn', href: '/activity' }, + span({ class: 'activity-back-arrow' }, '‹'), + span(i18n.activityChangeFilter || 'Change filter') + ), + span({ class: 'activity-active-filter' }, labelOf[filter] || filter) + ); + } return div({ class: 'activity-filter-acc' }, - div({ class: 'activity-quick-modes' }, QUICK.map(mkBtn)), - ul({ class: 'oasis-nav-groups activity-groups' }, - GROUPS.map(g => { - const openByFilter = g.types.includes(filter); - return li({ class: 'oasis-nav-group' }, - input({ type: 'checkbox', id: `oasis-nav-group-${g.id}`, class: 'oasis-nav-toggle', ...(openByFilter ? { checked: true } : {}) }), - label({ for: `oasis-nav-group-${g.id}`, class: 'oasis-nav-header' }, - span({ class: 'emoji' }, g.emoji), - span({ class: 'nav-text' }, g.title), - span({ class: 'act-count' }, String(g.types.length)), - span({ class: 'oasis-nav-arrow' }, '▾') - ), - ul({ class: 'oasis-nav-list' }, - g.types.map(t => li(mkBtn(t))) - ) - ); - }) - ) + div({ class: 'activity-quick-modes' }, QUICK.map(mkBtn)) ); })(), sub diff --git a/src/views/main_views.js b/src/views/main_views.js index b16b17e..8fb458e 100644 --- a/src/views/main_views.js +++ b/src/views/main_views.js @@ -112,6 +112,67 @@ const renderContentActions = (msgId, viewHref) => { }; exports.renderContentActions = renderContentActions; +const emptyState = ({ icon, title, text, ctaHref, ctaLabel }) => div( + { class: "empty-state" }, + icon ? span({ class: "empty-state-icon" }, icon) : "", + title ? p({ class: "empty-state-title" }, title) : "", + text ? p({ class: "empty-state-text" }, text) : "", + (ctaHref && ctaLabel) ? a({ class: "empty-state-cta filter-btn", href: ctaHref }, ctaLabel) : "" +); +exports.emptyState = emptyState; + +exports.bottomBarPickerView = () => { + const cfg = getConfig(); + const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined; + const MAX = 4; + const pinnedKeys = cfg.bottomBarPins || []; + const pinned = pinnedKeys + .map(key => exports.PINNABLE_MODULES.find(m => m.key === key)) + .filter(Boolean); + const full = pinned.length >= MAX; + const actionForm = (action, key, cls, node) => form({ method: 'POST', action: '/settings/bottombar', class: cls }, + input({ type: 'hidden', name: 'action', value: action }), + input({ type: 'hidden', name: 'key', value: key }), + node + ); + const pinnedRow = (m) => actionForm('remove', m.key, 'bb-manage-form', + button({ type: 'submit', class: 'bb-manage-btn', 'aria-label': i18n.bbRemove || 'Remove' }, + span({ class: 'bb-manage-ico' }, m.glyph), + span({ class: 'bb-manage-lbl' }, i18n[m.labelKey] || m.key), + span({ class: 'bb-manage-x' }, '✕') + ) + ); + const addBtn = (m) => actionForm('add', m.key, 'bb-pick-form', + button({ type: 'submit', class: 'bb-pick-btn' }, + span({ class: 'bb-pick-ico' }, m.glyph), + span({ class: 'bb-pick-lbl' }, i18n[m.labelKey] || m.key) + ) + ); + const addable = exports.PINNABLE_MODULES.filter(m => modOn(m.mod) && !pinnedKeys.includes(m.key)); + return template( + i18n.bbManage || 'Edit bar', + section( + div({ class: 'tags-header' }, + h2(i18n.bbPickTitle || 'Customize the bar'), + p(i18n.bbPickDesc || 'Choose up to 4 shortcuts for the bottom bar.') + ), + div({ class: 'bb-manage' }, + h3(`${i18n.bbYourBar || 'Your bar'} (${pinned.length}/${MAX})`), + pinned.length + ? div({ class: 'bb-manage-list' }, ...pinned.map(pinnedRow)) + : p({ class: 'bb-manage-empty' }, i18n.bbPickNone || 'None') + ), + div({ class: 'bb-manage-add' }, + h3(i18n.bbAdd || 'Add'), + full + ? p({ class: 'bb-manage-full' }, i18n.bbFull || 'Maximum reached') + : div({ class: 'bb-pick-grid' }, ...addable.map(addBtn)) + ), + a({ class: 'filter-btn bb-manage-done', href: '/activity' }, i18n.bbDone || 'Done') + ) + ); +}; + exports.renderStateChip = renderStateChip; exports.renderOpenClosedChip = renderOpenClosedChip; exports.renderVisibilityChip = renderVisibilityChip; @@ -1080,27 +1141,160 @@ const renderMobileTopbar = () => div( a({ class: "omt-avatar", href: "/profile" }, img({ src: (typeof global !== "undefined" && global.__OASIS_SELF_AVATAR__) || "/assets/images/default-avatar.png", alt: i18n.profile || "Profile" })) ); +const PINNABLE_MODULES = [ + { key: 'search', href: '/search', glyph: 'ꔅ', labelKey: 'searchTitle', mod: null }, + { key: 'inbox', href: '/inbox', glyph: '☂', labelKey: 'inbox', mod: null }, + { key: 'publish', href: '/publish', glyph: '❂', labelKey: 'publish', mod: null }, + { key: 'activity', href: '/activity', glyph: 'ꔙ', labelKey: 'activityTitle', mod: null }, + { key: 'tribes', href: '/tribes', glyph: 'ꖥ', labelKey: 'tribesTitle', mod: 'tribesMod' }, + { key: 'chats', href: '/chats', glyph: 'ꖒ', labelKey: 'chatsTitle', mod: 'chatsMod' }, + { key: 'market', href: '/market', glyph: 'ꕻ', labelKey: 'marketTitle', mod: 'marketMod' }, + { key: 'events', href: '/events', glyph: 'ꕆ', labelKey: 'eventsLabel', mod: 'eventsMod' }, + { key: 'calendars', href: '/calendars', glyph: 'ꖯ', labelKey: 'calendarsTitle', mod: 'calendarsMod' }, + { key: 'banking', href: '/banking', glyph: 'ꗴ', labelKey: 'bankingTitle', mod: 'bankingMod' }, + { key: 'forum', href: '/forum', glyph: 'ꕒ', labelKey: 'forumTitle', mod: 'forumMod' }, + { key: 'videos', href: '/videos', glyph: 'ꗟ', labelKey: 'videosLabel', mod: 'videosMod' }, + { key: 'images', href: '/images', glyph: 'ꕥ', labelKey: 'imagesLabel', mod: 'imagesMod' }, + { key: 'audios', href: '/audios', glyph: 'ꔿ', labelKey: 'audiosLabel', mod: 'audiosMod' }, + { key: 'documents', href: '/documents', glyph: 'ꕨ', labelKey: 'docsLabel', mod: 'docsMod' }, + { key: 'bookmarks', href: '/bookmarks', glyph: 'ꔪ', labelKey: 'bookmarksLabel', mod: 'bookmarksMod' }, + { key: 'maps', href: '/maps', glyph: 'ꔌ', labelKey: 'mapsLabel', mod: 'mapsMod' }, + { key: 'trending', href: '/trending', glyph: 'ꗝ', labelKey: 'trendingLabel', mod: 'trendingMod' }, + { key: 'jobs', href: '/jobs', glyph: 'ꗒ', labelKey: 'jobsTitle', mod: 'jobsMod' }, + { key: 'agenda', href: '/agenda', glyph: 'ꗤ', labelKey: 'agendaTitle', mod: 'agendaMod' }, + { key: 'wallet', href: '/wallet', glyph: '❄', labelKey: 'wallet', mod: 'walletMod' }, + { key: 'tasks', href: '/tasks', glyph: 'ꖧ', labelKey: 'tasksTitle', mod: 'tasksMod' }, + { key: 'torrents', href: '/torrents', glyph: 'ꖅ', labelKey: 'torrentsLabel', mod: 'torrentsMod' }, + { key: 'tags', href: '/tags', glyph: 'ꖶ', labelKey: 'tagsLabel', mod: 'tagsMod' }, + { key: 'pads', href: '/pads', glyph: 'ꔗ', labelKey: 'padsTitle', mod: 'padsMod' }, + { key: 'projects', href: '/projects', glyph: 'ꕧ', labelKey: 'projectsTitle', mod: 'projectsMod' }, + { key: 'shops', href: '/shops', glyph: 'ꔜ', labelKey: 'shopsTitle', mod: 'shopsMod' }, + { key: 'transfers', href: '/transfers', glyph: 'ꘉ', labelKey: 'transfersTitle', mod: 'transfersMod' }, + { key: 'feed', href: '/feed', glyph: 'ꕿ', labelKey: 'feedTitle', mod: 'feedMod' }, + { key: 'pixelia', href: '/pixelia', glyph: 'ꔘ', labelKey: 'pixeliaTitle', mod: 'pixeliaMod' }, + { key: 'melody', href: '/melody', glyph: '♪', labelKey: 'melodyTitle', mod: 'melodyMod' }, + { key: 'games', href: '/games', glyph: 'ꕇ', labelKey: 'gamesTitle', mod: 'gamesMod' }, + { key: 'votes', href: '/votes', glyph: 'ꔰ', labelKey: 'votationsTitle', mod: 'votesMod' }, + { key: 'reports', href: '/reports', glyph: 'ꕥ', labelKey: 'reportsTitle', mod: 'reportsMod' }, + { key: 'opinions', href: '/opinions', glyph: 'ꔍ', labelKey: 'opinionsTitle', mod: 'opinionsMod' }, + { key: 'parliament',href: '/parliament',glyph: 'ꗞ', labelKey: 'parliamentTitle',mod: 'parliamentMod' }, + { key: 'courts', href: '/courts', glyph: 'ꖻ', labelKey: 'courtsTitle', mod: 'courtsMod' }, + { key: 'larp', href: '/larp', glyph: '✦', labelKey: 'larpTitle', mod: 'larpMod' }, + { key: 'ai', href: '/ai', glyph: 'ꘜ', labelKey: 'ai', mod: 'aiMod' }, + { key: 'cipher', href: '/cipher', glyph: 'ꗄ', labelKey: 'cipher', mod: 'cipherMod' }, + { key: 'fediverse', href: '/fediverse', glyph: 'ꗵ', labelKey: 'fediverseTimeline', mod: 'fediverseMod' }, + { key: 'inhabitants', href: '/inhabitants', glyph: 'ꖘ', labelKey: 'inhabitantsLabel', mod: null }, + { key: 'cv', href: '/cv', glyph: 'ꕛ', labelKey: 'cvTitle', mod: null }, + { key: 'stats', href: '/stats', glyph: 'ꕷ', labelKey: 'statistics', mod: null }, + { key: 'blockexplorer', href: '/blockexplorer', glyph: 'ꖸ', labelKey: 'blockchain', mod: null }, +]; +exports.PINNABLE_MODULES = PINNABLE_MODULES; + const renderBottomBar = () => { const inboxN = sharedState.getInboxCount(); + const cfg = getConfig(); + const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined; const bbLink = (href, ico, label, badge) => a( { class: "bb-item", href }, span({ class: "bb-ico" }, ico), (badge && badge > 0) ? span({ class: "bb-badge" }, String(badge)) : "", span({ class: "bb-lbl" }, label) ); + const MAX = 4; + const pins = (cfg.bottomBarPins || []) + .map(key => PINNABLE_MODULES.find(m => m.key === key)) + .filter(m => m && modOn(m.mod)) + .slice(0, MAX); + const slots = pins.map(m => bbLink( + m.href, m.glyph, i18n[m.labelKey] || m.key, m.key === 'inbox' ? inboxN : 0 + )); + const editSlot = a( + { class: "bb-item bb-add", href: "/settings/bottombar" }, + span({ class: "bb-ico" }, pins.length < MAX ? "+" : "✎"), + span({ class: "bb-lbl" }, pins.length < MAX ? (i18n.bbAdd || "Add") : (i18n.bbManage || "Edit")) + ); + const pair = [ bbLink("/peers", "⧖", i18n.peers || "Peers") ]; + if (modOn('invitesMod')) pair.push(bbLink("/invites", "ꔮ", i18n.invites || "Invites")); return nav( - { class: "oasis-bottombar", style: "display:none", "aria-label": "Quick actions" }, - bbLink("/search", "ꔅ", i18n.searchTitle || "Buscar"), - bbLink("/inbox", "☂", i18n.inbox || "Inbox", inboxN), - bbLink("/publish", "❂", i18n.publish || "Write"), - bbLink("/activity", "ꔙ", i18n.activityTitle || "Activity"), - div({ class: "bb-pair" }, - bbLink("/peers", "⧖", i18n.peers || "Peers"), - bbLink("/invites", "ꔮ", i18n.invites || "Invites") - ) + { class: "oasis-bottombar", style: "display:none", "aria-label": i18n.bbQuickActions || "Quick actions" }, + ...slots, + editSlot, + div({ class: "bb-pair" }, ...pair) ); }; +const RADIAL_ITEMS = [ + { key: 'search', href: '/search', glyph: 'ꔅ', labelKey: 'searchTitle', mod: null }, + { key: 'chats', href: '/chats', glyph: 'ꖒ', labelKey: 'chatsTitle', mod: 'chatsMod' }, + { key: 'tribes', href: '/tribes', glyph: 'ꖥ', labelKey: 'tribesTitle', mod: 'tribesMod' }, + { key: 'market', href: '/market', glyph: 'ꕻ', labelKey: 'marketTitle', mod: 'marketMod' }, + { key: 'events', href: '/events', glyph: 'ꕆ', labelKey: 'eventsLabel', mod: 'eventsMod' }, + { key: 'forum', href: '/forum', glyph: 'ꕒ', labelKey: 'forumTitle', mod: 'forumMod' }, + { key: 'agenda', href: '/agenda', glyph: 'ꗤ', labelKey: 'agendaTitle', mod: 'agendaMod' }, +]; + +const renderHiveNav = () => { + if (process.env.OASIS_MOBILE !== '1') return ""; + const cfg = getConfig(); + const modOn = (m) => !m || cfg.modules[m] === 'on' || cfg.modules[m] === undefined; + const CATS = [ + { id: 'personal', emoji: '⚉', title: i18n.menuPersonal, mods: [ + { href: '/profile', glyph: '⚉', labelKey: 'profile', mod: null }, { href: '/cv', glyph: 'ꕛ', labelKey: 'cvTitle', mod: null }, + { href: '/agenda', glyph: 'ꗤ', labelKey: 'agendaTitle', mod: 'agendaMod' }, { href: '/favorites', glyph: 'ꘝ', labelKey: 'favoritesTitle', mod: 'favoritesMod' }, + { href: '/logs', glyph: 'ꗯ', labelKey: 'logsTitle', mod: 'logsMod' }, { href: '/wallet', glyph: '❄', labelKey: 'wallet', mod: 'walletMod' }, + { href: '/modules', glyph: 'ꗣ', labelKey: 'modules', mod: null }, { href: '/settings', glyph: '⚙', labelKey: 'settings', mod: null } ] }, + { id: 'governance', emoji: '⚖', title: i18n.menuGovernance, mods: [ + { href: '/inhabitants', glyph: 'ꖘ', labelKey: 'inhabitantsLabel', mod: null }, { href: '/tribes', glyph: 'ꖥ', labelKey: 'tribesTitle', mod: 'tribesMod' }, + { href: '/larp', glyph: '✦', labelKey: 'larpTitle', mod: 'larpMod' }, { href: '/parliament', glyph: 'ꗞ', labelKey: 'parliamentTitle', mod: 'parliamentMod' }, + { href: '/courts', glyph: 'ꖻ', labelKey: 'courtsTitle', mod: 'courtsMod' } ] }, + { id: 'office', emoji: '⌂', title: i18n.menuOffice, mods: [ + { href: '/votes', glyph: 'ꔰ', labelKey: 'votationsTitle', mod: 'votesMod' }, { href: '/events', glyph: 'ꕆ', labelKey: 'eventsLabel', mod: 'eventsMod' }, + { href: '/calendars', glyph: 'ꖯ', labelKey: 'calendarsTitle', mod: 'calendarsMod' }, { href: '/tasks', glyph: 'ꖧ', labelKey: 'tasksTitle', mod: 'tasksMod' }, + { href: '/reports', glyph: 'ꕥ', labelKey: 'reportsTitle', mod: 'reportsMod' } ] }, + { id: 'economy', emoji: '¤', title: i18n.menuEconomy, mods: [ + { href: '/banking', glyph: 'ꗴ', labelKey: 'bankingTitle', mod: 'bankingMod' }, { href: '/market', glyph: 'ꕻ', labelKey: 'marketTitle', mod: 'marketMod' }, + { href: '/projects', glyph: 'ꕧ', labelKey: 'projectsTitle', mod: 'projectsMod' }, { href: '/jobs', glyph: 'ꗒ', labelKey: 'jobsTitle', mod: 'jobsMod' }, + { href: '/shops', glyph: 'ꔜ', labelKey: 'shopsTitle', mod: 'shopsMod' }, { href: '/transfers', glyph: 'ꘉ', labelKey: 'transfersTitle', mod: 'transfersMod' } ] }, + { id: 'tools', emoji: '⚒', title: i18n.menuTools, mods: [ + { href: '/ai', glyph: 'ꘜ', labelKey: 'ai', mod: 'aiMod' }, { href: '/blockexplorer', glyph: 'ꖸ', labelKey: 'blockchain', mod: null }, + { href: '/cipher', glyph: 'ꗄ', labelKey: 'cipher', mod: 'cipherMod' }, { href: '/invites', glyph: 'ꔹ', labelKey: 'invites', mod: 'invitesMod' }, + { href: '/legacy', glyph: 'ꖤ', labelKey: 'legacy', mod: 'legacyMod' }, { href: '/stats', glyph: 'ꕷ', labelKey: 'statistics', mod: null } ] }, + { id: 'network', emoji: '☍', title: i18n.menuNetwork, mods: [ + { href: '/activity', glyph: 'ꔙ', labelKey: 'activityTitle', mod: null }, { href: '/tags', glyph: 'ꖶ', labelKey: 'tagsLabel', mod: 'tagsMod' }, + { href: '/trending', glyph: 'ꗝ', labelKey: 'trendingLabel', mod: 'trendingMod' }, { href: '/opinions', glyph: 'ꔍ', labelKey: 'opinionsTitle', mod: 'opinionsMod' }, + { href: '/pads', glyph: 'ꔗ', labelKey: 'padsTitle', mod: 'padsMod' }, { href: '/forum', glyph: 'ꕒ', labelKey: 'forumTitle', mod: 'forumMod' }, + { href: '/maps', glyph: 'ꔌ', labelKey: 'mapsLabel', mod: 'mapsMod' }, { href: '/chats', glyph: 'ꖒ', labelKey: 'chatsTitle', mod: 'chatsMod' } ] }, + { id: 'blogs', emoji: '✦', title: i18n.menuBlogs, mods: [ + { href: '/mentions', glyph: '✺', labelKey: 'mentions', mod: null }, { href: '/public/latest', glyph: '☄', labelKey: 'latest', mod: 'latestMod' }, + { href: '/public/latest/threads', glyph: '♺', labelKey: 'threads', mod: 'threadsMod' }, { href: '/public/latest/topics', glyph: 'ϟ', labelKey: 'topics', mod: 'topicsMod' }, + { href: '/public/latest/summaries', glyph: '※', labelKey: 'summaries', mod: 'summariesMod' }, { href: '/public/popular/day', glyph: '⌘', labelKey: 'popular', mod: 'popularMod' }, + { href: '/public/latest/extended', glyph: '∞', labelKey: 'multiverse', mod: 'multiverseMod' } ] }, + { id: 'creative', emoji: '✎', title: i18n.menuCreative, mods: [ + { href: '/feed', glyph: 'ꕿ', labelKey: 'feedTitle', mod: 'feedMod' }, { href: '/games', glyph: 'ꕇ', labelKey: 'gamesTitle', mod: 'gamesMod' }, + { href: '/pixelia', glyph: 'ꔘ', labelKey: 'pixeliaTitle', mod: 'pixeliaMod' }, { href: '/melody', glyph: '♪', labelKey: 'melodyTitle', mod: 'melodyMod' } ] }, + { id: 'media', emoji: '▤', title: i18n.menuMedia, mods: [ + { href: '/audios', glyph: 'ꔿ', labelKey: 'audiosLabel', mod: 'audiosMod' }, { href: '/bookmarks', glyph: 'ꔪ', labelKey: 'bookmarksLabel', mod: 'bookmarksMod' }, + { href: '/documents', glyph: 'ꕨ', labelKey: 'docsLabel', mod: 'docsMod' }, { href: '/images', glyph: 'ꕥ', labelKey: 'imagesLabel', mod: 'imagesMod' }, + { href: '/torrents', glyph: 'ꖅ', labelKey: 'torrentsLabel', mod: 'torrentsMod' }, { href: '/videos', glyph: 'ꗟ', labelKey: 'videosLabel', mod: 'videosMod' } ] }, + { id: 'fediverse', emoji: '⌬', title: i18n.fediverse, mods: [ + { href: '/fediverse', glyph: 'ꗵ', labelKey: 'fediverseTimeline', mod: 'fediverseMod' } ] }, + ]; + const catHex = (c) => label({ for: `hive-${c.id}`, class: 'hex-cat' }, + span({ class: 'hx-ico' }, c.emoji), span({ class: 'hx-lbl' }, c.title)); + const modHex = (m) => a({ class: 'hex-mod', href: m.href }, + span({ class: 'hx-ico' }, m.glyph), span({ class: 'hx-lbl' }, i18n[m.labelKey] || '')); + const radios = CATS.map(c => input({ type: 'radio', name: 'hive-cat', id: `hive-${c.id}`, class: 'hive-radio' })); + const catRows = []; + let idx = 0; + for (const size of [3, 4, 3]) { catRows.push(div({ class: 'hive-row' }, ...CATS.slice(idx, idx + size).map(catHex))); idx += size; } + const grid = div({ class: 'hive-grid hive-cats' }, ...catRows); + const panels = CATS.map(c => div({ class: 'hive-panel', id: `hive-panel-${c.id}` }, + div({ class: 'hive-mods' }, ...c.mods.filter(m => modOn(m.mod)).map(modHex)) + )); + return div({ class: 'hive-nav' }, ...radios, grid, ...panels); +}; +exports.renderHiveNav = renderHiveNav; + const template = (titlePrefix, ...elements) => { const currentConfig = getConfig(); const theme = currentConfig.themes.current || "Dark-SNH"; @@ -1116,6 +1310,7 @@ const template = (titlePrefix, ...elements) => { link({ rel: "stylesheet", href: "/assets/styles/style.css" }), themeLink, link({ rel: "stylesheet", href: "/assets/styles/mobile.css", ...(process.env.OASIS_MOBILE === '1' ? {} : { media: "(max-width: 768px)" }) }), + link({ rel: "stylesheet", href: "/assets/themes/OasisMobileUX.css", ...(process.env.OASIS_MOBILE === '1' ? {} : { media: "(max-width: 768px)" }) }), link({ rel: "icon", href: "/assets/images/favicon.svg" }), meta({ charset: "utf-8" }), meta({ name: "description", content: i18n.oasisDescription }), @@ -1439,6 +1634,7 @@ exports.ainavHomeView = ({ recentTags = [] } = {}) => { link({ rel: "stylesheet", href: "/assets/styles/style.css" }), link({ rel: "stylesheet", href: `/assets/themes/${theme}.css` }), link({ rel: "stylesheet", href: "/assets/styles/mobile.css", ...(process.env.OASIS_MOBILE === '1' ? {} : { media: "(max-width: 768px)" }) }), + link({ rel: "stylesheet", href: "/assets/themes/OasisMobileUX.css", ...(process.env.OASIS_MOBILE === '1' ? {} : { media: "(max-width: 768px)" }) }), link({ rel: "icon", href: "/assets/images/favicon.svg" }), meta({ charset: "utf-8" }), meta({ name: "description", content: i18n.oasisDescription }), @@ -3402,7 +3598,7 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => { threadGroups[tid].push(msg) } - if (!threadOrder.length) return p({ class: 'empty' }, i18n.noPrivateMessages) + if (!threadOrder.length) return emptyState({ icon: "✉", title: i18n.noPrivateMessages, text: i18n.emptyConnectHint, ctaHref: "/invites", ctaLabel: i18n.emptyConnectCta }) return threadOrder.map(tid => { const msgs = threadGroups[tid] diff --git a/src/views/search_view.js b/src/views/search_view.js index 055519a..a8a583c 100644 --- a/src/views/search_view.js +++ b/src/views/search_view.js @@ -1,5 +1,5 @@ const { form, button, div, h2, p, section, input, label, select, option, img, audio: audioHyperaxe, video: videoHyperaxe, table, hr, hd, br, td, tr, th, a, span } = require("../server/node_modules/hyperaxe"); -const { template, i18n, userLink, renderContentActions } = require('./main_views'); +const { template, i18n, userLink, renderContentActions, emptyState } = require('./main_views'); const moment = require("../server/node_modules/moment"); const { renderTextWithStyles } = require('../backend/renderTextWithStyles'); const { renderUrl } = require('../backend/renderUrl'); @@ -601,7 +601,7 @@ const searchView = ({ messages = [], blobs = {}, query = "", type = "", types = }) ) ) - : div({ class: 'no-results' }, p(i18n.noResultsFound)); + : emptyState({ icon: "🔍", title: i18n.noResultsFound }); let html = template( hashtag ? `#${hashtag}` : i18n.searchTitle,