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
15 changes: 15 additions & 0 deletions app/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,18 @@ main {
input[type="range"] {
accent-color: hsl(var(--link-hue), 75%, 50%);
}

/* Locator throb animation */
@keyframes throb {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}

.animate-throb {
animation: throb 2s ease-in-out infinite;
}
3 changes: 2 additions & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"scripts": {
"dev": "bunx --bun vite --open",
"build": "bunx --bun vite build",
"check": "tsc --noEmit && biome check",
"check": "tsc --noEmit && biome check && bun run check:shaders",
"check:shaders": "glslangValidator src/room/gl/shaders/*.vert src/room/gl/shaders/*.frag",
"fix": "biome check --fix",
"tauri": "tauri"
},
Expand Down
25 changes: 17 additions & 8 deletions app/src/about.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import solid from "@kixelated/signals/solid";
import { createEffect, createSignal, type JSX, onCleanup } from "solid-js";
import AudioPrompt from "./components/audio-prompt";
import CreateHang from "./components/create";
import DemoHeader from "./components/demo-header";
import Layout from "./layout/web";
import { Canvas } from "./room/canvas";
import { FakeRoom } from "./room/fake";

export function About(): JSX.Element {
const canvas = <canvas class="border-3 border-link-hue rounded-lg w-full h-full" id="demo" />;

const room = new FakeRoom(new Canvas(canvas as HTMLCanvasElement, { demo: true }));
const room = new FakeRoom(new Canvas(canvas as HTMLCanvasElement));
onCleanup(() => room.close());

const audioSuspended = solid(room.sound.suspended);

const handleEnableAudio = () => {
room.sound.enabled.set(true);
};

const services = ["Meet", "Zoom", "Teams", "Discord", "Skype", "WebEx", "FaceTime", "WhatsApp"];
const [currentService, setCurrentService] = createSignal(0);

Expand Down Expand Up @@ -109,11 +118,11 @@ export function About(): JSX.Element {
() => {},
() => {
two.user.name.set("omni-chan");
two.user.avatar.set("/avatar/omni.jpg");
two.show(new URL("/avatar/omni.jpg", import.meta.url));
},
() => two.chat.typing.active.set(true),
() => two.chat.message.latest.set("oops wrong button"),
() => two.user.avatar.set("/avatar/43.svg"),
() => two.stop(),
() => three.chat.typing.active.set(true),
() => three.chat.message.latest.set("dude"),
() => two.play(new URL("/meme/linus.mp4", import.meta.url)),
Expand Down Expand Up @@ -251,17 +260,17 @@ export function About(): JSX.Element {
</div>
</div>

<div class="sm:m-8 m-4 h-128 w-full">{canvas}</div>
<div class="sm:m-8 m-4 h-128 w-full relative">
<DemoHeader />
<AudioPrompt show={audioSuspended()} onClick={handleEnableAudio} />
{canvas}
</div>

<p>
Powered by new and <a href="https://github.com/kixelated/moq">open source</a> web tech:{" "}
<a href="https://moq.dev">MoQ</a>. There's more to live than another {services[currentService()]}{" "}
clone. <i>Crazy</i>, I know.
</p>

<div class="flex my-18">
<img src="/image/we-are/5.svg" alt="we are live" class="max-w-120 w-full" />
</div>
</div>
</Layout>
);
Expand Down
4 changes: 2 additions & 2 deletions app/src/account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ function AccountLoad(): JSX.Element {
<div class="bg-gray-900 border border-gray-700 rounded-2xl p-8 max-w-md mx-4">
<h2 class="text-2xl font-bold mb-4 text-red-400">Delete Account?</h2>
<p class="text-gray-300 mb-6">
This action cannot be undone. Your account, profile information, and all associated data will be
permanently deleted.
This action cannot be undone. Your account, profile information, and all associated data
will be permanently deleted.
</p>
<div class="flex gap-4">
<button
Expand Down
21 changes: 21 additions & 0 deletions app/src/components/audio-prompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Show } from "solid-js";
import type { JSX } from "solid-js/jsx-runtime";

export default function AudioPrompt(props: { show: boolean; onClick: () => void }): JSX.Element {
return (
<Show when={props.show}>
<div class="absolute bottom-0 left-0 right-0 flex items-center justify-center z-10 m-4">
<button
type="button"
onClick={props.onClick}
class="backdrop-blur-sm rounded-2xl px-8 py-4 text-white transition-all shadow-2xl hover:scale-105 cursor-pointer"
>
<div class="flex items-center gap-3">
<span class="icon-[mdi--volume-mute] w-6 h-6 text-red-500" />
<span class="text-lg font-semibold">Click to enable audio</span>
</div>
</button>
</div>
</Show>
);
}
16 changes: 11 additions & 5 deletions app/src/components/badge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ import * as Tauri from "../tauri";

async function set(count: number | undefined) {
if (Tauri.Api) {
await Tauri.Api.window
const success = await Tauri.Api.window
.getCurrentWindow()
.setBadgeCount(count || undefined)
.catch((error) => console.warn("Failed to set Tauri badge:", error));
} else if (navigator.setAppBadge) {
await navigator
.then(() => true)
.catch(() => false);
if (success) return;
}

if (navigator.setAppBadge) {
const success = await navigator
.setAppBadge(count || undefined)
.catch((error) => console.warn("Failed to set Web badge:", error));
.then(() => true)
.catch(() => false);
if (success) return;
}
}

Expand Down
9 changes: 9 additions & 0 deletions app/src/components/demo-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { JSX } from "solid-js/jsx-runtime";

export default function DemoHeader(): JSX.Element {
return (
<div class="absolute top-0 left-0 m-4 z-10 px-4 py-2 bg-black/70 backdrop-blur-sm rounded-lg">
<div class="text-2xl font-bold text-white/90 underline decoration-link-hue underline-offset-2">DEMO</div>
</div>
);
}
2 changes: 1 addition & 1 deletion app/src/components/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ class LocalPreview {

constructor(element: HTMLCanvasElement, camera: Publish.Broadcast) {
// Create a minimal canvas without the background effects
this.canvas = new Canvas(element, { demo: false });
this.canvas = new Canvas(element);

// Create a minimal sound context (muted for preview)
this.sound = new Sound();
Expand Down
2 changes: 1 addition & 1 deletion app/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { Canvas } from "./room/canvas";
import { Sup } from "./sup";

export function Hang(): JSX.Element {
const background = (<canvas class="fixed inset-0 w-full h-full" />) as HTMLCanvasElement;
const background = (<canvas class="fixed inset-0 w-full h-full bg-black" />) as HTMLCanvasElement;
const canvas = new Canvas(background);
onCleanup(() => canvas.close());

Expand Down
77 changes: 56 additions & 21 deletions app/src/privacy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,41 @@ export default function Privacy() {
<p class="text-sm text-gray-600 mb-8">Last Updated: October 2, 2025</p>

<section class="mb-8">
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">Philosophy</h2>
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">
Philosophy
</h2>
<p>
We believe in the right to privacy.
Have fun and be weird; you're not being judged (by us at least).
We believe in the right to privacy. Have fun and be weird; you're not being judged (by us at
least).
</p>
</section>

<section class="mb-8">
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">What We Collect</h2>
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">
What We Collect
</h2>
<p class="mb-4">We collect minimal information to provide our service:</p>
<ul class="list-disc pl-6 space-y-2">
<li>
<strong>Account Information</strong>: When you sign in with a linked provider (ex. Google, Discord, or Apple), we store
your email address, display name, and avatar. You can replace your name and avatar at any time.
<strong>Account Information</strong>: When you sign in with a linked provider (ex. Google,
Discord, or Apple), we store your email address, display name, and avatar. You can replace
your name and avatar at any time.
</li>
<li>
<strong>Session State</strong>: An authentication token and any user preferences are stored in your browser's local storage. It is cleared when you log out.
<strong>Session State</strong>: An authentication token and any user preferences are stored
in your browser's local storage. It is cleared when you log out.
</li>
<li>
<strong>Media Cache</strong>: We cache seconds worth of media to improve the playback experience. It is cleared immediately after disconnecting.
<strong>Media Cache</strong>: We cache seconds worth of media to improve the playback
experience. It is cleared immediately after disconnecting.
</li>
</ul>
</section>

<section class="mb-8">
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">What We Don't Collect</h2>
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">
What We Don't Collect
</h2>
<p class="mb-4">We do not:</p>
<ul class="list-disc pl-6 space-y-2">
<li>Store any video/audio/conversations</li>
Expand All @@ -44,7 +53,9 @@ export default function Privacy() {
</section>

<section class="mb-8">
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">How We Use Your Information</h2>
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">
How We Use Your Information
</h2>
<p class="mb-4">Your account information is used to:</p>
<ul class="list-disc pl-6 space-y-2">
<li>Identify you when you're logged in</li>
Expand All @@ -53,7 +64,9 @@ export default function Privacy() {
</section>

<section class="mb-8">
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">Data Storage</h2>
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">
Data Storage
</h2>
<ul class="list-disc pl-6 space-y-2">
<li>Account information is stored securely on our servers</li>
<li>Authentication tokens are stored locally in your browser</li>
Expand All @@ -62,15 +75,19 @@ export default function Privacy() {
</section>

<section class="mb-8">
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">Third-Party Authentication</h2>
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">
Third-Party Authentication
</h2>
<p>
We use Google, Discord, and Apple for sign-in. When you authenticate, you're subject to their
privacy policies. We only receive basic profile information with your consent.
</p>
</section>

<section class="mb-8">
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">Data Sharing</h2>
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">
Data Sharing
</h2>
<ul class="list-disc pl-6 space-y-2">
<li>We do not sell, rent, or share your information</li>
<li>We do not display ads or work with advertisers</li>
Expand All @@ -79,7 +96,9 @@ export default function Privacy() {
</section>

<section class="mb-8">
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">Hang Privacy</h2>
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">
Hang Privacy
</h2>
<ul class="list-disc pl-6 space-y-2">
<li>All hangs are public to anyone with the URL</li>
<li>Do not share sensitive information in hangs</li>
Expand All @@ -89,24 +108,40 @@ export default function Privacy() {
</section>

<section class="mb-8">
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">Your Rights</h2>
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">
Your Rights
</h2>
<ul class="list-disc pl-6 space-y-2">
<li>Delete your account from the <a href="/account" class="text-blue-400 hover:underline">account settings page</a></li>
<li>
Delete your account from the{" "}
<a href="/account" class="text-blue-400 hover:underline">
account settings page
</a>
</li>
<li>Logout or clear browser storage to remove any local state</li>
<li>Request a copy of your stored information by contacting <a href="mailto:admin@hang.live">admin@hang.live</a></li>
<li>
Request a copy of your stored information by contacting{" "}
<a href="mailto:admin@hang.live">admin@hang.live</a>
</li>
</ul>
</section>

<section class="mb-8">
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">Changes to This Policy</h2>
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">
Changes to This Policy
</h2>
<p>We'll notify users via email of significant policy changes.</p>
</section>

<section class="mb-8">
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">Contact</h2>
<p>For questions: <a href="mailto:admin@hang.live">admin@hang.live</a></p>
<h2 class="text-2xl font-semibold mb-4 underline decoration-link-hue underline-offset-2">
Contact
</h2>
<p>
For questions: <a href="mailto:admin@hang.live">admin@hang.live</a>
</p>
</section>
</div>
</Layout>
)
);
}
Loading
Loading