[0.9.1] FORK_IA_UX v2: fix top-bar (visibility sheet) + Karvan module (ephemeral messages + WebRTC)
Navigation fixes: - The bottom "Explore" sheet no longer blocks the top bar: closed state is visibility:hidden (out of hit-testing), so the top identity buttons and page content always receive taps (fixes "top buttons don't work"). - Top bar is now identity only (logo, avatar). The Personal/Community filter moved into the Explore sheet, next to the hive, as a segmented control. New module "Karvan" (self-contained; inspired by karvan-protocol ephemeral rooms): - Ephemeral/temporary chat rooms held only in RAM, self-destructing on idle (30 min) / absolute (2 h) TTL — nothing is written to disk. - Server relay chat (RAM + polling) as the reliable path, plus a WebRTC data-channel layer (perfect negotiation, HTTP signaling mailbox) for instant P2P delivery; degrades to the relay if WebRTC is unavailable. Data-channel only (no camera/mic) so no wrapper permission changes are needed. - Files: models/karvan_model.js, views/karvan_view.js, client/public/js/karvan.js; routes /karvan* + karvanMod config + nav entry (network) + karvanTitle i18n (11 langs). Additive and gated by OASIS_MOBILE / karvanMod; Linux and the UX_OASIS branch untouched.
This commit is contained in:
parent
745c340917
commit
e938c8bb42
19 changed files with 592 additions and 8 deletions
91
src/views/karvan_view.js
Normal file
91
src/views/karvan_view.js
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
"use strict";
|
||||
// Vista del módulo KARVAN (mensajes temporales + WebRTC). El chat va por relay servidor (RAM,
|
||||
// autodestrucción) y, si conectan pares, por data-channel WebRTC (instantáneo). El <script> solo
|
||||
// se carga en la página de sala; el WebRTC usa data-channel (sin cámara/mic => sin permisos extra).
|
||||
|
||||
const { div, section, h2, h3, p, a, form, input, button, span, ul, li, script, label } =
|
||||
require("../server/node_modules/hyperaxe");
|
||||
const { template, i18n } = require("./main_views");
|
||||
|
||||
const fmtExpires = (expiresAt) => {
|
||||
const mins = Math.max(0, Math.round((expiresAt - Date.now()) / 60000));
|
||||
if (mins >= 60) return `${Math.floor(mins / 60)}h ${mins % 60}m`;
|
||||
return `${mins}m`;
|
||||
};
|
||||
|
||||
const shortId = (id) => {
|
||||
id = String(id || "?");
|
||||
return id.startsWith("@") ? id.slice(1, 7) : id.slice(0, 6);
|
||||
};
|
||||
exports.karvanShortId = shortId;
|
||||
|
||||
const renderMsg = (m) =>
|
||||
li({ class: "karvan-msg", "data-seq": String(m.seq), "data-mid": m.mid || "" },
|
||||
span({ class: "km-from" }, shortId(m.from)),
|
||||
span({ class: "km-text" }, m.text)
|
||||
);
|
||||
|
||||
const ttlOpt = (val, lbl, checked) =>
|
||||
label({ class: "karvan-ttl-opt" },
|
||||
input(Object.assign({ type: "radio", name: "ttl", value: val }, checked ? { checked: "checked" } : {})),
|
||||
span(lbl)
|
||||
);
|
||||
|
||||
exports.karvanView = (rooms, params = {}) =>
|
||||
template(
|
||||
i18n.karvanTitle || "Karvan",
|
||||
section({ class: "karvan-intro" },
|
||||
h2("Karvan · " + (i18n.karvanEphemeral || "Ephemeral rooms")),
|
||||
p({ class: "karvan-sub" },
|
||||
i18n.karvanBlurb ||
|
||||
"Temporary rooms that self-destruct. Messages live only in memory — never saved — and vanish when the room expires. Real-time over WebRTC when a peer connects.")
|
||||
),
|
||||
section({ class: "karvan-create" },
|
||||
form({ method: "POST", action: "/karvan/create" },
|
||||
input({ type: "text", name: "title", placeholder: i18n.karvanRoomName || "Room name (optional)", maxlength: "80", autocomplete: "off" }),
|
||||
div({ class: "karvan-ttl" },
|
||||
span({ class: "karvan-ttl-lbl" }, i18n.karvanTtl || "Self-destruct in:"),
|
||||
ttlOpt("30", "30m", true), ttlOpt("120", "2h", false), ttlOpt("480", "8h", false)
|
||||
),
|
||||
button({ type: "submit" }, i18n.karvanCreate || "Create ephemeral room")
|
||||
)
|
||||
),
|
||||
section({ class: "karvan-list" },
|
||||
h3(i18n.karvanActive || "Active rooms"),
|
||||
rooms && rooms.length
|
||||
? ul({ class: "karvan-rooms" },
|
||||
...rooms.map((r) =>
|
||||
li(
|
||||
a({ href: "/karvan/" + r.id, class: "karvan-room-item" },
|
||||
span({ class: "kr-title" }, r.title || "room " + r.id.slice(0, 6)),
|
||||
span({ class: "kr-meta" }, `${r.count} · ⏳ ${fmtExpires(r.expiresAt)}`)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
: p({ class: "karvan-empty" }, i18n.karvanNone || "No active rooms. Create one — it disappears on its own.")
|
||||
)
|
||||
);
|
||||
|
||||
exports.karvanRoomView = (room, messages, params = {}) =>
|
||||
template(
|
||||
"Karvan · " + (room.title || room.id.slice(0, 6)),
|
||||
section({ class: "karvan-room", "data-room": room.id, "data-self": params.selfId || "" },
|
||||
div({ class: "karvan-room-head" },
|
||||
a({ href: "/karvan", class: "karvan-back" }, "‹ " + (i18n.karvanRooms || "Rooms")),
|
||||
span({ class: "karvan-room-title" }, room.title || "room " + room.id.slice(0, 6)),
|
||||
span({ class: "karvan-expire", id: "karvan-expire" }, "⏳ " + fmtExpires(room.expiresAt))
|
||||
),
|
||||
div({ class: "karvan-live", id: "karvan-live" },
|
||||
span({ class: "kl-dot" }),
|
||||
span({ class: "kl-txt", id: "karvan-live-txt" }, i18n.karvanRelay || "server relay")
|
||||
),
|
||||
ul({ class: "karvan-msgs", id: "karvan-msgs" }, ...(messages || []).map(renderMsg)),
|
||||
form({ class: "karvan-compose", method: "POST", action: "/karvan/" + room.id + "/msg", id: "karvan-form" },
|
||||
input({ type: "hidden", name: "from", value: params.selfId || "" }),
|
||||
input({ type: "text", name: "text", id: "karvan-text", placeholder: i18n.karvanSay || "Temporary message…", maxlength: "2000", autocomplete: "off" }),
|
||||
button({ type: "submit" }, i18n.karvanSend || "Send")
|
||||
)
|
||||
),
|
||||
script({ src: "/js/karvan.js", defer: "defer" })
|
||||
);
|
||||
|
|
@ -1105,14 +1105,12 @@ const renderTasksLink = () => {
|
|||
// === Oasis UX (portado de 0.8.7_UX): topbar + bottombar + hive ===
|
||||
// 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__) || "";
|
||||
// FORK_IA_UX v2: topbar SOLO identidad (logo→home, avatar→perfil). El filtro Personal/Community
|
||||
// se movió a la hoja "Explore" (junto al hive). Así el top no tiene botones "huérfanos" que confundan.
|
||||
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"))
|
||||
),
|
||||
div({ class: "omt-spacer" }),
|
||||
a({ class: "omt-avatar", href: "/profile" }, img({ src: (typeof global !== "undefined" && global.__OASIS_SELF_AVATAR__) || "/assets/images/default-avatar.png", alt: i18n.profile || "Profile" }))
|
||||
);
|
||||
};
|
||||
|
|
@ -1124,6 +1122,7 @@ const PINNABLE_MODULES = [
|
|||
{ 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: 'karvan', href: '/karvan', glyph: '⧗', labelKey: 'karvanTitle', mod: 'karvanMod' },
|
||||
{ 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' },
|
||||
|
|
@ -1225,7 +1224,8 @@ const renderHiveNav = () => {
|
|||
{ 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' } ] },
|
||||
{ href: '/maps', glyph: 'ꔌ', labelKey: 'mapsLabel', mod: 'mapsMod' }, { href: '/chats', glyph: 'ꖒ', labelKey: 'chatsTitle', mod: 'chatsMod' },
|
||||
{ href: '/karvan', glyph: '⧗', labelKey: 'karvanTitle', mod: 'karvanMod' } ] },
|
||||
{ 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' },
|
||||
|
|
@ -1274,6 +1274,16 @@ const renderHiveSheet = () => {
|
|||
label({ class: "hive-sheet-backdrop", for: "fork-explore-toggle", "aria-label": "Close" }),
|
||||
div({ class: "hive-sheet-panel" },
|
||||
label({ class: "hive-sheet-grip", for: "fork-explore-toggle", "aria-label": "Close" }),
|
||||
// filtro Personal/Community/Todo (movido desde el topbar; filtra el hive server-side via ?hive=)
|
||||
(() => {
|
||||
const hf = (typeof global !== "undefined" && global.__OASIS_HIVE_FILTER__) || "";
|
||||
const seg = (href, lbl, on) => a({ class: on ? "hs-seg active" : "hs-seg", href }, lbl);
|
||||
return div({ class: "hive-sheet-seg" },
|
||||
seg("/activity", i18n.menuAll || "All", hf === ""),
|
||||
seg("/activity?hive=personal", i18n.menuPersonal || "Personal", hf === "personal"),
|
||||
seg("/activity?hive=community", i18n.menuCommunity || "Community", hf === "community")
|
||||
);
|
||||
})(),
|
||||
// accesos que no están en el hive (CATS): red y estado
|
||||
div({ class: "hive-sheet-quick" },
|
||||
q("/peers", "⧖", "Peers"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue