feat(rmeets): register rMeets as rApp with Jitsi embed

Adds rMeets module with hub page (Quick Meet, Join Room, Jitsi Lobby)
and room pages that embed jeffsi.localvibe.live via renderExternalAppShell.
Jitsi URL configurable via JITSI_URL env var.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-09 19:54:10 -07:00
parent d45aaabea7
commit 0cf1db56f4
2 changed files with 118 additions and 0 deletions

116
modules/rmeets/mod.ts Normal file
View File

@ -0,0 +1,116 @@
/**
* rMeets module video meetings powered by Jitsi.
*
* Hub page with Quick Meet + room name input,
* room pages embed Jitsi via renderExternalAppShell.
*/
import { Hono } from "hono";
import { renderShell, renderExternalAppShell, escapeHtml } from "../../server/shell";
import { getModuleInfoList } from "../../shared/module";
import type { RSpaceModule } from "../../shared/module";
const JITSI_URL = process.env.JITSI_URL || "https://jeffsi.localvibe.live";
const routes = new Hono();
// ── Room embed ──
routes.get("/room/:room", (c) => {
const space = c.req.param("space") || "demo";
const room = c.req.param("room");
return c.html(renderExternalAppShell({
title: `${room} — rMeets | rSpace`,
moduleId: "rmeets",
spaceSlug: space,
modules: getModuleInfoList(),
appUrl: `${JITSI_URL}/${encodeURIComponent(room)}`,
appName: "Jitsi Meet",
theme: "dark",
}));
});
// ── Direct Jitsi lobby ──
routes.get("/meet", (c) => {
const space = c.req.param("space") || "demo";
return c.html(renderExternalAppShell({
title: `Jitsi Meet — rMeets | rSpace`,
moduleId: "rmeets",
spaceSlug: space,
modules: getModuleInfoList(),
appUrl: JITSI_URL,
appName: "Jitsi Meet",
theme: "dark",
}));
});
// ── Hub page ──
routes.get("/", (c) => {
const space = c.req.param("space") || "demo";
const base = `/${escapeHtml(space)}/rmeets`;
const randomId = Math.random().toString(36).slice(2, 10);
return c.html(renderShell({
title: `rMeets — ${space} | rSpace`,
moduleId: "rmeets",
spaceSlug: space,
modules: getModuleInfoList(),
theme: "dark",
styles: `<style>
.rs-hub{max-width:720px;margin:3rem auto;padding:0 1.5rem}
.rs-hub h1{font-size:1.8rem;margin-bottom:.5rem}
.rs-hub p{color:var(--rs-text-secondary,#aaa);margin-bottom:2rem}
.rs-nav{display:flex;flex-direction:column;gap:1rem}
.rs-nav a,.rs-nav .room-form{display:flex;align-items:center;gap:1rem;padding:1.25rem 1.5rem;border-radius:12px;background:var(--rs-surface,#1e1e2e);border:1px solid var(--rs-border,#333);text-decoration:none;color:inherit;transition:border-color .15s,background .15s}
.rs-nav a:hover,.rs-nav .room-form:hover{border-color:var(--rs-accent,#14b8a6);background:var(--rs-surface-hover,#252538)}
.rs-nav .nav-icon{font-size:2rem;flex-shrink:0}
.rs-nav .nav-body h3{margin:0 0 .25rem;font-size:1.1rem}
.rs-nav .nav-body p{margin:0;font-size:.85rem;color:var(--rs-text-secondary,#aaa)}
.room-form{flex-wrap:wrap}
.room-form input[type="text"]{flex:1;min-width:140px;padding:.5rem .75rem;border-radius:8px;border:1px solid var(--rs-border,#444);background:var(--rs-bg,#161625);color:inherit;font-size:.95rem}
.room-form button{padding:.5rem 1.25rem;border-radius:8px;border:none;background:var(--rs-accent,#14b8a6);color:#fff;font-weight:600;cursor:pointer;font-size:.95rem}
.room-form button:hover{opacity:.85}
</style>`,
body: `<div class="rs-hub">
<h1>rMeets</h1>
<p>Video meetings powered by Jitsi no account required</p>
<nav class="rs-nav">
<a href="${base}/room/${randomId}">
<span class="nav-icon">🚀</span>
<div class="nav-body">
<h3>Quick Meet</h3>
<p>Start an instant meeting with a random room name</p>
</div>
</a>
<form class="room-form" onsubmit="event.preventDefault();var n=this.querySelector('input').value.trim();if(n)location.href='${base}/room/'+encodeURIComponent(n)">
<span class="nav-icon">🔗</span>
<div class="nav-body">
<h3>Join a Room</h3>
<p>Enter a room name to join or create a meeting</p>
</div>
<input type="text" placeholder="room-name" required>
<button type="submit">Join</button>
</form>
<a href="${base}/meet">
<span class="nav-icon">📹</span>
<div class="nav-body">
<h3>Jitsi Lobby</h3>
<p>Open the full Jitsi Meet interface directly</p>
</div>
</a>
</nav>
</div>`,
}));
});
// ── Module export ──
export const meetsModule: RSpaceModule = {
id: "rmeets",
name: "rMeets",
icon: "📹",
description: "Video meetings powered by Jitsi",
scoping: { defaultScope: "space", userConfigurable: false },
routes,
externalApp: { url: JITSI_URL, name: "Jitsi Meet" },
};

View File

@ -66,6 +66,7 @@ import { dataModule } from "../modules/rdata/mod";
import { splatModule } from "../modules/rsplat/mod";
import { photosModule } from "../modules/rphotos/mod";
import { socialsModule } from "../modules/rsocials/mod";
import { meetsModule } from "../modules/rmeets/mod";
// import { docsModule } from "../modules/rdocs/mod";
// import { designModule } from "../modules/rdesign/mod";
import { scheduleModule } from "../modules/rschedule/mod";
@ -111,6 +112,7 @@ registerModule(socialsModule);
// registerModule(docsModule); // placeholder — not yet an rApp
// registerModule(designModule); // placeholder — not yet an rApp
registerModule(scheduleModule);
registerModule(meetsModule);
// ── Config ──
const PORT = Number(process.env.PORT) || 3000;