From 348819c139b46846e703a0c610b27e24fea80da5 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 12 Sep 2026 09:49:37 -0700 Subject: [PATCH 1/4] feat: switch conferencing onto @moq/room Roster, local camera/screen, token claims, and user/preview metadata come from @moq/room. Location, JSON chat, 3D space, and memes stay hang.live. Tokens now put only under the participant identity. Until @moq/room is on npm, package.json overrides point at the local moq worktree. Co-Authored-By: grok-4.6 --- api/package.json | 2 +- api/src/room.ts | 8 +- app/package.json | 13 +- app/src/api.ts | 4 +- app/src/components/meme-selector.tsx | 4 +- app/src/components/profile.tsx | 6 +- app/src/controls.tsx | 58 ++- app/src/home.tsx | 2 +- app/src/layout/app.tsx | 19 +- app/src/layout/logo.tsx | 2 +- app/src/preview.tsx | 10 +- app/src/room/audio.ts | 12 +- app/src/room/broadcast.ts | 10 +- app/src/room/canvas.ts | 2 +- app/src/room/chat.ts | 14 +- app/src/room/debug.ts | 30 +- app/src/room/fake.ts | 14 +- app/src/room/index.ts | 168 ++++----- app/src/room/local.ts | 370 ++++++++---------- app/src/room/locator.ts | 14 +- app/src/room/metadata.ts | 392 +++++++------------ app/src/room/name.ts | 10 +- app/src/room/preview/member.ts | 89 +---- app/src/room/preview/room.ts | 78 +--- app/src/room/sound.ts | 4 +- app/src/room/space.ts | 30 +- app/src/room/tts/index.ts | 2 +- app/src/room/video.ts | 22 +- app/src/room/watch.ts | 189 ++++------ app/src/sup.tsx | 5 +- app/src/vite-env.d.ts | 3 + app/tsconfig.json | 16 +- app/vite.config.ts | 1 + bun.lock | 540 ++++++++++++++++++++++----- package.json | 14 +- 35 files changed, 1082 insertions(+), 1075 deletions(-) diff --git a/api/package.json b/api/package.json index 04e6248d..432b1e1b 100644 --- a/api/package.json +++ b/api/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@hono/zod-validator": "^0.7.2", - "@moq/token": "^0.2.3", + "@moq/token": "^0.3.3", "drizzle-orm": "^0.44.5", "hono": "^4.9.4", "jose": "^6.0.13", diff --git a/api/src/room.ts b/api/src/room.ts index 06b48a46..175826d3 100644 --- a/api/src/room.ts +++ b/api/src/room.ts @@ -19,10 +19,10 @@ export class Context { this.#env = env; } // Returns the path/token to join the room - async sign(room: Name): Promise { + async sign(room: Name, identity: string): Promise { const root = `${this.#env.RELAY_PREFIX}/${room}`; - // TODO add a field to force publishing, preventing someone from lurking. - const token = await Token.sign(this.#key, { root, get: "", put: "" }); + const put = identity.endsWith("/") ? identity : `${identity}/`; + const token = await Token.sign(this.#key, { root, get: "", put }); return `${root}/?jwt=${token}`; } @@ -53,7 +53,7 @@ export const router = rpc const ctx = c.var.ctx; const room = c.req.valid("param").room; const path = c.var.account_id ?? c.req.valid("json").guest ?? `guest/${Uuid.v4()}`; - const token = await ctx.room.sign(room); + const token = await ctx.room.sign(room, path); const url = new URL(token, ctx.env.RELAY_URL); return c.json({ url, path, guest: !c.var.account_id ? path : undefined }); }, diff --git a/app/package.json b/app/package.json index dc8148a8..b334a595 100644 --- a/app/package.json +++ b/app/package.json @@ -17,12 +17,13 @@ "@fontsource-variable/montserrat": "^5.2.6", "@hang/api": "workspace:*", "@huggingface/transformers": "^4.0.0", - "@moq/hang": "^0.2.12", - "@moq/json": "^0.1.1", - "@moq/lite": "^0.3.0", - "@moq/publish": "^0.2.16", - "@moq/signals": "^0.1.10", - "@moq/watch": "^0.2.18", + "@moq/hang": "0.4.3", + "@moq/json": "0.3.3", + "@moq/net": "0.3.5", + "@moq/publish": "0.4.7", + "@moq/room": "0.1.0", + "@moq/signals": "0.2.3", + "@moq/watch": "0.5.4", "@solidjs/router": "^0.15.3", "@tailwindcss/vite": "^4.1.12", "@tauri-apps/api": "^2.8.0", diff --git a/app/src/api.ts b/app/src/api.ts index 79e03d31..d3e8a756 100644 --- a/app/src/api.ts +++ b/app/src/api.ts @@ -33,7 +33,7 @@ export class Client { const headers: Record = token ? { Authorization: `Bearer ${token}` } : {}; this.routes = hc(import.meta.env.VITE_API_URL, { headers }); - this.signals.effect((effect: Effect) => { + this.signals.run((effect: Effect) => { // TODO async verify the token is valid const token = effect.get(Settings.oauth.token); this.#authenticated.set(!!token); @@ -43,7 +43,7 @@ export class Client { this.routes = hc(import.meta.env.VITE_API_URL, { headers }); }); - this.signals.effect((effect) => { + this.signals.run((effect) => { const authenticated = effect.get(this.#authenticated); if (!authenticated) return; diff --git a/app/src/components/meme-selector.tsx b/app/src/components/meme-selector.tsx index 34c3ae5b..16ab823e 100644 --- a/app/src/components/meme-selector.tsx +++ b/app/src/components/meme-selector.tsx @@ -1,14 +1,14 @@ import solid from "@moq/signals/solid"; import { createEffect, createSignal, For, onCleanup, onMount, type Setter, Show } from "solid-js"; import type { JSX } from "solid-js/jsx-runtime"; +import type { HangLocalSource } from "../room/local"; import * as Meme from "../room/meme"; -import type { HangPublishBroadcast } from "../room/metadata"; import Settings from "../settings"; export type Tab = "emoji" | "audio" | "video"; export type MemeSelectorProps = { - broadcast: HangPublishBroadcast; + broadcast: HangLocalSource; chatInput: HTMLInputElement | undefined; chatMessage: string; setChatMessage: Setter; diff --git a/app/src/components/profile.tsx b/app/src/components/profile.tsx index bdf2141e..4da917c1 100644 --- a/app/src/components/profile.tsx +++ b/app/src/components/profile.tsx @@ -4,8 +4,8 @@ import { createSignal, JSX, onCleanup, Show } from "solid-js"; import * as Api from "../api"; import { Camera, Microphone } from "../controls"; import { Canvas } from "../room/canvas"; +import type { HangLocalSource } from "../room/local"; import { Local } from "../room/local"; -import type { HangPublishBroadcast } from "../room/metadata"; import { Sound } from "../room/sound"; import { Space } from "../room/space"; import Settings from "../settings"; @@ -122,7 +122,7 @@ class LocalPreview { signals = new Effect(); - constructor(element: HTMLCanvasElement, camera: HangPublishBroadcast) { + constructor(element: HTMLCanvasElement, camera: HangLocalSource) { // Create a minimal canvas without the background effects this.canvas = new Canvas(element); @@ -137,7 +137,7 @@ class LocalPreview { const broadcast = this.space.add("local", camera); this.signals.cleanup(() => this.space.remove("local")); - this.signals.effect((effect: Effect) => { + this.signals.run((effect: Effect) => { const position = effect.get(broadcast.position); if (position.x === 0 && position.y === 0 && position.s === 1) return; diff --git a/app/src/controls.tsx b/app/src/controls.tsx index 9d4bace1..40da53b7 100644 --- a/app/src/controls.tsx +++ b/app/src/controls.tsx @@ -1,4 +1,4 @@ -import type * as Publish from "@moq/publish"; +import type { Getter } from "@moq/signals"; import solid from "@moq/signals/solid"; import { type Accessor, @@ -18,8 +18,8 @@ import Tooltip from "./components/tooltip"; import { Tutorial } from "./components/tutorial"; import type { Room } from "./room"; import type { Canvas } from "./room/canvas"; +import type { HangLocalSource } from "./room/local"; import { Local } from "./room/local"; -import type { HangPublishBroadcast } from "./room/metadata"; import { Sound } from "./room/sound"; import Settings, { Modal } from "./settings"; import * as Tauri from "./tauri"; @@ -42,8 +42,8 @@ export function Controls(props: { room: Room; local: Local; canvas: Canvas }): J // Check if any publish source is active const micActive = solid(props.local.camera.audio.root); - const cameraActive = solid(props.local.webcam.source); - const screenActive = solid(props.local.screen.source); + const cameraActive = solid(props.local.webcam.out.source); + const screenActive = solid(props.local.screen.out.source); const anySourceActive = createMemo(() => !!micActive() || !!cameraActive() || !!screenActive()); // Close expanded sections when clicking outside (mobile only) @@ -252,7 +252,7 @@ export function Controls(props: { room: Room; local: Local; canvas: Canvas }): J export function Microphone(props: { local: Local }): JSX.Element { const toggle = () => { - props.local.microphone.enabled.update((prev: boolean) => !prev); + props.local.core.microphoneEnabled.update((prev: boolean) => !prev); }; const root = solid(props.local.camera.audio.root); @@ -263,10 +263,10 @@ export function Microphone(props: { local: Local }): JSX.Element { // Use device signals from the Device API const device = props.local.microphone.device; - const enabled = solid(props.local.microphone.enabled); - const available = solid(device.available); - const requested = createSelector(solid(device.requested)); - const active = createSelector(solid(device.active)); + const enabled = solid(props.local.core.microphoneEnabled); + const available = solid(device.out.available); + const requested = createSelector(solid(device.out.requested)); + const active = createSelector(solid(device.out.active)); // Watch for device changes and trigger indicator let previousDeviceCount = available()?.length ?? 0; @@ -315,14 +315,14 @@ export function Microphone(props: { local: Local }): JSX.Element { // Handle device selection const selectDevice = (deviceId: string) => { - if (root() && (deviceId === device.active.peek() || deviceId === device.requested.peek())) { + if (root() && (deviceId === device.out.active.peek() || deviceId === device.out.requested.peek())) { // Same device selected and mic is enabled - disable it - props.local.microphone.enabled.set(false); + props.local.core.microphoneEnabled.set(false); device.preferred.set(undefined); } else { // Different device or mic is disabled - enable it device.preferred.set(deviceId); - props.local.microphone.enabled.set(true); + props.local.core.microphoneEnabled.set(true); } }; @@ -424,7 +424,7 @@ export function Microphone(props: { local: Local }): JSX.Element { {(devices) => ( <> - {devices().map((dev) => ( + {devices().map((dev: MediaDeviceInfo) => (