[0.9.1] Mobile: fixed topbar, global hive, per-message chat reply/switcher, forum subtopics, square module grid

- Topbar: single fixed line [logo][Personal][Community][avatar] in one dark
  tone (#121212) with gold accents only; the hive hexagons scroll below it.
- Hive: Personal/Community filter the home hive from any page; opening a
  category shows its modules as a 2x2 grid of square buttons (a lone odd
  button is centered instead of left-aligned).
- Chats: per-message reply (?replyTo=), chat switcher strip and quote banner;
  the message rate limit is lifted on mobile.
- Forums: optional subtopics (?topic=) with threaded rendering.
- Bottombar: fixed Inbox - PM - Write - Search - Graphos - Peers.
This commit is contained in:
s1to 2026-08-05 23:36:18 +02:00
parent 2b2c65fc7a
commit ed9f01e8e6
8 changed files with 280 additions and 129 deletions

View file

@ -1103,12 +1103,19 @@ const renderTasksLink = () => {
// === Oasis UX (portado de 0.8.7_UX): topbar + bottombar + hive ===
const renderMobileTopbar = () => div(
{ class: "oasis-mobile-topbar" },
a({ class: "omt-logo", href: "/" }, img({ src: "/assets/images/snh-oasis.jpg", alt: "Oasis" })),
a({ class: "omt-title", href: "/" }, "OASIS"),
a({ class: "omt-avatar", href: "/profile" }, img({ src: (typeof global !== "undefined" && global.__OASIS_SELF_AVATAR__) || "/assets/images/default-avatar.png", alt: i18n.profile || "Profile" }))
);
// topbar fija arriba: [logo] [Personal] [Community] [avatar] en una línea (sin título OASIS)
const renderMobileTopbar = () => {
const hiveFilter = (typeof global !== "undefined" && global.__OASIS_HIVE_FILTER__) || "";
return div(
{ class: "oasis-mobile-topbar" },
a({ class: "omt-logo", href: "/" }, img({ src: "/assets/images/snh-oasis.jpg", alt: "Oasis" })),
div({ class: "omt-menus" },
a({ class: hiveFilter === "personal" ? "oasis-menu-btn active" : "oasis-menu-btn", href: hiveFilter === "personal" ? "/activity" : "/activity?hive=personal" }, span(i18n.menuPersonal || "Personal")),
a({ class: hiveFilter === "community" ? "oasis-menu-btn active" : "oasis-menu-btn", href: hiveFilter === "community" ? "/activity" : "/activity?hive=community" }, span(i18n.menuCommunity || "Community"))
),
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 },
@ -1170,26 +1177,18 @@ const renderBottomBar = () => {
(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"));
// barra inferior FIJA (estilo epsylon): Inbox · PM · Write · Search · Graphos · Peers
const items = [
bbLink("/inbox", "☂", "Inbox", inboxN),
bbLink("/pm", "✉", "PM"),
bbLink("/publish", "✎", "Write"),
bbLink("/search", "ꔅ", "Search"),
bbLink("/graphos", "◈", "Graphos"),
bbLink("/peers", "⧖", "Peers"),
];
return nav(
{ class: "oasis-bottombar", "aria-label": i18n.bbQuickActions || "Quick actions" },
...slots,
editSlot,
div({ class: "bb-pair" }, ...pair)
{ class: "oasis-bottombar oasis-bottombar-fixed", "aria-label": i18n.bbQuickActions || "Quick actions" },
...items
);
};
@ -1292,33 +1291,8 @@ const template = (titlePrefix, ...elements) => {
div(
{ class: uxMode === "ainav" ? "header ainav-only" : "header" },
renderMobileTopbar(),
div(
{ class: "oasis-mobile-menus" },
// enlaces (no checkboxes): funcionan en CUALQUIER página → navegan al home /activity con el filtro.
// toggle: si el filtro ya está activo, el enlace lleva a /activity (se deselecciona → todos)
a({ class: hiveFilter === "personal" ? "oasis-menu-btn active" : "oasis-menu-btn", href: hiveFilter === "personal" ? "/activity" : "/activity?hive=personal" }, span(i18n.menuPersonal || "Personal")),
a({ class: hiveFilter === "community" ? "oasis-menu-btn active" : "oasis-menu-btn", href: hiveFilter === "community" ? "/activity" : "/activity?hive=community" }, span(i18n.menuCommunity || "Community"))
),
(() => {
const aiNavOn = getConfig().modules.aiNavMod === 'on';
if (!aiNavOn && uxMode !== 'ainav') return null;
return div(
{ class: uxMode === 'ainav' ? "top-bar-center top-bar-center-ainav" : "top-bar-center" },
form(
{ method: 'POST', action: '/ai/ask', class: 'ai-ask-form' },
input({
type: 'text',
name: 'q',
class: 'ai-ask-input',
placeholder: i18n.aiNavPlaceholder || 'Where do you want to go?',
autocomplete: 'off',
maxlength: '300',
autofocus: uxMode === 'ainav' ? 'autofocus' : undefined
}),
button({ type: 'submit', class: 'ai-ask-btn' }, '➤')
)
);
})()
// hive global: los hexágonos salen en TODAS las páginas y SCROLLEAN bajo la topbar fija (antes solo en /activity)
renderHiveNav()
),
(() => {
const updateFlagPath = path.join(__dirname, '../server/.update_required');