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:
parent
3937ddab1e
commit
096a7d110b
30 changed files with 266 additions and 1918 deletions
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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>');
|
||||
|
|
@ -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" }),
|
||||
|
|
|
|||
|
|
@ -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"/>',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue