Hang UI as a separate package - Issue 695#717
Conversation
WalkthroughIntroduces a new TypeScript SolidJS package Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
…-publish exports from compiled sources
js/hang-demo/src/publish.html
Outdated
| <!-- 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> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
js/hang-ui/.prettierrc
Outdated
| @@ -0,0 +1,12 @@ | |||
| { | |||
There was a problem hiding this comment.
Give biome a try; it's sooo much better than prettier IMO.
js/hang-ui/package.json
Outdated
| "rollup": "^4.53.3", | ||
| "rollup-plugin-string": "^3.0.0", | ||
| "solid-element": "^1.9.1", | ||
| "solid-js": "^1.9.10", |
There was a problem hiding this comment.
your choice if you use solids. I'd be down with trying out other frameworks
js/moq/package.json
Outdated
| "exports": { | ||
| ".": "./src/index.ts", | ||
| "./zod": "./src/zod.ts" | ||
| ".": "./dist/index.js", |
There was a problem hiding this comment.
Ideally we can avoid this change because it makes local testing (via link) more difficult.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I got around this requirement by using esbuild to do the typescript handling in Rollup.
|
@kixelated do we want to do the "Meet" UI as well? |
fa5a559 to
8cad818
Compare
…by using esbuild instead of typescript rollup plugin.
8cad818 to
8b23c5b
Compare
1782e8a to
c187923
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (1)
32-36: Avoid desync betweenisMutedsignal and the underlying Hang audio stateRight now
isMutedis managed as local Solid state and only updated intoggleMuted, whilecurrentVolumeis driven fromwatchInstance.audio.volume. This can cause drift:
toggleMuted(Lines 54-62) flipsisMuted()even whenprops.hangWatch()isundefined, so the UI may display “muted” with no underlying element.- External changes to
hangWatchEl.mutedor the underlying audio emitter won’t be reflected back intoisMuted.To align with how volume is handled, consider:
- Drive
isMutedfrom the instance, similar totrackVolume:@@ 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); + });
- Make
toggleMutedonly 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
📒 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.jsonfor 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 fromHangWatchInstanceinto context looks solidThe effects for status, playing, volume, buffering, and latency correctly subscribe to the corresponding
HangWatchInstance/HangWatchsignals and project them into Solid signals, matching the shapes exposed byWatchUIContextValues. No issues stand out here.
|
@christriants updated this branch to include the Latency Slider as a component. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
js/hang-ui/src/Components/watch/LatencySlider.tsx (1)
10-14: Consider usingonInputfor real-time updates.The
onChangeevent fires only when the user releases the slider. For a latency control, usingonInputinstead 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
📒 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.jsonfor 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
There was a problem hiding this comment.
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 nowThe
createEffectcorrectly:
- Reads the current
HangWatchelement fromprops.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
setFilefunction 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
statusis 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: DeriveisMutedfrom the HangWatch instance instead of maintaining local-only mute stateRight now
isMutedis only updated whentoggleMutedruns; if something else toggleshangWatch.muted(e.g., another UI or attribute change), the context gets out of sync. Since other fields (isPlaying,currentVolume) are driven fromwatchInstancesignals, it would be more consistent to do the same for mute and lettoggleMutedjust 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
setVolumedivides by 100 andtrackVolumemultiplies by 100, which is symmetric, but if a caller passes values outside 0–100 you can end up feeding >1 or <0 intohangWatchEl.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
📒 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.jsonfor JavaScript/TypeScript development
Files:
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsxjs/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:
effect()returns a dispose/cleanup function- These cleanup functions are collected and invoked in an
onCleanupcallback- 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
hangPublishElchanges.js/hang-ui/src/Components/watch/WatchUIContextProvider.tsx (2)
7-28: Context types and value shape look consistent and sufficient
WatchUIContextProviderProps,WatchStatus, andWatchUIContextValuesline 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 coherentlyThe
onWatchInstanceAvailableeffects correctly derive:
watchStatusfrom connection URL/status and broadcast status.isPlayingfromvideo.paused.currentVolumefromaudio.volumewith the expected 0–100 mapping.bufferingfromsyncStatus/bufferStatus(matching the existing buffering overlay behavior).latencyfromwatchEl.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.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
js/hang-ui/src/Components/publish/PublishUIContextProvider.tsx (1)
86-86: Inconsistent optional chaining onactiveproperty.Line 78 uses
hangPublishEl?.active?.peek()with optional chaining onactive, but line 86 useshangPublishEl.active.peek?.()without optional chaining onactive. While technically safe due to the control flow (the else branch only executes whenactive.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
📒 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.jsonfor JavaScript/TypeScript development
Files:
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsxjs/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 withinonPublishInstanceAvailableare automatically cleaned up by Solid.js when the outercreateEffectreruns. 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
trackLatencyeffect 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
syncStatusandbufferStatusvalues returned byeffect.get()are guaranteed to be defined. These values come from Signal instances that are always initialized in the Source class and maintain their.stateproperty. Adding optional chaining would be defensive programming without practical benefit.Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
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
setVolumeandsetLatencyValuetrust 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>= 0for 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
volumeandlatencyagainst@kixelated/hang’s current API before adopting this.Also applies to: 62-68
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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.jsonfor 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", plusonCleanupto 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
onWatchInstanceAvailablehelpers consistently derive UI state fromconnection,broadcast,video,audio, andlatencysignals, 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.
There was a problem hiding this comment.
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), butsetLatencyValuedirectly accessessignals.latency.set(). SinceHangWatchhas alatencyproperty 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 HangWatchInstanceis misleading becausepeek()returnsHangWatchInstance | undefined. While the code is safe due to the subsequentif (hangWatchInstance)check, removing the type assertion or usingas HangWatchInstance | undefinedwould 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
📒 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.jsonfor 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
onWatchInstanceAvailablefunction correctly sets up reactive effects to track all relevant watch state (status, playback, volume, muting, buffering, latency). The effects are automatically cleaned up when theHangWatchInstanceis closed (viasignals.close()), which happens when the element disconnects from the DOM.
kixelated
left a comment
There was a problem hiding this comment.
Oh and we could roll this back into @kixelated/hang if we migrate that package over to rollup as well.
js/hang-demo/vite.config.ts
Outdated
| }, | ||
| resolve: { | ||
| alias: { | ||
| "@kixelated/hang-ui/publish/element": path.resolve( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I'll see if I can make it work like the other packages in the workspace.
There was a problem hiding this comment.
removed this and followed the build pattern from the other packages.
js/hang-ui/package.json
Outdated
| }, | ||
| "sideEffects": true, | ||
| "scripts": { | ||
| "build": "npm run clean && rollup -c", |
There was a problem hiding this comment.
And yeah, the other packages use a script to rewrite package.json to change src to dist for publishing. Your alias approach works too.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Updated this to follow the pattern from the other packages. Works as far as I can tell.
|
|
||
| function inlineCss() { | ||
| return { | ||
| name: 'inline-css', |
There was a problem hiding this comment.
We might want to do this anyway for @kixelated/hang because of worklets. I left some thoughts in #677
js/hang-ui/package.json
Outdated
| }, | ||
| "devDependencies": { | ||
| "@biomejs/biome": "^2.2.2", | ||
| "@kixelated/hang": "workspace:^0.7.0", |
There was a problem hiding this comment.
Should this be a peerDependency?
| return <PublishUIContext.Provider value={value}>{props.children}</PublishUIContext.Provider>; | ||
|
|
||
| function onPublishInstanceAvailable(el: HangPublish, publishInstance: HangPublishInstance) { | ||
| publishInstance?.signals.effect(function trackCameraDevices(effect) { |
There was a problem hiding this comment.
you should remove the ? from all of these, it can't be null.
There was a problem hiding this comment.
these should all be gone now.
| const [hangPublishEl, setHangPublishEl] = createSignal<HangPublish>(); | ||
|
|
||
| const onInstanceAvailable = (event: CustomEvent) => { | ||
| const hangPublishEl = event.target as HangPublish; |
There was a problem hiding this comment.
Not a fan of casting, the event should be typed.
There was a problem hiding this comment.
Added typings for both events.
| if (publishEl) { | ||
| setHangPublishEl(publishEl); | ||
| } else { | ||
| element.addEventListener("publish-instance-available", onInstanceAvailable); |
There was a problem hiding this comment.
You can use the signal instead if you want.
const dispose = element.active.watch((instance?: PublishInstance) => {
});
onCleanup(dispose);There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 itRight now
detail.instanceis aSignal<HangWatchInstance | undefined>(viaHangWatch['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 toHangWatchInstanceand 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 settingbubbles: true, composed: trueon theCustomEvent. If you only ever attach the listener directly to<hang-watch>, the current non-bubbling event is fine.
202-307: PublicHangWatchInstance.signalsexposure and lifecycle wiring look consistentSwitching from a private field to a public
signals: Effectwhile keepingEffectownership withinHangWatchInstance(construct, use for attribute sync/effect cleanup, and close inclose()) preserves existing behavior and cleanly exposes the reactive root needed by the new UI layer. The additionalcleanupfor theMutationObserveris correctly tied intosignals, so disconnect happens when the instance is closed.Just be aware this effectively makes
signalspart of the public API; external callers invokinginstance.signals.close()could tear down internal effects. If that’s not desired, you may want to documentsignalsas an advanced/low-level escape hatch.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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.jsonfor 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: Typedwatch-instance-availableevent and DOM augmentation look goodUsing
HangWatch['active']inInstanceAvailableEventkeeps the event detail aligned with theactiveproperty type, and theGlobalEventHandlersEventMapaugmentation will give consumers properly typedaddEventListener/onwatch-instance-availablehandlers without affecting runtime behavior.
…e vite config to remove alias since src exports are used now for hang-ui
There was a problem hiding this comment.
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, andselectedMicrophoneSourceas 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 inconsistentFunctionally, the new
videoDevice/audioDevicesetters are guarded correctly:
- no-op if there is no active instance,
- no-op if the current source has no
deviceproperty,- and they update
device.preferredwhen 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 behaviorThe new
InstanceAvailableEventand'watch-instance-available'dispatch look good and match the publish side. Two small API-shape suggestions:
- The exported alias name
InstanceAvailableEventis now used in both watch and publish modules; consider renaming to something specific likeWatchInstanceAvailableEventto 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-uiis 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 needsThe
InstanceAvailableEventexport 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
PublishInstanceAvailableEventto 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-uicurrently attaches its listener before changing the event options.Also applies to: 48-56
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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.jsonfor JavaScript/TypeScript development
Files:
js/hang-demo/vite.config.tsjs/hang-ui/src/Components/publish/PublishUIContextProvider.tsxjs/hang/src/watch/element.tsjs/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 correctImporting
vite-plugin-solidand addingsolidPlugin()alongsidetailwindcss()in thepluginsarray 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-solidversion inpackage.jsonis officially compatible with Vite6.3.6, and- Tailwind v4 via
@tailwindcss/vitestill processes styles for the new Solid-based components as expected (e.g., runnix develop -c just devand 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
setFilefunction 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-availableevent. 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
trackVideoSourcesActiveeffect 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
trackPublishStatuseffect handles all possible state combinations appropriately.js/hang/src/watch/element.ts (1)
193-308: Signals/effects wiring and cleanup for HangWatchInstance look solidUsing a public
signals: EffectonHangWatchInstanceto own:
- the
MutationObservercleanup viasignals.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/signalsindeed 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 correctThe refactor that:
- exposes
video,audio,file, andsignalsas public fields onHangPublishInstance,- uses
this.signals.cleanupfor theMutationObserver,- drives preview/video/audio/file source selection via
this.signals.effect(...)andeffect.set(...), and- closes everything via
this.signals.close()inclose()is consistent with the prior private-#signals pattern and gives hang-ui a clean way to observe the underlying sources. Guards in
#runSourceensure the correctSource.*is constructed for eachsourcevalue, andeffect.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/signalsmatches 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
There was a problem hiding this comment.
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 movingonPublishInstanceAvailablebefore 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
📒 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.jsonfor 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
trackPublishStatuseffect 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 leavepublishStatusundefined or in an unexpected state.Add explicit status validation or document the expected status values from
publishInstance.connection.statusto ensure all cases are handled correctly.
There was a problem hiding this comment.
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, andselectedMicrophoneSourceas 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?.()buthangWatchElis already confirmed to exist by the guard on Line 87, making the?.onhangWatchElunnecessary.Apply this diff:
- const hangWatchInstance = hangWatchEl?.active?.peek?.(); + const hangWatchInstance = hangWatchEl.active?.peek?.();
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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.jsonfor JavaScript/TypeScript development
Files:
js/hang-ui/src/Components/watch/WatchUIContextProvider.tsxjs/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
setFilehelper correctly guards against undefinedhangPublishEland 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
Effectinstance and closing it inonCleanupproperly 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.
|
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. |
What does it change?
This PR addresses #695. It migrates the Watch and Publish UIs to a separate
hang-uipackage, that provides two Web Components (hang-publish-uiandhang-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
hangWeb Components (hang-publishforhang-publish-uiandhang-watchforhang-watch-ui). In order to use the existing Signal reactivity, the UI components will wait until they have a reference to the respectiveHang...Instance(published as aCustomEvent: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.tsxPublishUIContextProvider.tsxI'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
nix develop -c just dev) to start your local devmainmain