Configurable bottom bar and empty-state helper

- Bottom bar: replace Activity with a + slot to pin a module; hide slots for disabled modules.
- Add a reusable empty-state helper and the inbox empty state.
- Mobile styles and translations for the above.
This commit is contained in:
s1to 2026-07-09 16:44:17 +02:00
parent 851042e576
commit 9c100ed773
13 changed files with 252 additions and 10 deletions

View file

@ -1531,3 +1531,58 @@ button, input[type="submit"], input[type="button"],
.main-column { padding: 10px 12px 16px 12px !important; } .main-column { padding: 10px 12px 16px 12px !important; }
.activity-filter-acc { margin-bottom: 4px !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; }

View file

@ -1,6 +1,15 @@
const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); const { a, em, strong } = require('../../../server/node_modules/hyperaxe');
module.exports = { module.exports = {
ar: { ar: {
bbAdd: "إضافة",
bbQuickActions: "إجراءات سريعة",
bbPickTitle: "تثبيت وحدة",
bbPickDesc: "اختر وحدة لخانة + في الشريط السفلي.",
bbPickNone: "لا شيء",
activityChangeFilter: "تغيير التصفية",
emptyConnectHint: "اتصل بـ pub للمزامنة مع الشبكة.",
emptyConnectCta: "الاتصال بـ pub",
menuCommunity: "المجتمع",
activityGroupCommunity: "المجتمع والحوكمة", activityGroupCommunity: "المجتمع والحوكمة",
activityGroupOrg: "التنظيم", activityGroupOrg: "التنظيم",
activityGroupComm: "التواصل", activityGroupComm: "التواصل",

View file

@ -1,6 +1,15 @@
const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); const { a, em, strong } = require('../../../server/node_modules/hyperaxe');
module.exports = { module.exports = {
de: { de: {
bbAdd: "Hinzufügen",
bbQuickActions: "Schnellaktionen",
bbPickTitle: "Modul anheften",
bbPickDesc: "Wähle ein Modul für den +-Platz in der unteren Leiste.",
bbPickNone: "Keins",
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", activityGroupCommunity: "Gemeinschaft & Governance",
activityGroupOrg: "Organisation", activityGroupOrg: "Organisation",
activityGroupComm: "Kommunikation", activityGroupComm: "Kommunikation",

View file

@ -1,6 +1,15 @@
const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); const { a, em, strong } = require('../../../server/node_modules/hyperaxe');
module.exports = { module.exports = {
en: { en: {
bbAdd: "Add",
bbQuickActions: "Quick actions",
bbPickTitle: "Pin a module",
bbPickDesc: "Choose a module for the + slot in the bottom bar.",
bbPickNone: "None",
activityChangeFilter: "Change filter",
emptyConnectHint: "Connect to a pub to sync with the network.",
emptyConnectCta: "Connect to a pub",
menuCommunity: "Community",
activityGroupCommunity: "Community & Governance", activityGroupCommunity: "Community & Governance",
activityGroupOrg: "Organization", activityGroupOrg: "Organization",
activityGroupComm: "Communication", activityGroupComm: "Communication",

View file

@ -1,6 +1,15 @@
const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); const { a, em, strong } = require('../../../server/node_modules/hyperaxe');
module.exports = { module.exports = {
es: { 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",
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", activityGroupCommunity: "Comunidad y Gobernanza",
activityGroupOrg: "Organización", activityGroupOrg: "Organización",
activityGroupComm: "Comunicación", activityGroupComm: "Comunicación",

View file

@ -1,6 +1,15 @@
const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); const { a, em, strong } = require('../../../server/node_modules/hyperaxe');
module.exports = { module.exports = {
eu: { eu: {
bbAdd: "Gehitu",
bbQuickActions: "Ekintza azkarrak",
bbPickTitle: "Ainguratu modulu bat",
bbPickDesc: "Aukeratu modulu bat beheko barrako + hutsunerako.",
bbPickNone: "Bat ere ez",
activityChangeFilter: "Aldatu iragazkia",
emptyConnectHint: "Konektatu pub batera sarearekin sinkronizatzeko.",
emptyConnectCta: "Konektatu pub batera",
menuCommunity: "Komunitatea",
activityGroupCommunity: "Komunitatea eta gobernantza", activityGroupCommunity: "Komunitatea eta gobernantza",
activityGroupOrg: "Antolakuntza", activityGroupOrg: "Antolakuntza",
activityGroupComm: "Komunikazioa", activityGroupComm: "Komunikazioa",

View file

@ -1,6 +1,15 @@
const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); const { a, em, strong } = require('../../../server/node_modules/hyperaxe');
module.exports = { module.exports = {
fr: { fr: {
bbAdd: "Ajouter",
bbQuickActions: "Actions rapides",
bbPickTitle: "Épingler un module",
bbPickDesc: "Choisis un module pour l'emplacement + de la barre inférieure.",
bbPickNone: "Aucun",
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", activityGroupCommunity: "Communauté et gouvernance",
activityGroupOrg: "Organisation", activityGroupOrg: "Organisation",
activityGroupComm: "Communication", activityGroupComm: "Communication",

View file

@ -1,6 +1,15 @@
const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); const { a, em, strong } = require('../../../server/node_modules/hyperaxe');
module.exports = { module.exports = {
hi: { hi: {
bbAdd: "जोड़ें",
bbQuickActions: "त्वरित क्रियाएँ",
bbPickTitle: "एक मॉड्यूल पिन करें",
bbPickDesc: "नीचे की बार में + स्लॉट के लिए एक मॉड्यूल चुनें।",
bbPickNone: "कोई नहीं",
activityChangeFilter: "फ़िल्टर बदलें",
emptyConnectHint: "नेटवर्क से सिंक करने के लिए किसी pub से जुड़ें।",
emptyConnectCta: "pub से जुड़ें",
menuCommunity: "समुदाय",
activityGroupCommunity: "समुदाय और शासन", activityGroupCommunity: "समुदाय और शासन",
activityGroupOrg: "संगठन", activityGroupOrg: "संगठन",
activityGroupComm: "संचार", activityGroupComm: "संचार",

View file

@ -1,6 +1,15 @@
const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); const { a, em, strong } = require('../../../server/node_modules/hyperaxe');
module.exports = { module.exports = {
it: { it: {
bbAdd: "Aggiungi",
bbQuickActions: "Azioni rapide",
bbPickTitle: "Fissa un modulo",
bbPickDesc: "Scegli un modulo per lo spazio + nella barra inferiore.",
bbPickNone: "Nessuno",
activityChangeFilter: "Cambia filtro",
emptyConnectHint: "Connettiti a un pub per sincronizzarti con la rete.",
emptyConnectCta: "Connettiti a un pub",
menuCommunity: "Comunità",
activityGroupCommunity: "Comunità e governance", activityGroupCommunity: "Comunità e governance",
activityGroupOrg: "Organizzazione", activityGroupOrg: "Organizzazione",
activityGroupComm: "Comunicazione", activityGroupComm: "Comunicazione",

View file

@ -1,6 +1,15 @@
const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); const { a, em, strong } = require('../../../server/node_modules/hyperaxe');
module.exports = { module.exports = {
pt: { 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",
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", activityGroupCommunity: "Comunidade e governança",
activityGroupOrg: "Organização", activityGroupOrg: "Organização",
activityGroupComm: "Comunicação", activityGroupComm: "Comunicação",

View file

@ -1,6 +1,15 @@
const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); const { a, em, strong } = require('../../../server/node_modules/hyperaxe');
module.exports = { module.exports = {
ru: { ru: {
bbAdd: "Добавить",
bbQuickActions: "Быстрые действия",
bbPickTitle: "Закрепить модуль",
bbPickDesc: "Выберите модуль для слота + в нижней панели.",
bbPickNone: "Нет",
activityChangeFilter: "Сменить фильтр",
emptyConnectHint: "Подключитесь к пабу для синхронизации с сетью.",
emptyConnectCta: "Подключиться к пабу",
menuCommunity: "Сообщество",
activityGroupCommunity: "Сообщество и управление", activityGroupCommunity: "Сообщество и управление",
activityGroupOrg: "Организация", activityGroupOrg: "Организация",
activityGroupComm: "Коммуникация", activityGroupComm: "Коммуникация",

View file

@ -1,6 +1,15 @@
const { a, em, strong } = require('../../../server/node_modules/hyperaxe'); const { a, em, strong } = require('../../../server/node_modules/hyperaxe');
module.exports = { module.exports = {
zh: { zh: {
bbAdd: "添加",
bbQuickActions: "快捷操作",
bbPickTitle: "固定一个模块",
bbPickDesc: "为底部栏的 + 位置选择一个模块。",
bbPickNone: "无",
activityChangeFilter: "更改筛选",
emptyConnectHint: "连接到一个 pub 以与网络同步。",
emptyConnectCta: "连接到 pub",
menuCommunity: "社区",
activityGroupCommunity: "社区与治理", activityGroupCommunity: "社区与治理",
activityGroupOrg: "组织", activityGroupOrg: "组织",
activityGroupComm: "沟通", activityGroupComm: "沟通",

View file

@ -112,6 +112,42 @@ const renderContentActions = (msgId, viewHref) => {
}; };
exports.renderContentActions = renderContentActions; 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 current = cfg.bottomBarPin || '';
const pick = (key, glyph, labelText) => form({ method: 'POST', action: '/settings/bottombar', class: 'bb-pick-form' },
input({ type: 'hidden', name: 'pin', value: key }),
button({ type: 'submit', class: current === key ? 'bb-pick-btn active' : 'bb-pick-btn' },
span({ class: 'bb-pick-ico' }, glyph),
span({ class: 'bb-pick-lbl' }, labelText)
)
);
const items = exports.PINNABLE_MODULES.filter(m => modOn(m.mod)).map(m => pick(m.key, m.glyph, i18n[m.labelKey] || m.key));
return template(
i18n.bbPickTitle || 'Pin a module',
section(
div({ class: 'tags-header' },
h2(i18n.bbPickTitle || 'Pin a module'),
p(i18n.bbPickDesc || 'Choose a module for the + slot in the bottom bar.')
),
div({ class: 'bb-pick-grid' },
pick('', '✕', i18n.bbPickNone || 'None'),
...items
)
)
);
};
exports.renderStateChip = renderStateChip; exports.renderStateChip = renderStateChip;
exports.renderOpenClosedChip = renderOpenClosedChip; exports.renderOpenClosedChip = renderOpenClosedChip;
exports.renderVisibilityChip = renderVisibilityChip; exports.renderVisibilityChip = renderVisibilityChip;
@ -1080,24 +1116,76 @@ 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" })) 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: '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 renderBottomBar = () => {
const inboxN = sharedState.getInboxCount(); 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( const bbLink = (href, ico, label, badge) => a(
{ class: "bb-item", href }, { class: "bb-item", href },
span({ class: "bb-ico" }, ico), span({ class: "bb-ico" }, ico),
(badge && badge > 0) ? span({ class: "bb-badge" }, String(badge)) : "", (badge && badge > 0) ? span({ class: "bb-badge" }, String(badge)) : "",
span({ class: "bb-lbl" }, label) span({ class: "bb-lbl" }, label)
); );
const pinned = PINNABLE_MODULES.find(m => m.key === (cfg.bottomBarPin || '') && modOn(m.mod));
const pinSlot = pinned
? bbLink(pinned.href, pinned.glyph, i18n[pinned.labelKey] || pinned.key)
: a({ class: "bb-item bb-add", href: "/settings/bottombar" },
span({ class: "bb-ico" }, ""),
span({ class: "bb-lbl" }, i18n.bbAdd || "Add"));
const pair = [ bbLink("/peers", "⧖", i18n.peers || "Peers") ];
if (modOn('invitesMod')) pair.push(bbLink("/invites", "ꔮ", i18n.invites || "Invites"));
return nav( return nav(
{ class: "oasis-bottombar", style: "display:none", "aria-label": "Quick actions" }, { class: "oasis-bottombar", style: "display:none", "aria-label": i18n.bbQuickActions || "Quick actions" },
bbLink("/search", "ꔅ", i18n.searchTitle || "Buscar"), bbLink("/search", "ꔅ", i18n.searchTitle || "Search"),
bbLink("/inbox", "☂", i18n.inbox || "Inbox", inboxN), bbLink("/inbox", "☂", i18n.inbox || "Inbox", inboxN),
bbLink("/publish", "❂", i18n.publish || "Write"), bbLink("/publish", "❂", i18n.publish || "Write"),
bbLink("/activity", "ꔙ", i18n.activityTitle || "Activity"), pinSlot,
div({ class: "bb-pair" }, div({ class: "bb-pair" }, ...pair)
bbLink("/peers", "⧖", i18n.peers || "Peers"),
bbLink("/invites", "ꔮ", i18n.invites || "Invites")
)
); );
}; };
@ -3402,7 +3490,7 @@ exports.privateView = async (messagesInput, filter, decrypted = null) => {
threadGroups[tid].push(msg) 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 => { return threadOrder.map(tid => {
const msgs = threadGroups[tid] const msgs = threadGroups[tid]