diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index f70caa4a..f5970df9 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -16,8 +16,11 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + submodules: recursive + persist-credentials: false - uses: DeterminateSystems/nix-installer-action@main - uses: DeterminateSystems/magic-nix-cache-action@main - uses: DeterminateSystems/flake-checker-action@main - - run: nix develop --command just check + - run: nix develop --command bash -c 'just check && just build' diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..41b4bc78 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "moq"] + path = moq + url = https://github.com/moq-dev/moq.git diff --git a/README.md b/README.md index 41d20197..ff20cafc 100644 --- a/README.md +++ b/README.md @@ -79,3 +79,10 @@ Hang uses [moq](https://github.com/moq-dev/moq) — an open-source library for r ## License Dual-licensed under [MIT](LICENSE-MIT) or [Apache 2.0](LICENSE-APACHE), at your option. + +## MoQ source dependency + +This migration pins MoQ as the `moq` submodule until the room package is released. +Clone with `git clone --recurse-submodules`, or run `git submodule update --init` +in an existing checkout, then `bun install --frozen-lockfile`. +The app uses the submodule's MoQ workspaces, so CI and local builds use the same source. 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..13045a75 100644 --- a/app/package.json +++ b/app/package.json @@ -9,7 +9,8 @@ "build": "bunx --bun vite build", "check": "tsc --noEmit && biome check", "fix": "biome check --fix", - "tauri": "tauri" + "tauri": "tauri", + "test": "bun test src/room/metadata.test.ts" }, "license": "MIT", "dependencies": { @@ -17,12 +18,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": "workspace:*", + "@moq/json": "workspace:*", + "@moq/net": "workspace:*", + "@moq/publish": "workspace:*", + "@moq/room": "workspace:*", + "@moq/signals": "workspace:*", + "@moq/watch": "workspace:*", "@solidjs/router": "^0.15.3", "@tailwindcss/vite": "^4.1.12", "@tauri-apps/api": "^2.8.0", @@ -61,7 +63,8 @@ "vite-plugin-solid": "^2.11.8", "vite-plugin-static-copy": "^4.0.0", "vite-plugin-wrangler": "^0.1.1", - "wrangler": "^4.33.0" + "wrangler": "^4.33.0", + "@types/bun": "^1.4.0" }, "packageManager": "pnpm@10.13.1+sha512.37ebf1a5c7a30d5fabe0c5df44ee8da4c965ca0c5af3dbab28c3a1681b70a256218d05c81c9c0dcf767ef6b8551eb5b960042b9ed4300c59242336377e01cfad" } diff --git a/app/src/about.tsx b/app/src/about.tsx index 1560afd0..3212eb78 100644 --- a/app/src/about.tsx +++ b/app/src/about.tsx @@ -1,4 +1,4 @@ -import solid from "@moq/signals/solid"; +import { createAccessor as solid } from "@moq/signals/solid"; import { createEffect, createSignal, type JSX, onCleanup } from "solid-js"; import AudioPrompt from "./components/audio-prompt"; import CreateHang from "./components/create"; diff --git a/app/src/account.tsx b/app/src/account.tsx index 97984072..12e8a71e 100644 --- a/app/src/account.tsx +++ b/app/src/account.tsx @@ -1,4 +1,4 @@ -import solid from "@moq/signals/solid"; +import { createAccessor as solid } from "@moq/signals/solid"; import { createEffect, createMemo, createSignal, Match, onCleanup, onMount, Show, Switch } from "solid-js"; import type { JSX } from "solid-js/jsx-runtime"; import * as Api from "./api"; 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..4c5051c0 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 { createAccessor as 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..29ac8005 100644 --- a/app/src/components/profile.tsx +++ b/app/src/components/profile.tsx @@ -1,11 +1,11 @@ import { Effect } from "@moq/signals"; -import solid from "@moq/signals/solid"; +import { createAccessor as solid } from "@moq/signals/solid"; 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/components/tutorial.tsx b/app/src/components/tutorial.tsx index 6ded467c..e9d41e9f 100644 --- a/app/src/components/tutorial.tsx +++ b/app/src/components/tutorial.tsx @@ -1,4 +1,4 @@ -import solid from "@moq/signals/solid"; +import { createAccessor as solid } from "@moq/signals/solid"; import { onCleanup, onMount, Show } from "solid-js"; import type { JSX } from "solid-js/jsx-runtime"; import Settings from "../settings"; diff --git a/app/src/controls.tsx b/app/src/controls.tsx index 9d4bace1..3f1b0d6d 100644 --- a/app/src/controls.tsx +++ b/app/src/controls.tsx @@ -1,5 +1,5 @@ -import type * as Publish from "@moq/publish"; -import solid from "@moq/signals/solid"; +import type { Getter } from "@moq/signals"; +import { createAccessor as solid } from "@moq/signals/solid"; import { type Accessor, createEffect, @@ -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) => (