Skip to content

Hang UI as a separate package - Issue 695#717

Merged
kixelated merged 65 commits intomoq-dev:mainfrom
jdreetz:package-hang-ui
Dec 5, 2025
Merged

Hang UI as a separate package - Issue 695#717
kixelated merged 65 commits intomoq-dev:mainfrom
jdreetz:package-hang-ui

Conversation

@jdreetz
Copy link
Collaborator

@jdreetz jdreetz commented Nov 24, 2025

What does it change?

This PR addresses #695. It migrates the Watch and Publish UIs to a separate hang-ui package, that provides two Web Components (hang-publish-ui and hang-watch-ui). Internally, the Web Components use SolidJS to render and interact with the elements.

Web Components

The Web Components expect to wrap their respective hang Web Components (hang-publish for hang-publish-ui and hang-watch for hang-watch-ui). In order to use the existing Signal reactivity, the UI components will wait until they have a reference to the respective Hang...Instance (published as a CustomEvent: watch-instance-available, publish-instance-available), that provides information about the state of the watch/publish. To interact with the Signals on the Instances, we had to make some of their properties public: video, audio, signals, file.

Context Provider

Once the Instance properties are ready, we use a SolidJS Context to expose the Instance properties to the UI components:

  • WatchUIContextProvider.tsx
  • PublishUIContextProvider.tsx

I'm open to using the Instance Signals directly in the components, but we still need something that waits for the Instance to become in the wrapped component (for some reason it's not available immediately) before the UI components can use them. Personally, I think it's helpful to keep the binding layers (Context) separate from the UI component implementation, as it reduces the noise in the components and allows the developer to focus on the markup and interactions, but I'm open to other design ideas.

How to test

  • Run the dev command (nix develop -c just dev) to start your local dev
  • Open the "publish" demo page
  • Verify the controls are displayed under the preview element
  • Verify the controls look and function the same as on main
  • Open the index demo page
  • Verify the controls are displayed under the preview element
  • Verify the controls look and function the same as on main

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 24, 2025

Walkthrough

Introduces a new TypeScript SolidJS package @kixelated/hang-ui (package.json, rollup build, tsconfig, CSS typings, README) implementing many Solid components, context providers, styles, and two distributable element bundles. Registers web components hang-watch-ui and hang-publish-ui and integrates them into the demo (imports, HTML, Vite config, workspace). Updates core hang to dispatch instance-available events, expose former private signals as public fields (signals, video, audio, file), add videoDevice/audioDevice setters on HangPublish, and re-export Time from the index.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: migrating Hang UI to a separate package, referencing the associated issue.
Description check ✅ Passed The description is directly related to the changeset, explaining the migration of Watch and Publish UIs to a new hang-ui package with Web Components, context providers, and testing instructions.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

<!-- It's optional to provide a video element to preview the outgoing media. -->
<video style="width: 100%; height: auto; border-radius: 4px; margin: 0 auto;" muted autoplay></video>
</hang-publish>
<hang-publish-ui url="%VITE_RELAY_URL%" path="me"></hang-publish-ui>
Copy link
Collaborator

@kixelated kixelated Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should have:

<hang-publish-ui>
  <hang-publish><hang-publish>
<hang-publish-ui>

That way we don't need to duplicate all of the attributes, and can make attributes that only apply to the UI (ex. the location of various controls).

Of course it's more annoying to implement but that's web components baybee.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was rendering <hang-publish> within the web component for hang-publish-ui because it's easier to get a ref to the hang-publish el, but I see what you're saying about having to duplicate all the attributes. I don't think it'll be an issue to get the ref using query selectors though.

