fuera Karvan: se usan el Telefono y las Salas del dev

El dev ya trae Telefono (llamadas uno a uno, buzon de voz) y Salas (hasta 50,
con video, grabacion y emision). La rama quita su modulo propio de salas y
llamadas: modelo, vista, JS de cliente, CSS, rutas, credenciales TURN, pruebas
y textos.

- el router propio pasa a llamarse fork_routes.js, sin Karvan
- el audio, la camara y la emision de Android se enchufan desde main.js con
  setAudioFactory/setVideoFactory/setStreamFactory, que exporta el propio dev;
  SSB_server.js vuelve a tener su linea original
- Clear-SNH.css y modules_view.js vuelven a ser identicos a los del dev
- el workflow acampada y la config de serie cambian Karvan por Telefono y Salas
- README y guia: las llamadas son las del dev; antes de hacer un modulo propio,
  mirar si el dev ya lo tiene
This commit is contained in:
hacklab 2026-10-19 13:39:41 +02:00
parent 3937ddab1e
commit 096a7d110b
30 changed files with 266 additions and 1918 deletions

View file

@ -43,7 +43,6 @@ module.exports = ({ i18n }) => {
{ 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: 'rooms', href: '/rooms', glyph: 'ꕢ', labelKey: 'roomsTitle', mod: 'roomsMod' },
{ key: 'wiki', href: '/wiki', glyph: 'ꖿ', labelKey: 'wikiTitle', mod: 'wikiMod' },
{ key: 'polls', href: '/polls', glyph: 'ꖙ', labelKey: 'pollsTitle', mod: 'pollsMod' },
@ -129,7 +128,6 @@ module.exports = ({ i18n }) => {
{ href: '/activity', glyph: 'ꔙ', labelKey: 'activityTitle', mod: null },
{ href: '/feed', glyph: 'ꕿ', labelKey: 'feedTitle', mod: 'feedMod' },
{ href: '/chats', glyph: 'ꖒ', labelKey: 'chatsTitle', mod: 'chatsMod' },
{ href: '/karvan', glyph: '⧗', labelKey: 'karvanTitle', mod: 'karvanMod' },
{ href: '/rooms', glyph: 'ꕢ', labelKey: 'roomsTitle', mod: 'roomsMod' },
{ href: '/forum', glyph: 'ꕒ', labelKey: 'forumTitle', mod: 'forumMod' },
{ href: '/pads', glyph: 'ꔗ', labelKey: 'padsTitle', mod: 'padsMod' },

View file

@ -1,134 +0,0 @@
"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, video } =
require("../server/node_modules/hyperaxe");
const { template, i18n } = require("./main_views");
const { iconNode } = require("./mobile_icons");
// boton de la barra de llamada: icono y su nombre debajo (antes 🎤 🎥 ⛔ sin texto)
const callBtn = (id, cls, icon, text) =>
button({ type: "button", class: "kc-btn " + cls, id }, iconNode("kc-ico", icon), span({ class: "kc-lbl" }, text));
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);
};
// los propios se marcan ya en el servidor: karvan.js solo marca los que llegan
// despues, y al recargar la sala tus mensajes salian como de otra persona
const renderMsg = (m, selfId) =>
li({ class: "karvan-msg" + (selfId && m.from === selfId ? " karvan-msg-self" : ""), "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) =>
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",
"data-txt-relay": i18n.karvanRelayHuman || "server relay", "data-txt-p2p": i18n.karvanP2pHuman || "🔒 P2P (WebRTC)" },
span({ class: "kl-dot" }),
span({ class: "kl-txt", id: "karvan-live-txt" }, i18n.karvanRelayHuman || i18n.karvanRelay || "server relay")
),
// Fase 1: panel de videollamada (media P2P sobre el mismo WebRTC del chat). La galería
// aparece al iniciar; los botones cablean cámara/micro. En el móvil actual getUserMedia
// fallará hasta que el wrapper conceda permisos (Fases 2/3) → se avisa en #kc-status.
div({ class: "karvan-call", id: "karvan-call",
"data-txt-mic": i18n.karvanVoiceOn || "Voice on", "data-txt-cam": i18n.karvanCamOn || "Camera on",
"data-txt-nocam": i18n.karvanNoCam || "No camera permission: the call goes on with voice only.",
"data-txt-nomic": i18n.karvanNoMic || "No microphone permission. You can allow it in the phone settings; the chat still works." },
div({ class: "karvan-videos", id: "karvan-videos" },
video({ id: "karvan-local", class: "kc-video kc-local", muted: "muted", autoplay: "autoplay", playsinline: "playsinline" }),
div({ class: "kc-remote", id: "karvan-remote" })
),
div({ class: "karvan-call-bar" },
callBtn("kc-start", "kc-btn-start", "phone", i18n.karvanCall || "Call"),
callBtn("kc-mic", "", "mic", i18n.karvanMic || "Mic"),
callBtn("kc-cam", "", "camera", i18n.karvanCam || "Camera"),
callBtn("kc-stop", "kc-btn-stop", "hangup", i18n.karvanHangUp || "Hang up")
),
div({ class: "kc-status", id: "kc-status" }, "")
),
ul({ class: "karvan-msgs", id: "karvan-msgs" }, ...(messages || []).map(m => renderMsg(m, params.selfId))),
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")
),
// Fase 2: invitar a un contacto por su feed id → le llega al inbox un enlace a esta sala
form({ class: "karvan-invite", method: "POST", action: "/karvan/" + room.id + "/invite" },
label({ class: "karvan-invite-lbl", for: "karvan-invite-to" }, i18n.karvanInviteLabel || "Invite someone"),
input({ type: "text", name: "to", id: "karvan-invite-to", placeholder: i18n.karvanInvitePlaceholder || "Invite a contact by @feed-id…", autocomplete: "off", spellcheck: "false" }),
p({ class: "form-hint" }, i18n.karvanInviteHint || "Paste their @id: it is on their profile, under their name."),
button({ type: "submit" }, i18n.karvanInvite || "Invite")
),
params.invite === "ok"
? p({ class: "karvan-invite-msg ok" }, i18n.karvanInviteSent || "✓ Invitation sent to their inbox.")
: params.invite === "bad"
? p({ class: "karvan-invite-msg err" }, i18n.karvanInviteBad || "✗ That is not a valid @feed-id.")
: params.invite === "fail"
? p({ class: "karvan-invite-msg err" }, i18n.karvanInviteFail || "✗ Could not send the invitation.")
: ""
),
script({ src: "/js/karvan.js", defer: "defer" })
// sin JavaScript no hay data-channel ni sondeo, asi que la sala se refresca sola.
// Va en <noscript> para que con JS no recargue encima del chat en vivo.
// Mismo recurso que usa upstream en indexing_view.
).replace('</head>', '<noscript><meta http-equiv="refresh" content="10"></noscript></head>');

