Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
submodules: recursive
Comment thread
kixelated marked this conversation as resolved.
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'
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "moq"]
path = moq
url = https://github.com/moq-dev/moq.git
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions api/src/room.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ export class Context {
this.#env = env;
}
// Returns the path/token to join the room
async sign(room: Name): Promise<string> {
async sign(room: Name, identity: string): Promise<string> {
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}`;
}

Expand Down Expand Up @@ -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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const url = new URL(token, ctx.env.RELAY_URL);
return c.json({ url, path, guest: !c.var.account_id ? path : undefined });
},
Expand Down
19 changes: 11 additions & 8 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,22 @@
"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": {
"@fabianlars/tauri-plugin-oauth": "2",
"@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",
Expand Down Expand Up @@ -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"
}
2 changes: 1 addition & 1 deletion app/src/about.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion app/src/account.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
4 changes: 2 additions & 2 deletions app/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class Client {
const headers: Record<string, string> = token ? { Authorization: `Bearer ${token}` } : {};
this.routes = hc<Api.App>(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);
Expand All @@ -43,7 +43,7 @@ export class Client {
this.routes = hc<Api.App>(import.meta.env.VITE_API_URL, { headers });
});

this.signals.effect((effect) => {
this.signals.run((effect) => {
const authenticated = effect.get(this.#authenticated);
if (!authenticated) return;

Expand Down
6 changes: 3 additions & 3 deletions app/src/components/meme-selector.tsx
Original file line number Diff line number Diff line change
@@ -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<string>;
Expand Down
8 changes: 4 additions & 4 deletions app/src/components/profile.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);

Expand All @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion app/src/components/tutorial.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
60 changes: 29 additions & 31 deletions app/src/controls.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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";
Expand All @@ -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)
Expand Down Expand Up @@ -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);

Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
};

Expand Down Expand Up @@ -424,7 +424,7 @@ export function Microphone(props: { local: Local }): JSX.Element {
<Match when={available()}>
{(devices) => (
<>
{devices().map((dev) => (
{devices().map((dev: MediaDeviceInfo) => (
<button
type="button"
onClick={() => selectDevice(dev.deviceId)}
Expand Down Expand Up @@ -463,19 +463,19 @@ export function Microphone(props: { local: Local }): JSX.Element {

export function Camera(props: { local: Local; room?: Room }): JSX.Element {
const toggle = () => {
props.local.webcam.enabled.update((prev: boolean) => !prev);
props.local.core.cameraEnabled.update((prev: boolean) => !prev);
};
const media = solid(props.local.webcam.source);
const media = solid(props.local.webcam.out.source);

const [showMenu, setShowMenu] = createSignal(false);
const [deviceChangeIndicator, setDeviceChangeIndicator] = createSignal(false);
const [deviceChangeMessage, setDeviceChangeMessage] = createSignal("");

// Use device signals from the Device API
const device = props.local.webcam.device;
const available = solid(device.available);
const requested = createSelector(solid(device.requested));
const active = createSelector(solid(device.active));
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;
Expand Down Expand Up @@ -525,14 +525,14 @@ export function Camera(props: { local: Local; room?: Room }): JSX.Element {

// Handle device selection
const selectDevice = (deviceId: string) => {
if (media() && (deviceId === device.active.peek() || deviceId === device.requested.peek())) {
if (media() && (deviceId === device.out.active.peek() || deviceId === device.out.requested.peek())) {
// Same device selected and camera is enabled - disable it
props.local.webcam.enabled.set(false);
props.local.core.cameraEnabled.set(false);
device.preferred.set(undefined);
} else {
// Different device or camera is disabled - enable it
device.preferred.set(deviceId);
props.local.webcam.enabled.set(true);
props.local.core.cameraEnabled.set(true);
}
};

Expand Down Expand Up @@ -605,7 +605,7 @@ export function Camera(props: { local: Local; room?: Room }): JSX.Element {
<Match when={available()}>
{(devices) => (
<>
{devices().map((dev) => (
{devices().map((dev: MediaDeviceInfo) => (
<button
type="button"
onClick={() => selectDevice(dev.deviceId)}
Expand Down Expand Up @@ -645,9 +645,9 @@ export function Camera(props: { local: Local; room?: Room }): JSX.Element {

function Screen(props: { local: Local; room: Room }): JSX.Element {
const toggle = () => {
props.local.screen.enabled.update((prev: boolean) => !prev);
props.local.core.screenEnabled.update((prev: boolean) => !prev);
};
const media = solid(props.local.screen.source);
const media = solid(props.local.screen.out.source);

// Check if getDisplayMedia is supported
const supportsScreenShare = "mediaDevices" in navigator && "getDisplayMedia" in navigator.mediaDevices;
Expand Down Expand Up @@ -675,7 +675,7 @@ function Screen(props: { local: Local; room: Room }): JSX.Element {
}

// Renders a volume meter in the background of an element.
export function Visualize(props: { audio: Publish.Audio.Encoder }): JSX.Element {
export function Visualize(props: { audio: { root: Getter<AudioNode | undefined> } }): JSX.Element {
const [power, setPower] = createSignal<number | undefined>(undefined);

const top = createMemo(() => {
Expand Down Expand Up @@ -739,18 +739,15 @@ export function Visualize(props: { audio: Publish.Audio.Encoder }): JSX.Element
);
}

function Chat(props: { broadcast: HangPublishBroadcast; room: Room }): JSX.Element {
function Chat(props: { broadcast: HangLocalSource; room: Room }): JSX.Element {
const [input, setInput] = createSignal<HTMLInputElement | undefined>(undefined);
const [message, setMessage] = createSignal("");
const [showMemeSelector, setShowMemeSelector] = createSignal(false);

// Update typing status in preview
createEffect(() => {
const hasText = message().length > 0;
props.broadcast.preview.info.update((prev) => ({
...prev,
typing: hasText,
}));
props.room.local.core.typing.set(hasText);
});

const keydown = (e: KeyboardEvent) => {
Expand Down Expand Up @@ -793,6 +790,7 @@ function Chat(props: { broadcast: HangPublishBroadcast; room: Room }): JSX.Eleme

// Use a function to avoid the dequal check.
props.broadcast.chat.message.latest.update(() => m);
props.room.local.core.chatting.set(true);

setMessage("");
};
Expand Down
4 changes: 2 additions & 2 deletions app/src/home.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as Moq from "@moq/lite";
import solid from "@moq/signals/solid";
import * as Moq from "@moq/net";
import { createAccessor as solid } from "@moq/signals/solid";
import { createEffect, createResource, createSignal, For, Match, onCleanup, Show, Switch } from "solid-js";
import type { JSX } from "solid-js/jsx-runtime";
import * as Api from "./api";
Expand Down
2 changes: 1 addition & 1 deletion app/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ if (Tauri.DESKTOP) {
import("./tauri/update");
}

import solid from "@moq/signals/solid";
import { createAccessor as solid } from "@moq/signals/solid";
import { Route, Router, useLocation } from "@solidjs/router";
import { onCleanup, Show } from "solid-js";
import type { JSX } from "solid-js/jsx-runtime";
Expand Down
Loading
Loading