@@ -0,0 +1,12 @@
{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Give biome a try; it's sooo much better than prettier IMO.

"rollup": "^4.53.3",
"rollup-plugin-string": "^3.0.0",
"solid-element": "^1.9.1",
"solid-js": "^1.9.10",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

your choice if you use solids. I'd be down with trying out other frameworks

"exports": {
".": "./src/index.ts",
"./zod": "./src/zod.ts"
".": "./dist/index.js",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we can avoid this change because it makes local testing (via link) more difficult.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was having difficulties getting the types for HangPublish in the UI without this change. Is there another way to do that? Maybe I can avoid using the types, but it'd make things easier to have access to them.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got around this requirement by using esbuild to do the typescript handling in Rollup.

@jdreetz
Copy link
Collaborator Author

jdreetz commented Nov 27, 2025

@kixelated do we want to do the "Meet" UI as well?

…by using esbuild instead of typescript rollup plugin.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)

32-36: Avoid desync between isMuted signal and the underlying Hang audio state

Right now isMuted is managed as local Solid state and only updated in toggleMuted, while currentVolume is driven from watchInstance.audio.volume. This can cause drift:

  • toggleMuted (Lines 54-62) flips isMuted() even when props.hangWatch() is undefined, so the UI may display “muted” with no underlying element.
  • External changes to hangWatchEl.muted or the underlying audio emitter won’t be reflected back into isMuted.

To align with how volume is handled, consider:

  1. Drive isMuted from the instance, similar to trackVolume:
@@ function onWatchInstanceAvailable(watchEl: HangWatch, watchInstance: HangWatchInstance) {
 		watchInstance?.signals.effect(function trackVolume(effect) {
 			const volume = effect.get(watchInstance?.audio.volume);
 			setCurrentVolume(volume * 100);
 		});
+
+		watchInstance?.signals.effect(function trackMuted(effect) {
+			const muted = effect.get(watchInstance?.audio.muted);
+			setIsMuted(muted);
+		});
  1. Make toggleMuted only toggle the element and rely on the effect above to update the context:
-	const toggleMuted = () => {
-		setIsMuted(!isMuted());
-
-		const hangWatchEl = props.hangWatch();
-
-		if (hangWatchEl) {
-			hangWatchEl.muted = isMuted();
-		}
-	};
+	const toggleMuted = () => {
+		const hangWatchEl = props.hangWatch();
+		if (!hangWatchEl) return;
+
+		hangWatchEl.muted = !hangWatchEl.muted;
+	};

This keeps the UI state in sync with the actual audio pipeline and avoids misleading toggles when the watch element isn’t available yet.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 48b18a9 and cc111d5.

📒 Files selected for processing (2)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/styles.css (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • js/hang-ui/src/Components/watch/styles.css
🧰 Additional context used
📓 Path-based instructions (1)
js/**/*.{js,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun workspaces configured in js/package.json for JavaScript/TypeScript development

Files:

  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧬 Code graph analysis (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)
js/hang/src/watch/element.ts (10)
  • HangWatch (23-181)
  • latency (174-176)
  • latency (178-180)
  • volume (142-144)
  • volume (146-148)
  • HangWatchInstance (185-300)
  • url (109-111)
  • url (113-115)
  • paused (134-136)
  • paused (138-140)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check
🔇 Additional comments (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)

110-155: Signal wiring from HangWatchInstance into context looks solid

The effects for status, playing, volume, buffering, and latency correctly subscribe to the corresponding HangWatchInstance / HangWatch signals and project them into Solid signals, matching the shapes exposed by WatchUIContextValues. No issues stand out here.

@jdreetz
Copy link
Collaborator Author

jdreetz commented Dec 1, 2025

@christriants updated this branch to include the Latency Slider as a component.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
js/hang-ui/src/Components/watch/LatencySlider.tsx (1)

10-14: Consider using onInput for real-time updates.

The onChange event fires only when the user releases the slider. For a latency control, using onInput instead would provide continuous updates as the user drags, offering better real-time feedback.

Apply this diff to enable real-time updates:

 const onInputChange = (event: Event) => {
 	const target = event.currentTarget as HTMLInputElement;
 	const latency = parseFloat(target.value);
 	context?.setLatencyValue(latency);
 };

 return (
 	<div class="latencySliderContainer">
 		<span class="latencyLabel">Latency: </span>
 		<input
-			onChange={onInputChange}
+			onInput={onInputChange}
 			class="latencySlider"
 			type="range"
 			value={context?.latency()}
 			min={MIN_RANGE}
 			max={MAX_RANGE}
 			step={RANGE_STEP}
 		/>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cc111d5 and e16c241.

📒 Files selected for processing (1)
  • js/hang-ui/src/Components/watch/LatencySlider.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
js/**/*.{js,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun workspaces configured in js/package.json for JavaScript/TypeScript development

Files:

  • js/hang-ui/src/Components/watch/LatencySlider.tsx
🧬 Code graph analysis (1)
js/hang-ui/src/Components/watch/LatencySlider.tsx (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)
  • WatchUIContext (28-28)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)

86-106: Instance binding logic and event listener cleanup look correct now

The createEffect correctly:

  • Reads the current HangWatch element from props.hangWatch().
  • Uses active.peek() to decide between immediate binding vs. waiting for "watch-instance-available".
  • Registers the event listener only when needed and removes it in onCleanup.

This addresses the earlier concerns about dead code (if (!hangWatchEl.active)) and potential listener leaks.

🧹 Nitpick comments (4)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (2)

44-52: Consider validating file type and handling errors.

The setFile function assumes the provided file is valid and supported. Adding file type validation and error handling would improve robustness, especially if the underlying API rejects certain file types.


164-185: Consider explicitly handling all connection status values.

The status derivation logic implicitly assumes that if status is neither "disconnected" nor "connecting", the connection is established. Making this explicit would improve code clarity and prevent subtle bugs if new status values are added.

Consider adding an explicit check:

 		} else if (status === "connecting") {
 			setPublishStatus("connecting");
+		} else if (status !== "connected") {
+			// Handle unexpected status values
+			setPublishStatus("disconnected");
 		} else if (!audio && !video) {
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (2)

32-62: Derive isMuted from the HangWatch instance instead of maintaining local-only mute state

Right now isMuted is only updated when toggleMuted runs; if something else toggles hangWatch.muted (e.g., another UI or attribute change), the context gets out of sync. Since other fields (isPlaying, currentVolume) are driven from watchInstance signals, it would be more consistent to do the same for mute and let toggleMuted just flip the element property.

Suggested refactor:

-	const [isMuted, setIsMuted] = createSignal<boolean>(false);
+	const [isMuted, setIsMuted] = createSignal<boolean>(false);
@@
-	const toggleMuted = () => {
-		setIsMuted(!isMuted());
-
-		const hangWatchEl = props.hangWatch();
-
-		if (hangWatchEl) {
-			hangWatchEl.muted = isMuted();
-		}
-	};
+	const toggleMuted = () => {
+		const hangWatchEl = props.hangWatch();
+
+		if (hangWatchEl) {
+			hangWatchEl.muted = !hangWatchEl.muted;
+		}
+	};
@@
-		watchInstance?.signals.effect(function trackVolume(effect) {
+		watchInstance?.signals.effect(function trackVolume(effect) {
 			const volume = effect.get(watchInstance?.audio.volume);
 			setCurrentVolume(volume * 100);
 		});
+
+		watchInstance?.signals.effect(function trackMuted(effect) {
+			const muted = effect.get(watchInstance?.audio.muted);
+			setIsMuted(muted);
+		});

This keeps the context as a pure view of the underlying instance state and avoids divergence.

Also applies to: 138-141, 151-154


46-52: Optionally clamp volume to [0, 1] when mapping from 0–100 UI range

setVolume divides by 100 and trackVolume multiplies by 100, which is symmetric, but if a caller passes values outside 0–100 you can end up feeding >1 or <0 into hangWatchEl.volume.

A small defensive tweak would make this more robust:

-	const setVolume = (volume: number) => {
+	const setVolume = (volume: number) => {
 		const hangWatchEl = props.hangWatch();
 
 		if (hangWatchEl) {
-			hangWatchEl.volume = volume / 100;
+			const normalized = Math.min(1, Math.max(0, volume / 100));
+			hangWatchEl.volume = normalized;
 		}
 	};

Not critical, but it hardens the API against bad inputs.

Also applies to: 138-141

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e16c241 and 0a2f34e.

📒 Files selected for processing (3)
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/LatencySlider.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • js/hang-ui/src/Components/watch/LatencySlider.tsx
🧰 Additional context used
📓 Path-based instructions (1)
js/**/*.{js,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun workspaces configured in js/package.json for JavaScript/TypeScript development

Files:

  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧠 Learnings (1)
📚 Learning: 2025-11-22T15:03:15.091Z
Learnt from: pzanella
Repo: kixelated/moq PR: 714
File: js/hang/src/watch/element.ts:469-469
Timestamp: 2025-11-22T15:03:15.091Z
Learning: In js/hang/src/watch/element.ts, the buffering overlay intentionally uses `pointerEvents: "auto"` to block clicks and prevent accidental interactions with underlying controls during buffering.

Applied to files:

  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧬 Code graph analysis (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)
js/hang/src/watch/element.ts (10)
  • HangWatch (23-181)
  • latency (174-176)
  • latency (178-180)
  • volume (142-144)
  • volume (146-148)
  • HangWatchInstance (185-300)
  • url (109-111)
  • url (113-115)
  • paused (134-136)
  • paused (138-140)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check
🔇 Additional comments (3)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1)

93-191: Effect subscriptions require verification of cleanup implementation.

The nine publishInstance.signals.effect() calls (trackCameraDevices, trackMicrophoneDevices, trackNothingSourceActive, trackMicrophoneSourceActive, trackVideoSourcesActive, trackSelectedCameraSource, trackSelectedMicrophoneSource, trackPublishStatus, trackFileActive) may not be properly disposed. Verify whether:

  1. effect() returns a dispose/cleanup function
  2. These cleanup functions are collected and invoked in an onCleanup callback
  3. This cleanup is already implemented elsewhere in the component

If effects are not being disposed, this will cause memory leaks and stale state updates when the component unmounts or hangPublishEl changes.

js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (2)

7-28: Context types and value shape look consistent and sufficient

WatchUIContextProviderProps, WatchStatus, and WatchUIContextValues line up well with the HangWatch/HangWatchInstance API and the controls you expose; nothing blocking here.


110-155: Signal effects for status/playback/volume/buffering/latency are wired coherently

The onWatchInstanceAvailable effects correctly derive:

  • watchStatus from connection URL/status and broadcast status.
  • isPlaying from video.paused.
  • currentVolume from audio.volume with the expected 0–100 mapping.
  • buffering from syncStatus/bufferStatus (matching the existing buffering overlay behavior).
  • latency from watchEl.signals.latency.

This is a solid integration of the HangWatch signals into the UI context; aside from the separate mute handling already noted, there are no functional issues here.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1)

86-86: Inconsistent optional chaining on active property.

Line 78 uses hangPublishEl?.active?.peek() with optional chaining on active, but line 86 uses hangPublishEl.active.peek?.() without optional chaining on active. While technically safe due to the control flow (the else branch only executes when active.peek() is truthy), this inconsistency could cause confusion or become error-prone during refactoring.

Apply this diff for consistency:

 	} else {
-		const publishInstance = hangPublishEl.active.peek?.() as HangPublishInstance;
+		const publishInstance = hangPublishEl.active?.peek?.() as HangPublishInstance;
 		onPublishInstanceAvailable(hangPublishEl, publishInstance);
 	}
🧹 Nitpick comments (1)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1)

44-52: Consider potential intermediate states when setting multiple properties.

The function sets four properties sequentially on hangPublishEl. While this likely works correctly given synchronous setters, intermediate states could theoretically cause issues if any internal reactions observe partial updates.

If the hang library supports batching or atomic updates, consider using that pattern:

const setFile = (file: File) => {
	const hangPublishEl = props.hangPublish();
	if (!hangPublishEl) return;

	// If hang supports batching, wrap in a batch operation:
	// batch(() => {
		hangPublishEl.file = file;
		hangPublishEl.source = "file";
		hangPublishEl.video = true;
		hangPublishEl.audio = true;
	// });
};
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a2f34e and d1af166.

📒 Files selected for processing (2)
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
js/**/*.{js,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun workspaces configured in js/package.json for JavaScript/TypeScript development

Files:

  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx
🧠 Learnings (1)
📚 Learning: 2025-11-22T15:03:15.091Z
Learnt from: pzanella
Repo: kixelated/moq PR: 714
File: js/hang/src/watch/element.ts:469-469
Timestamp: 2025-11-22T15:03:15.091Z
Learning: In js/hang/src/watch/element.ts, the buffering overlay intentionally uses `pointerEvents: "auto"` to block clicks and prevent accidental interactions with underlying controls during buffering.

Applied to files:

  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧬 Code graph analysis (2)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)
js/hang/src/watch/element.ts (10)
  • HangWatch (23-181)
  • latency (174-176)
  • latency (178-180)
  • volume (142-144)
  • volume (146-148)
  • HangWatchInstance (185-300)
  • url (109-111)
  • url (113-115)
  • paused (134-136)
  • paused (138-140)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (2)
js/hang/src/publish/source/file.ts (2)
  • File (9-126)
  • setFile (35-37)
js/hang/src/publish/element.ts (1)
  • HangPublishInstance (178-339)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check
🔇 Additional comments (4)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1)

93-191: The nested effects registered within onPublishInstanceAvailable are automatically cleaned up by Solid.js when the outer createEffect reruns. In Solid.js, effects created within a reactive scope are part of that scope's lifecycle—when the scope reruns, previous inner effects are disposed before new ones are created. No manual cleanup is required for this nested effect pattern.

Likely an incorrect or invalid review comment.

js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (3)

86-106: LGTM: Instance binding logic correctly handles both immediate and event-driven binding.

The conditional check on Line 95 properly examines active.peek() (the signal's value) rather than the signal itself, ensuring the event listener is registered only when no instance exists yet. The cleanup logic correctly removes the listener when the effect re-runs or the component unmounts.


151-154: LGTM: Latency tracking is correctly implemented.

The trackLatency effect properly subscribes to the latency signal and updates the UI state. The previously reported typo has been fixed.


143-149: Optional chaining is unnecessary here.

The syncStatus and bufferStatus values returned by effect.get() are guaranteed to be defined. These values come from Signal instances that are always initialized in the Source class and maintain their .state property. Adding optional chaining would be defensive programming without practical benefit.

Likely an incorrect or invalid review comment.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)

38-60: Optionally clamp volume and latency inputs before writing to HangWatch.

Right now setVolume and setLatencyValue trust callers to pass sane ranges; a stray value (e.g., >100 or negative) would propagate directly into the Hang signals. Consider clamping to defensive ranges (0–100 for volume slider percentage, and >= 0 for latency) to make the context safer to use from any UI component.

-	const setVolume = (volume: number) => {
-		const hangWatchEl = props.hangWatch();
-
-		if (hangWatchEl) {
-			hangWatchEl.volume = volume / 100;
-		}
-	};
+	const setVolume = (volume: number) => {
+		const hangWatchEl = props.hangWatch();
+		if (!hangWatchEl) return;
+
+		const clamped = Math.min(100, Math.max(0, volume));
+		hangWatchEl.volume = clamped / 100;
+	};
@@
-	const setLatencyValue = (latency: number) => {
-		const hangWatchEl = props.hangWatch();
-
-		if (hangWatchEl) {
-			hangWatchEl.signals.latency.set(latency as Time.Milli);
-		}
-	};
+	const setLatencyValue = (latency: number) => {
+		const hangWatchEl = props.hangWatch();
+		if (!hangWatchEl) return;
+
+		const clamped = Math.max(0, latency);
+		hangWatchEl.signals.latency.set(clamped as Time.Milli);
+	};

Please double‑check expected ranges for volume and latency against @kixelated/hang’s current API before adopting this.

Also applies to: 62-68

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1af166 and fa6f555.

📒 Files selected for processing (1)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
js/**/*.{js,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun workspaces configured in js/package.json for JavaScript/TypeScript development

Files:

  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧠 Learnings (1)
📚 Learning: 2025-11-22T15:03:15.091Z
Learnt from: pzanella
Repo: kixelated/moq PR: 714
File: js/hang/src/watch/element.ts:469-469
Timestamp: 2025-11-22T15:03:15.091Z
Learning: In js/hang/src/watch/element.ts, the buffering overlay intentionally uses `pointerEvents: "auto"` to block clicks and prevent accidental interactions with underlying controls during buffering.

Applied to files:

  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧬 Code graph analysis (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)
js/hang/src/watch/element.ts (12)
  • HangWatch (23-181)
  • latency (174-176)
  • latency (178-180)
  • volume (142-144)
  • volume (146-148)
  • HangWatchInstance (185-300)
  • url (109-111)
  • url (113-115)
  • paused (134-136)
  • paused (138-140)
  • muted (150-152)
  • muted (154-156)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check
🔇 Additional comments (3)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (3)

7-37: Context shape and signal wiring look coherent.

Props, WatchStatus, WatchUIContextValues, and the provider’s internal signals line up cleanly with the exposed context API; the provider value matches the declared types and keeps UI state localized while delegating actual media control to the element/instance. No changes needed here.

Also applies to: 70-82, 106-106


84-104: Instance binding and listener cleanup correctly handle eager and deferred availability.

Using hangWatchEl.active.peek() to decide between immediate binding and subscribing to "watch-instance-available", plus onCleanup to remove the listener, addresses the earlier leak/race concerns and should work both when an instance already exists and when it appears later. This looks good; just ensure the demos still behave as expected when swapping/re‑creating <hang-watch> elements.


108-158: Effects for status, playback, volume, mute, buffering, and latency are well-aligned with HangWatchInstance signals.

The onWatchInstanceAvailable helpers consistently derive UI state from connection, broadcast, video, audio, and latency signals, and push into the Solid signals backing the context. The watch-status mapping covers the documented connection/broadcast states, and buffering/latency tracking aligns with the underlying element signals. No changes needed here.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (2)

62-68: Use element property for consistency with other setters.

The other action functions (togglePlayback, setVolume, toggleMuted) consistently use element properties (paused, volume, muted), but setLatencyValue directly accesses signals.latency.set(). Since HangWatch has a latency property setter, using it would maintain consistency.

Apply this diff:

 const setLatencyValue = (latency: number) => {
 	const hangWatchEl = props.hangWatch();
 
 	if (hangWatchEl) {
-		hangWatchEl.signals.latency.set(latency as Time.Milli);
+		hangWatchEl.latency = latency;
 	}
 };

93-93: Type assertion could be more precise.

The type assertion as HangWatchInstance is misleading because peek() returns HangWatchInstance | undefined. While the code is safe due to the subsequent if (hangWatchInstance) check, removing the type assertion or using as HangWatchInstance | undefined would be more accurate.

Apply this diff:

-	const hangWatchInstance = hangWatchEl?.active?.peek?.() as HangWatchInstance;
+	const hangWatchInstance = hangWatchEl?.active?.peek?.();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fa6f555 and 49ce3cf.

📒 Files selected for processing (2)
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx
🧰 Additional context used
📓 Path-based instructions (1)
js/**/*.{js,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun workspaces configured in js/package.json for JavaScript/TypeScript development

Files:

  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧠 Learnings (1)
📚 Learning: 2025-11-22T15:03:15.091Z
Learnt from: pzanella
Repo: kixelated/moq PR: 714
File: js/hang/src/watch/element.ts:469-469
Timestamp: 2025-11-22T15:03:15.091Z
Learning: In js/hang/src/watch/element.ts, the buffering overlay intentionally uses `pointerEvents: "auto"` to block clicks and prevent accidental interactions with underlying controls during buffering.

Applied to files:

  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧬 Code graph analysis (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (2)
js/hang/src/watch/element.ts (12)
  • HangWatch (23-181)
  • latency (174-176)
  • latency (178-180)
  • volume (142-144)
  • volume (146-148)
  • HangWatchInstance (185-300)
  • url (109-111)
  • url (113-115)
  • paused (134-136)
  • paused (138-140)
  • muted (150-152)
  • muted (154-156)
js/signals/src/index.ts (1)
  • event (489-504)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check
🔇 Additional comments (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)

109-159: Well-structured effect tracking with automatic cleanup.

The onWatchInstanceAvailable function correctly sets up reactive effects to track all relevant watch state (status, playback, volume, muting, buffering, latency). The effects are automatically cleaned up when the HangWatchInstance is closed (via signals.close()), which happens when the element disconnects from the DOM.

Copy link
Collaborator

@kixelated kixelated left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh and we could roll this back into @kixelated/hang if we migrate that package over to rollup as well.

},
resolve: {
alias: {
"@kixelated/hang-ui/publish/element": path.resolve(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kinda gross, but harmless I guess. You should leave a comment that this isn't needed when using the package from NPM.

This wasn't needed when package.json pointed to src instead of dist.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll see if I can make it work like the other packages in the workspace.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed this and followed the build pattern from the other packages.

},
"sideEffects": true,
"scripts": {
"build": "npm run clean && rollup -c",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And yeah, the other packages use a script to rewrite package.json to change src to dist for publishing. Your alias approach works too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll see if I can switch it to match how the other ones work. I don't like that the Vite config has to do special handling for that dependency.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated this to follow the pattern from the other packages. Works as far as I can tell.


function inlineCss() {
return {
name: 'inline-css',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to do this anyway for @kixelated/hang because of worklets. I left some thoughts in #677

},
"devDependencies": {
"@biomejs/biome": "^2.2.2",
"@kixelated/hang": "workspace:^0.7.0",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a peerDependency?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

return <PublishUIContext.Provider value={value}>{props.children}</PublishUIContext.Provider>;

function onPublishInstanceAvailable(el: HangPublish, publishInstance: HangPublishInstance) {
publishInstance?.signals.effect(function trackCameraDevices(effect) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you should remove the ? from all of these, it can't be null.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these should all be gone now.

const [hangPublishEl, setHangPublishEl] = createSignal<HangPublish>();

const onInstanceAvailable = (event: CustomEvent) => {
const hangPublishEl = event.target as HangPublish;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a fan of casting, the event should be typed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added typings for both events.

if (publishEl) {
setHangPublishEl(publishEl);
} else {
element.addEventListener("publish-instance-available", onInstanceAvailable);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use the signal instead if you want.

const dispose = element.active.watch((instance?: PublishInstance) => {

});

onCleanup(dispose);

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also we might be able to avoid the instance being undefined. The issue is the web component lifetime; we need to start work when inserted into the DOM and stop work when removed. Toggling enabled instead might do the trick.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for some reason, active isn't even defined when I first get a reference to the element in onMount, so I can't add the watch on active. Firing the event lets us know exactly when active is defined and has an instance AND it helps if the user adds hang-watch later on for some reason.

I guess I could do something like poll for the active Signal, but that feels less ideal than just firing this event.

I don't understand what you mean about "Toggling enabled" though?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
js/hang/src/watch/element.ts (2)

69-77: Clarify event detail intent and consider bubbling if wrappers need to observe it

Right now detail.instance is a Signal<HangWatchInstance | undefined> (via HangWatch['active']), not the instance itself; this is powerful but a bit surprising given the name. If you expect most consumers to treat it as “the instance”, consider either renaming the field (e.g. active/instanceSignal) or changing the type to HangWatchInstance and exposing the signal separately.

Also, if higher-level wrappers or host pages should be able to listen declaratively (e.g. on <hang-watch-ui> or outside a shadow root), it may be worth setting bubbles: true, composed: true on the CustomEvent. If you only ever attach the listener directly to <hang-watch>, the current non-bubbling event is fine.


202-307: Public HangWatchInstance.signals exposure and lifecycle wiring look consistent

Switching from a private field to a public signals: Effect while keeping Effect ownership within HangWatchInstance (construct, use for attribute sync/effect cleanup, and close in close()) preserves existing behavior and cleanly exposes the reactive root needed by the new UI layer. The additional cleanup for the MutationObserver is correctly tied into signals, so disconnect happens when the instance is closed.

Just be aware this effectively makes signals part of the public API; external callers invoking instance.signals.close() could tear down internal effects. If that’s not desired, you may want to document signals as an advanced/low-level escape hatch.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 15c3705 and 7957e18.

📒 Files selected for processing (4)
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1 hunks)
  • js/hang/src/publish/element.ts (11 hunks)
  • js/hang/src/watch/element.ts (10 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
  • js/hang/src/publish/element.ts
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx
🧰 Additional context used
📓 Path-based instructions (1)
js/**/*.{js,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun workspaces configured in js/package.json for JavaScript/TypeScript development

Files:

  • js/hang/src/watch/element.ts
🧠 Learnings (1)
📚 Learning: 2025-11-22T15:03:15.091Z
Learnt from: pzanella
Repo: kixelated/moq PR: 714
File: js/hang/src/watch/element.ts:469-469
Timestamp: 2025-11-22T15:03:15.091Z
Learning: In js/hang/src/watch/element.ts, the buffering overlay intentionally uses `pointerEvents: "auto"` to block clicks and prevent accidental interactions with underlying controls during buffering.

Applied to files:

  • js/hang/src/watch/element.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check
🔇 Additional comments (1)
js/hang/src/watch/element.ts (1)

22-28: Typed watch-instance-available event and DOM augmentation look good

Using HangWatch['active'] in InstanceAvailableEvent keeps the event detail aligned with the active property type, and the GlobalEventHandlersEventMap augmentation will give consumers properly typed addEventListener/onwatch-instance-available handlers without affecting runtime behavior.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1)

14-19: Remove optional markers from always-provided properties.

The type definition marks cameraActive, screenActive, fileActive, nothingActive, selectedCameraSource, and selectedMicrophoneSource as optional, but the value object always provides them (lines 59-66). This creates a type/implementation mismatch.

Apply this diff:

 type PublishUIContextValue = {
 	hangPublish: () => HangPublish | undefined;
 	cameraDevices: () => MediaDeviceInfo[];
 	microphoneDevices: () => MediaDeviceInfo[];
 	publishStatus: () => PublishStatus;
 	microphoneActive: () => boolean;
-	cameraActive?: () => boolean;
-	screenActive?: () => boolean;
-	fileActive?: () => boolean;
-	nothingActive?: () => boolean;
-	selectedCameraSource?: () => MediaDeviceInfo["deviceId"] | undefined;
-	selectedMicrophoneSource?: () => MediaDeviceInfo["deviceId"] | undefined;
+	cameraActive: () => boolean;
+	screenActive: () => boolean;
+	fileActive: () => boolean;
+	nothingActive: () => boolean;
+	selectedCameraSource: () => MediaDeviceInfo["deviceId"] | undefined;
+	selectedMicrophoneSource: () => MediaDeviceInfo["deviceId"] | undefined;
 	setFile: (file: File) => void;
 };
js/hang/src/publish/element.ts (1)

164-184: Device setters are safe, but indentation is still inconsistent

Functionally, the new videoDevice/audioDevice setters are guarded correctly:

  • no-op if there is no active instance,
  • no-op if the current source has no device property,
  • and they update device.preferred when a supporting source (e.g. camera/microphone) is active.

However, this block still uses mixed spaces/tabs compared to the rest of the file and the earlier review already flagged this.

	set videoDevice(sourceId: MediaDeviceInfo['deviceId']) {
 		const hangPublishInstance = this.active.peek();
-        if (!hangPublishInstance) return;
-
-        const video = hangPublishInstance.video?.peek();
-
-        if (!video || !('device' in video)) return;
-
-        video.device.preferred.set(sourceId);
+		if (!hangPublishInstance) return;
+
+		const video = hangPublishInstance.video?.peek();
+
+		if (!video || !("device" in video)) return;
+
+		video.device.preferred.set(sourceId);
 	}
 
 	set audioDevice(sourceId: MediaDeviceInfo['deviceId']) {
 		const hangPublishInstance = this.active.peek();
-        if (!hangPublishInstance) return;
-
-        const audio = hangPublishInstance.audio?.peek();
-
-        if (!audio || !('device' in audio)) return;
-
-        audio.device.preferred.set(sourceId);
+		if (!hangPublishInstance) return;
+
+		const audio = hangPublishInstance.audio?.peek();
+
+		if (!audio || !("device" in audio)) return;
+
+		audio.device.preferred.set(sourceId);
 	}

Please run the repo’s formatter (e.g., Prettier/ESLint or equivalent) to ensure this matches the project’s configured indentation style.

🧹 Nitpick comments (2)
js/hang/src/watch/element.ts (1)

22-28: Clarify instance-available event typing and bubbling behavior

The new InstanceAvailableEvent and 'watch-instance-available' dispatch look good and match the publish side. Two small API-shape suggestions:

  • The exported alias name InstanceAvailableEvent is now used in both watch and publish modules; consider renaming to something specific like WatchInstanceAvailableEvent to avoid import-name collisions for consumers that work with both.
  • If hang-ui or other consumers rely on delegated/custom-event handling higher up the tree, you may want this event to bubble (and possibly cross shadow boundaries). That would look like:
-		this.dispatchEvent(new CustomEvent('watch-instance-available', {
-			detail: {
-				instance,
-			}
-		}));
+		this.dispatchEvent(new CustomEvent("watch-instance-available", {
+			bubbles: true,
+			composed: true,
+			detail: { instance },
+		}));

Please double-check how hang-watch-ui is listening for this event (direct listener vs. delegated) before changing the bubbling/composed flags.

Also applies to: 71-78

js/hang/src/publish/element.ts (1)

24-30: Align publish instance-available event API with watch side and consumer needs

The InstanceAvailableEvent export and 'publish-instance-available' dispatch look consistent with the watch element. Two similar API-shape nits:

  • As with watch, consider a more specific alias like PublishInstanceAvailableEvent to avoid import-name collisions when a consumer works with both publish and watch events.
  • If the hang-ui wrapper listens via delegated/custom events above the <hang-publish> element, you may want the event to bubble and be composed:
-		this.dispatchEvent(new CustomEvent('publish-instance-available', {
-			detail: {
-				instance,
-			}
-		}));
+		this.dispatchEvent(new CustomEvent("publish-instance-available", {
+			bubbles: true,
+			composed: true,
+			detail: { instance },
+		}));

Please verify how hang-publish-ui currently attaches its listener before changing the event options.

Also applies to: 48-56

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7957e18 and 5b5eb37.

📒 Files selected for processing (6)
  • js/hang-demo/vite.config.ts (1 hunks)
  • js/hang-ui/package.json (1 hunks)
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1 hunks)
  • js/hang/src/publish/element.ts (11 hunks)
  • js/hang/src/watch/element.ts (10 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • js/hang-ui/package.json
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧰 Additional context used
📓 Path-based instructions (1)
js/**/*.{js,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun workspaces configured in js/package.json for JavaScript/TypeScript development

Files:

  • js/hang-demo/vite.config.ts
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx
  • js/hang/src/watch/element.ts
  • js/hang/src/publish/element.ts
🧠 Learnings (1)
📚 Learning: 2025-11-22T15:03:15.091Z
Learnt from: pzanella
Repo: kixelated/moq PR: 714
File: js/hang/src/watch/element.ts:469-469
Timestamp: 2025-11-22T15:03:15.091Z
Learning: In js/hang/src/watch/element.ts, the buffering overlay intentionally uses `pointerEvents: "auto"` to block clicks and prevent accidental interactions with underlying controls during buffering.

Applied to files:

  • js/hang/src/watch/element.ts
🧬 Code graph analysis (3)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1)
js/hang/src/publish/element.ts (12)
  • HangPublish (32-185)
  • file (132-134)
  • file (136-138)
  • InstanceAvailableEvent (24-24)
  • HangPublishInstance (187-348)
  • effect (256-341)
  • video (148-150)
  • video (152-154)
  • audio (140-142)
  • audio (144-146)
  • url (91-93)
  • url (95-97)
js/hang/src/watch/element.ts (2)
js/hang/src/publish/element.ts (2)
  • InstanceAvailableEvent (24-24)
  • effect (256-341)
js/signals/src/index.ts (1)
  • Effect (157-553)
js/hang/src/publish/element.ts (3)
js/hang/src/watch/element.ts (1)
  • InstanceAvailableEvent (22-22)
js/hang/src/publish/source/camera.ts (2)
  • Camera (11-72)
  • effect (28-66)
js/hang/src/publish/source/file.ts (1)
  • File (9-126)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check
🔇 Additional comments (8)
js/hang-demo/vite.config.ts (1)

1-8: Solid + Tailwind plugin wiring in Vite config looks correct

Importing vite-plugin-solid and adding solidPlugin() alongside tailwindcss() in the plugins array is the right way to enable Solid in this demo while keeping the existing Tailwind pipeline. No config issues stand out here.

Please double-check in your local environment that:

  • vite-plugin-solid version in package.json is officially compatible with Vite 6.3.6, and
  • Tailwind v4 via @tailwindcss/vite still processes styles for the new Solid-based components as expected (e.g., run nix develop -c just dev and verify HMR/build logs are clean and the demo UIs render correctly).
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (5)

44-52: LGTM!

The setFile function correctly configures the publish element for file-based streaming by setting the file, source type, and enabling both video and audio.


69-91: LGTM!

The effect correctly handles both scenarios: when the publish instance is immediately available and when it needs to wait for the publish-instance-available event. The event listener is properly cleaned up.


96-120: LGTM!

Both device tracking effects correctly monitor available devices and only populate the device lists when multiple devices are available (length >= 2), which ensures the UI only shows selectors when there are meaningful choices.


122-146: LGTM!

The source tracking effects correctly maintain button states. The trackVideoSourcesActive effect properly handles all source types including the implicit "file" case where both camera and screen should be inactive.


148-192: LGTM!

The remaining effects correctly track selected device IDs, derive comprehensive publish status from multiple signal sources, and monitor file source activation. The trackPublishStatus effect handles all possible state combinations appropriately.

js/hang/src/watch/element.ts (1)

193-308: Signals/effects wiring and cleanup for HangWatchInstance look solid

Using a public signals: Effect on HangWatchInstance to own:

  • the MutationObserver cleanup via signals.cleanup, and
  • the attribute sync effects (url/path/muted/paused/volume/controls/latency) closed from close()

is consistent and should avoid leaks while keeping attributes in sync with the underlying signals, including removal when values are falsy/undefined. No functional issues spotted here.

Please confirm Effect.close() from @kixelated/signals indeed runs all registered cleanups and terminates nested effects, as assumed by this lifecycle.

js/hang/src/publish/element.ts (1)

192-197: Public instance signals and source wiring look correct

The refactor that:

  • exposes video, audio, file, and signals as public fields on HangPublishInstance,
  • uses this.signals.cleanup for the MutationObserver,
  • drives preview/video/audio/file source selection via this.signals.effect(...) and effect.set(...), and
  • closes everything via this.signals.close() in close()

is consistent with the prior private-#signals pattern and gives hang-ui a clean way to observe the underlying sources. Guards in #runSource ensure the correct Source.* is constructed for each source value, and effect.set(...) is used in the standard way for temporary bindings.

No functional problems spotted here.

Please confirm that Effect.set’s default cleanup behavior in @kixelated/signals matches the intended “temporary within this effect run” semantics assumed by these bindings.

Also applies to: 207-207, 229-254, 272-274, 300-301, 333-333, 344-347

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1)

14-19: Optional markers still present on always-provided properties.

Lines 14-17 (cameraActive, screenActive, fileActive, nothingActive) and lines 18-19 (selectedCameraSource, selectedMicrophoneSource) are marked optional, but the value object (lines 59-66) always provides them. This inconsistency may cause unnecessary optional chaining by consumers.

-	cameraActive?: () => boolean;
-	screenActive?: () => boolean;
-	fileActive?: () => boolean;
-	nothingActive?: () => boolean;
-	selectedCameraSource?: () => MediaDeviceInfo["deviceId"] | undefined;
-	selectedMicrophoneSource?: () => MediaDeviceInfo["deviceId"] | undefined;
+	cameraActive: () => boolean;
+	screenActive: () => boolean;
+	fileActive: () => boolean;
+	nothingActive: () => boolean;
+	selectedCameraSource: () => MediaDeviceInfo["deviceId"] | undefined;
+	selectedMicrophoneSource: () => MediaDeviceInfo["deviceId"] | undefined;
🧹 Nitpick comments (1)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1)

93-95: Consider moving onPublishInstanceAvailable before the return statement.

While function declarations are hoisted and this works correctly, defining a function after the return statement is unconventional and may confuse readers. Moving it before the return improves readability.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5b5eb37 and d973224.

📒 Files selected for processing (2)
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧰 Additional context used
📓 Path-based instructions (1)
js/**/*.{js,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun workspaces configured in js/package.json for JavaScript/TypeScript development

Files:

  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx
🧬 Code graph analysis (1)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (4)
js/hang/src/publish/element.ts (12)
  • HangPublish (32-185)
  • file (132-134)
  • file (136-138)
  • InstanceAvailableEvent (24-24)
  • HangPublishInstance (187-348)
  • effect (256-341)
  • video (148-150)
  • video (152-154)
  • audio (140-142)
  • audio (144-146)
  • url (91-93)
  • url (95-97)
js/hang/src/publish/source/file.ts (2)
  • File (9-126)
  • setFile (35-37)
js/signals/src/index.ts (1)
  • event (489-504)
js/hang/src/watch/element.ts (3)
  • InstanceAvailableEvent (22-22)
  • url (118-120)
  • url (122-124)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check
🔇 Additional comments (2)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (2)

69-91: LGTM on instance subscription effect.

The event listener is properly cleaned up with onCleanup, and the optional chaining is consistent. The logic correctly handles both cases: when the instance is already active and when it becomes available later.


166-187: Consider explicitly validating connection status values.

The trackPublishStatus effect assumes any status that is not "disconnected" or "connecting" can proceed to audio/video checks. If the connection status can have other values beyond the commonly expected ones (e.g., "connected", "error", "reconnecting"), this could leave publishStatus undefined or in an unexpected state.

Add explicit status validation or document the expected status values from publishInstance.connection.status to ensure all cases are handled correctly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1)

15-20: Remove optional markers from always-provided properties.

The type definition marks cameraActive, screenActive, fileActive, nothingActive, selectedCameraSource, and selectedMicrophoneSource as optional (?:), but the value object always provides them (lines 60-63, 65-67). This inconsistency may lead consumers to add unnecessary optional chaining.

Apply this diff to make the type definition consistent:

 type PublishUIContextValue = {
 	hangPublish: () => HangPublish | undefined;
 	cameraDevices: () => MediaDeviceInfo[];
 	microphoneDevices: () => MediaDeviceInfo[];
 	publishStatus: () => PublishStatus;
 	microphoneActive: () => boolean;
-	cameraActive?: () => boolean;
-	screenActive?: () => boolean;
-	fileActive?: () => boolean;
-	nothingActive?: () => boolean;
-	selectedCameraSource?: () => MediaDeviceInfo["deviceId"] | undefined;
-	selectedMicrophoneSource?: () => MediaDeviceInfo["deviceId"] | undefined;
+	cameraActive: () => boolean;
+	screenActive: () => boolean;
+	fileActive: () => boolean;
+	nothingActive: () => boolean;
+	selectedCameraSource: () => MediaDeviceInfo["deviceId"] | undefined;
+	selectedMicrophoneSource: () => MediaDeviceInfo["deviceId"] | undefined;
 	setFile: (file: File) => void;
 };
🧹 Nitpick comments (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)

97-97: Remove redundant optional chaining.

Line 97 uses hangWatchEl?.active?.peek?.() but hangWatchEl is already confirmed to exist by the guard on Line 87, making the ?. on hangWatchEl unnecessary.

Apply this diff:

-		const hangWatchInstance = hangWatchEl?.active?.peek?.();
+		const hangWatchInstance = hangWatchEl.active?.peek?.();
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d973224 and 6c3a9e4.

📒 Files selected for processing (2)
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1 hunks)
  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
js/**/*.{js,ts,tsx,json}

📄 CodeRabbit inference engine (CLAUDE.md)

Use bun workspaces configured in js/package.json for JavaScript/TypeScript development

Files:

  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
  • js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx
🧠 Learnings (1)
📚 Learning: 2025-11-22T15:03:15.091Z
Learnt from: pzanella
Repo: kixelated/moq PR: 714
File: js/hang/src/watch/element.ts:469-469
Timestamp: 2025-11-22T15:03:15.091Z
Learning: In js/hang/src/watch/element.ts, the buffering overlay intentionally uses `pointerEvents: "auto"` to block clicks and prevent accidental interactions with underlying controls during buffering.

Applied to files:

  • js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx
🧬 Code graph analysis (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)
js/hang/src/watch/element.ts (13)
  • HangWatch (31-190)
  • latency (183-185)
  • latency (187-189)
  • volume (151-153)
  • volume (155-157)
  • InstanceAvailableEvent (22-22)
  • HangWatchInstance (194-309)
  • url (118-120)
  • url (122-124)
  • paused (143-145)
  • paused (147-149)
  • muted (159-161)
  • muted (163-165)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Check
🔇 Additional comments (7)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (6)

45-53: LGTM!

The setFile helper correctly guards against undefined hangPublishEl and appropriately configures the element for file-based publishing.


70-92: LGTM!

The effect properly handles both scenarios (existing instance and pending instance) and correctly cleans up the event listener using onCleanup.


96-102: LGTM!

Creating a local Effect instance and closing it in onCleanup properly addresses the lifecycle mismatch between the component and the publish instance, preventing memory leaks.


103-143: LGTM!

Both device-tracking effects correctly clear stale device arrays when conditions are no longer met, ensuring UI state stays synchronized with available devices.


145-169: LGTM!

The source-active tracking logic correctly maintains mutually exclusive camera/screen states and properly reflects the current audio and video source selections.


171-215: LGTM!

The selected-source tracking effects safely handle missing device properties, and the publish-status derivation exhaustively covers all connection and source states.

js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)

1-168: LGTM! Context provider implementation is solid.

The implementation correctly:

  • Manages local UI state with SolidJS signals
  • Binds to HangWatch instances via events or immediate connection
  • Subscribes to instance state changes with proper cleanup
  • Addresses all issues from previous reviews (memory leaks, missing muted tracking, instance availability check)

The control functions follow a consistent pattern and properly delegate to the element, while effects keep the UI synchronized with instance state.

@kixelated
Copy link
Collaborator

I had an idea, #738 should make things much easier for you. I'm thinking we hit da merge button and I'll deal with the conflicts.

@kixelated kixelated merged commit 72a8d03 into moq-dev:main Dec 5, 2025
1 check passed
@jdreetz
Copy link
Collaborator Author

jdreetz commented Dec 5, 2025

I had an idea, #738 should make things much easier for you. I'm thinking we hit da merge button and I'll deal with the conflicts.

@kixelated let me know if you want help with this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants