[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
149
src/models/karvan_model.js
Normal file
149
src/models/karvan_model.js
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
"use strict";
|
||||
// Módulo KARVAN (self-contained) para Oasis Mobile — inspirado en karvan-protocol
|
||||
// (packages/core/addons/ephemeral.js): salas EFÍMERAS en RAM que se AUTODESTRUYEN por TTL.
|
||||
// - Nada se escribe a disco. Los mensajes viven solo en memoria y desaparecen con la sala.
|
||||
// - TTL de inactividad (idle) que se resetea con cada actividad + TTL absoluto que nunca se resetea.
|
||||
// - Mailbox RAM para señalización WebRTC (data-channel) entre pares.
|
||||
// No depende de @karvan/core ni de cripto nativa: es una capa local sobre el backend de Oasis.
|
||||
|
||||
const crypto = require("crypto");
|
||||
|
||||
const DEFAULT_IDLE_TTL_MS = 30 * 60 * 1000; // 30 min sin actividad -> autodestrucción
|
||||
const DEFAULT_ABS_TTL_MS = 2 * 60 * 60 * 1000; // 2 h absoluto (tope)
|
||||
const MAX_ABS_TTL_MS = 24 * 60 * 60 * 1000; // nunca más de 24 h
|
||||
const MAX_MSGS = 250; // anillo: solo los últimos N en RAM
|
||||
const MAX_SIGNALS = 500;
|
||||
const MAX_ROOMS = 100;
|
||||
|
||||
module.exports = ({ idleTtlMs = DEFAULT_IDLE_TTL_MS, absTtlMs = DEFAULT_ABS_TTL_MS } = {}) => {
|
||||
const rooms = new Map(); // id -> room
|
||||
|
||||
const publicRoom = (r) => ({
|
||||
id: r.id, title: r.title, createdAt: r.createdAt, expiresAt: r.expiresAt,
|
||||
count: r.messages.length, members: r.members.size,
|
||||
});
|
||||
|
||||
const resetIdle = (room) => {
|
||||
if (room.idleTimer) clearTimeout(room.idleTimer);
|
||||
room.idleTimer = setTimeout(() => destroy(room.id), idleTtlMs);
|
||||
if (room.idleTimer.unref) room.idleTimer.unref();
|
||||
};
|
||||
|
||||
const destroy = (id) => {
|
||||
const room = rooms.get(id);
|
||||
if (!room) return;
|
||||
clearTimeout(room.idleTimer);
|
||||
clearTimeout(room.absTimer);
|
||||
// AUTODESTRUCCIÓN: vaciar todo el material en RAM
|
||||
room.messages.length = 0;
|
||||
room.signals.length = 0;
|
||||
room.members.clear();
|
||||
rooms.delete(id);
|
||||
};
|
||||
|
||||
const createRoom = ({ title = "", ttlMinutes } = {}) => {
|
||||
if (rooms.size >= MAX_ROOMS) {
|
||||
// desalojar la más antigua para no crecer sin límite
|
||||
const oldest = [...rooms.values()].sort((a, b) => a.createdAt - b.createdAt)[0];
|
||||
if (oldest) destroy(oldest.id);
|
||||
}
|
||||
const id = crypto.randomBytes(9).toString("hex"); // 18 hex
|
||||
const createdAt = Date.now();
|
||||
let absMs = absTtlMs;
|
||||
const t = parseInt(ttlMinutes, 10);
|
||||
if (Number.isFinite(t) && t > 0) absMs = Math.min(t * 60000, MAX_ABS_TTL_MS);
|
||||
const room = {
|
||||
id,
|
||||
title: String(title || "").slice(0, 80),
|
||||
createdAt,
|
||||
expiresAt: createdAt + absMs,
|
||||
messages: [],
|
||||
signals: [],
|
||||
members: new Set(),
|
||||
seq: 0,
|
||||
sigSeq: 0,
|
||||
idleTimer: null,
|
||||
absTimer: null,
|
||||
};
|
||||
rooms.set(id, room);
|
||||
room.absTimer = setTimeout(() => destroy(id), absMs);
|
||||
if (room.absTimer.unref) room.absTimer.unref();
|
||||
resetIdle(room);
|
||||
return publicRoom(room);
|
||||
};
|
||||
|
||||
const listRooms = () =>
|
||||
[...rooms.values()].map(publicRoom).sort((a, b) => b.createdAt - a.createdAt);
|
||||
|
||||
const getRoom = (id) => {
|
||||
const r = rooms.get(id);
|
||||
return r ? publicRoom(r) : null;
|
||||
};
|
||||
|
||||
const postMessage = (id, { from = "?", text = "", mid = "" } = {}) => {
|
||||
const room = rooms.get(id);
|
||||
if (!room) return null;
|
||||
const clean = String(text || "").slice(0, 2000);
|
||||
if (!clean) return null;
|
||||
const msg = {
|
||||
seq: ++room.seq,
|
||||
mid: String(mid || "").slice(0, 40),
|
||||
from: String(from || "?").slice(0, 80),
|
||||
text: clean,
|
||||
ts: Date.now(),
|
||||
};
|
||||
room.messages.push(msg);
|
||||
if (room.messages.length > MAX_MSGS) room.messages.shift();
|
||||
room.members.add(msg.from);
|
||||
resetIdle(room);
|
||||
return msg;
|
||||
};
|
||||
|
||||
const listMessages = (id, since = 0) => {
|
||||
const room = rooms.get(id);
|
||||
if (!room) return null;
|
||||
const s = parseInt(since, 10) || 0;
|
||||
return room.messages.filter((m) => m.seq > s);
|
||||
};
|
||||
|
||||
// --- señalización WebRTC (mailbox RAM) ---
|
||||
const postSignal = (id, { from = "", to = "", payload = null } = {}) => {
|
||||
const room = rooms.get(id);
|
||||
if (!room) return null;
|
||||
const sig = {
|
||||
seq: ++room.sigSeq,
|
||||
from: String(from || "").slice(0, 80),
|
||||
to: String(to || "").slice(0, 80),
|
||||
payload,
|
||||
ts: Date.now(),
|
||||
};
|
||||
room.signals.push(sig);
|
||||
if (room.signals.length > MAX_SIGNALS) room.signals.shift();
|
||||
if (sig.from) room.members.add(sig.from);
|
||||
resetIdle(room);
|
||||
return sig;
|
||||
};
|
||||
|
||||
const getSignals = (id, { forId = "", since = 0 } = {}) => {
|
||||
const room = rooms.get(id);
|
||||
if (!room) return null;
|
||||
const s = parseInt(since, 10) || 0;
|
||||
return {
|
||||
signals: room.signals.filter(
|
||||
(x) => x.seq > s && x.from !== forId && (!x.to || x.to === forId)
|
||||
),
|
||||
members: [...room.members].filter((m) => m !== forId),
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
createRoom,
|
||||
listRooms,
|
||||
getRoom,
|
||||
postMessage,
|
||||
listMessages,
|
||||
postSignal,
|
||||
getSignals,
|
||||
_destroy: destroy,
|
||||
};
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue