karvan.js solo marcaba como propios los que llegaban despues de abrir la sala; los que pintaba el servidor salian todos como de otra persona.
123 lines
6.6 KiB
JavaScript
123 lines
6.6 KiB
JavaScript
"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 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" },
|
||
span({ class: "kl-dot" }),
|
||
span({ class: "kl-txt", id: "karvan-live-txt" }, 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" },
|
||
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" },
|
||
button({ type: "button", class: "kc-btn kc-btn-start", id: "kc-start" }, "📞 " + (i18n.karvanCall || "Call")),
|
||
button({ type: "button", class: "kc-btn", id: "kc-mic", "aria-label": "mic" }, "🎤"),
|
||
button({ type: "button", class: "kc-btn", id: "kc-cam", "aria-label": "camera" }, "🎥"),
|
||
button({ type: "button", class: "kc-btn kc-btn-stop", id: "kc-stop", "aria-label": "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" },
|
||
input({ type: "text", name: "to", placeholder: i18n.karvanInvitePlaceholder || "Invite a contact by @feed-id…", autocomplete: "off", spellcheck: "false" }),
|
||
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>');
|