View file

@ -1101,17 +1101,6 @@ const navGroup = ({ id, emoji, title, defaultOpen = false }, ...items) => {
const renderKarvanLink = () => {
const karvanMod = getConfig().modules.karvanMod === "on";
return karvanMod
? navLink({
href: "/karvan",
emoji: "⧗",
text: i18n.karvanTitle,
class: "karvan-link enabled"
})
: "";
};
const renderPollsLink = () => {
const pollsMod = getConfig().modules.pollsMod === "on";
@ -2020,9 +2009,6 @@ const template = (titlePrefix, ...elements) => {
...(process.env.OASIS_MOBILE === '1'
? [ link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}` }), acampadaBase(theme), themeLink, acampadaLink(theme) ]
: [ themeLink, link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}`, media: "(max-width: 768px)" }) ]),
// el modulo Karvan trae su propia hoja: asi se ve igual en cualquier tema
// y tambien en escritorio, donde no se carga OasisMobile.css
link({ rel: "stylesheet", href: `/assets/styles/karvan.css?v=${assetVersion()}` }),
process.env.OASIS_MOBILE === '1' ? link({ rel: "stylesheet", href: `/assets/styles/mobile_ux.css?v=${assetVersion()}` }) : null,
link({ rel: "icon", href: "/assets/images/favicon.svg" }),
meta({ charset: "utf-8" }),
@ -2348,8 +2334,7 @@ const template = (titlePrefix, ...elements) => {
renderWikiLink(),
renderMapsLink(),
renderRoomsLink(),
renderChatsLink(),
renderKarvanLink()
renderChatsLink()
),
navGroup(
{
@ -2412,9 +2397,6 @@ exports.ainavHomeView = ({ recentTags = [] } = {}) => {
...(process.env.OASIS_MOBILE === '1'
? [ link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}` }), acampadaBase(theme), link({ rel: "stylesheet", href: `/assets/themes/${theme}.css?v=${assetVersion()}` }), acampadaLink(theme) ]
: [ link({ rel: "stylesheet", href: `/assets/themes/${theme}.css?v=${assetVersion()}` }), link({ rel: "stylesheet", href: `/assets/styles/mobile.css?v=${assetVersion()}`, media: "(max-width: 768px)" }) ]),
// el modulo Karvan trae su propia hoja: asi se ve igual en cualquier tema
// y tambien en escritorio, donde no se carga OasisMobile.css
link({ rel: "stylesheet", href: `/assets/styles/karvan.css?v=${assetVersion()}` }),
process.env.OASIS_MOBILE === '1' ? link({ rel: "stylesheet", href: `/assets/styles/mobile_ux.css?v=${assetVersion()}` }) : null,
link({ rel: "icon", href: "/assets/images/favicon.svg" }),
meta({ charset: "utf-8" }),

View file

@ -14,7 +14,6 @@ const ICONS = {
activity: '<polyline points="3 12 7 12 10 5 14 19 17 12 21 12"/>',
feed: '<path d="M4 6h16M4 12h16M4 18h10"/>',
chats: '<path d="M21 11.5a8.5 8.5 0 0 1-12.3 7.6L3 20.5l1.4-5A8.5 8.5 0 1 1 21 11.5z"/>',
karvan: '<path d="M21 11.5a8.5 8.5 0 0 1-12.3 7.6L3 20.5l1.4-5A8.5 8.5 0 1 1 21 11.5z"/><path d="M12 7.5v4l2.5 2"/>',
rooms: '<path d="M6 21V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v17M3 21h18"/><circle cx="14.5" cy="12" r="1"/>',
forum: '<path d="M14 9a2 2 0 0 1-2 2H6l-3 3V4a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2z"/><path d="M18 9h1a2 2 0 0 1 2 2v10l-3-3h-6a2 2 0 0 1-2-2v-1"/>',
pads: '<path d="M14 3H6a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><path d="M14 3v6h6M8 13h8M8 17h5"/>',

View file

@ -1,5 +1,5 @@
"use strict";
// Pantalla de Modulos del movil (la sirve el router propio, karvan_routes.js).
// Pantalla de Modulos del movil (la sirve el router propio, fork_routes.js).
//
// Los modulos agrupados como en el lanzador, cada uno una fila que se toca entera con
// un interruptor, y Guardar siempre a la vista. La tabla de upstream eran 52 filas
@ -20,7 +20,7 @@ const STEMS = {
bookmarks: 'Bookmarks', calendars: 'Calendars', chats: 'Chats', cipher: 'Cipher', courts: 'Courts', dev: 'Dev',
docs: 'Docs', events: 'Events', favorites: 'Favorites', blogs: 'Blogs', fediverse: 'Fediverse', feed: 'Feed',
files: 'Files', forum: 'Forum', games: 'Games', graphos: 'Graphos', housing: 'Housing', images: 'Images',
industry: 'Industry', invites: 'Invites', jobs: 'Jobs', karvan: 'Karvan', larp: 'Larp', backup: 'Backup',
industry: 'Industry', invites: 'Invites', jobs: 'Jobs', larp: 'Larp', backup: 'Backup',
logs: 'Logs', maps: 'Map', market: 'Market', melody: 'Melody', opinions: 'Opinions', pads: 'Pads',
parliament: 'Parliament', pixelia: 'Pixelia', polls: 'Polls', projects: 'Projects', reports: 'Reports', school: 'School',
shops: 'Shops', tags: 'Tags', tasks: 'Tasks', torrents: 'Torrents', transfers: 'Transfers', trending: 'Trending',

View file

@ -39,7 +39,6 @@ const modulesView = () => {
{ name: 'industry', label: i18n.modulesIndustryLabel, description: i18n.modulesIndustryDescription },
{ name: 'invites', label: i18n.modulesInvitesLabel, description: i18n.modulesInvitesDescription },
{ name: 'jobs', label: i18n.modulesJobsLabel, description: i18n.modulesJobsDescription },
{ name: 'karvan', label: i18n.modulesKarvanLabel, description: i18n.modulesKarvanDescription },
{ name: 'larp', label: i18n.modulesLarpLabel, description: i18n.modulesLarpDescription },
{ name: 'backup', label: i18n.modulesBackupLabel, description: i18n.modulesBackupDescription },
{ name: 'logs', label: i18n.modulesLogsLabel, description: i18n.modulesLogsDescription },
@ -67,17 +66,13 @@ const modulesView = () => {
{ name: 'wiki', label: i18n.modulesWikiLabel, description: i18n.modulesWikiDescription }
].sort((x, y) => String(x.label || x.name).localeCompare(String(y.label || y.name), undefined, { sensitivity: 'base' }));
const MOBILE_UNAVAILABLE = ['ai', 'aiNav', 'dev', 'games', 'logs', 'maps', 'pixelia'];
const isMobile = process.env.OASIS_MOBILE === '1';
const visibleModules = isMobile ? modules.filter(mod => !MOBILE_UNAVAILABLE.includes(mod.name)) : modules;
const moduleStates = visibleModules.reduce((acc, mod) => {
const moduleStates = modules.reduce((acc, mod) => {
acc[`${mod.name}Mod`] = config[`${mod.name}Mod`] === 'on' ? 'on' : 'off';
return acc;
}, {});
const activeModulesCount = visibleModules.filter(mod => moduleStates[`${mod.name}Mod`] === 'on').length;
const totalModulesCount = visibleModules.length;
const activeModulesCount = modules.filter(mod => moduleStates[`${mod.name}Mod`] === 'on').length;
const totalModulesCount = modules.length;
const disabledModulesCount = totalModulesCount - activeModulesCount;
const header = [div({ class: 'tags-header module-header-line' },
@ -105,7 +100,7 @@ const modulesView = () => {
const presetButtons = div({ class: 'preset-group' },
Object.entries(PRESETS).map(([key, mods]) => {
const presetLabel = (i18n[`modulesPreset_${key}`] || key).toUpperCase();
const isActive = visibleModules.every(m => mods.includes(m.name) === (moduleStates[`${m.name}Mod`] === 'on'));
const isActive = modules.every(m => mods.includes(m.name) === (moduleStates[`${m.name}Mod`] === 'on'));
return form({ action: "/modules/preset", method: "post", class: "modules-preset-form" },
input({ type: "hidden", name: "preset", value: key }),
button({
@ -120,7 +115,8 @@ const modulesView = () => {
i18n.modules,
section(header),
section(
...(isMobile ? [] : [h2(i18n.modulesPresetTitle || "Common Configurations"), presetButtons]),
h2(i18n.modulesPresetTitle || "Common Configurations"),
presetButtons,
counts,
form(
{ action: "/save-modules", method: "post" },
@ -131,7 +127,7 @@ const modulesView = () => {
td(i18n.modulesModuleDescription),
td({ class: 'module-status-cell' }, i18n.modulesModuleStatus)
),
visibleModules.map(mod =>
modules.map(mod =>
tr(
td(a({ href: `/${mod.name}` }, mod.label)),
td(p(mod.description)),