refactor: organize desktop vs. mobile into separate folders

This commit is contained in:
Arjun Patel
2026-04-29 08:42:56 -07:00
parent 3d9fe67936
commit 3a11a82cd3
194 changed files with 213 additions and 213 deletions
+213
View File
@@ -0,0 +1,213 @@
import '@livekit/components-styles';
import {
CarouselLayout,
Chat,
ControlBar,
FocusLayout,
FocusLayoutContainer,
GridLayout,
LayoutContextProvider,
LiveKitRoom,
ParticipantTile,
RoomAudioRenderer,
StartAudio,
isTrackReference,
useCreateLayoutContext,
usePinnedTracks,
useRoomContext,
useTracks,
ScreenShareIcon,
ScreenShareStopIcon,
} from '@livekit/components-react';
import { RoomEvent, Track } from 'livekit-client';
import { useState, useEffect, useRef, useCallback } from 'react';
import { ScreenSourcePicker } from '@/components/screen-source-picker';
import { logError } from '@/lib/errors';
function readConnectionFromHash(): { token: string; serverUrl: string } | null {
const params = new URLSearchParams(window.location.hash.slice(1));
const token = params.get('token');
const serverUrl = params.get('serverUrl');
return token && serverUrl ? { token, serverUrl } : null;
}
export function HuddleApp() {
const [connection] = useState(readConnectionFromHash);
const handleDisconnected = () => {
window.electronWindow.closeHuddle();
};
if (!connection) {
return (
<div className="flex h-screen items-center justify-center bg-background text-foreground">
<p className="text-muted-foreground text-sm">Missing huddle connection data.</p>
</div>
);
}
return (
<LiveKitRoom
token={connection.token}
serverUrl={connection.serverUrl}
connect={true}
audio={true}
video={false}
onDisconnected={handleDisconnected}
data-lk-theme="default"
style={{ height: '100vh' }}
>
<HuddleContent />
</LiveKitRoom>
);
}
function HuddleContent() {
const room = useRoomContext();
const [showPicker, setShowPicker] = useState(false);
const [isSharing, setIsSharing] = useState(false);
const [widgetState, setWidgetState] = useState({ showChat: false, unreadMessages: 0 });
const tracks = useTracks(
[
{ source: Track.Source.Camera, withPlaceholder: true },
{ source: Track.Source.ScreenShare, withPlaceholder: false },
],
{ updateOnlyOn: [RoomEvent.ActiveSpeakersChanged], onlySubscribed: false },
);
const layoutContext = useCreateLayoutContext();
const screenShareTracks = tracks
.filter(isTrackReference)
.filter((t) => t.publication.source === Track.Source.ScreenShare);
const pinnedTracks = usePinnedTracks(layoutContext);
const focusTrack = pinnedTracks?.[0];
const remainingTracks = tracks.filter(
(t) =>
!focusTrack ||
t.participant.identity !== focusTrack.participant.identity ||
t.source !== focusTrack.source,
);
// Auto-pin/unpin screen share tracks (same logic as VideoConference)
const lastAutoPin = useRef<{ trackSid: string } | null>(null);
useEffect(() => {
if (
screenShareTracks.some((t) => t.publication.isSubscribed) &&
lastAutoPin.current === null
) {
layoutContext.pin.dispatch?.({ msg: 'set_pin', trackReference: screenShareTracks[0] });
lastAutoPin.current = { trackSid: screenShareTracks[0].publication.trackSid };
} else if (
lastAutoPin.current &&
!screenShareTracks.some(
(t) => t.publication.trackSid === lastAutoPin.current?.trackSid,
)
) {
layoutContext.pin.dispatch?.({ msg: 'clear_pin' });
lastAutoPin.current = null;
}
}, [
screenShareTracks.map((t) => `${t.publication.trackSid}_${t.publication.isSubscribed}`).join(),
]);
// Sync isSharing state with actual track state
useEffect(() => {
const onLocalTrackUnpublished = () => {
const pub = room.localParticipant.getTrackPublication(Track.Source.ScreenShare);
if (!pub) setIsSharing(false);
};
room.on(RoomEvent.LocalTrackUnpublished, onLocalTrackUnpublished);
return () => {
room.off(RoomEvent.LocalTrackUnpublished, onLocalTrackUnpublished);
};
}, [room]);
const handleScreenShare = useCallback(
async (sourceId: string) => {
setShowPicker(false);
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: sourceId,
},
} as MediaTrackConstraints,
});
const videoTrack = stream.getVideoTracks()[0];
await room.localParticipant.publishTrack(videoTrack, {
source: Track.Source.ScreenShare,
});
setIsSharing(true);
videoTrack.onended = () => stopScreenShare();
} catch (err) {
logError(err, { scope: "huddle.screenShare" });
}
},
[room],
);
const stopScreenShare = useCallback(async () => {
const pub = room.localParticipant.getTrackPublication(Track.Source.ScreenShare);
if (pub?.track) {
await room.localParticipant.unpublishTrack(pub.track.mediaStreamTrack);
pub.track.stop();
}
setIsSharing(false);
}, [room]);
return (
<div className="lk-video-conference">
<LayoutContextProvider value={layoutContext} onWidgetChange={setWidgetState}>
<div className="lk-video-conference-inner">
{focusTrack ? (
<div className="lk-focus-layout-wrapper">
<FocusLayoutContainer>
<CarouselLayout tracks={remainingTracks}>
<ParticipantTile />
</CarouselLayout>
<FocusLayout trackRef={focusTrack} />
</FocusLayoutContainer>
</div>
) : (
<div className="lk-grid-layout-wrapper">
<GridLayout tracks={tracks}>
<ParticipantTile />
</GridLayout>
</div>
)}
<div className="lk-control-bar">
<ControlBar
controls={{ screenShare: false, chat: true }}
style={{ border: 'none', padding: 0, maxHeight: 'none' }}
/>
<button
className="lk-button"
onClick={isSharing ? stopScreenShare : () => setShowPicker(true)}
aria-label={isSharing ? 'Stop sharing' : 'Share screen'}
>
{isSharing ? <ScreenShareStopIcon /> : <ScreenShareIcon />}
{isSharing ? 'Stop share' : 'Share screen'}
</button>
</div>
</div>
<Chat style={{ display: widgetState.showChat ? 'grid' : 'none' }} />
</LayoutContextProvider>
<RoomAudioRenderer />
<StartAudio label="Allow audio" />
{showPicker && (
<ScreenSourcePicker
title="Share your screen"
confirmLabel="Share"
getSources={window.electronHuddle.getScreenSources}
onSelect={handleScreenShare}
onCancel={() => setShowPicker(false)}
/>
)}
</div>
);
}
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html class="dark">
<head>
<meta charset="UTF-8" />
<title>llink - Huddle</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./renderer.tsx"></script>
</body>
</html>
@@ -0,0 +1,9 @@
import { createRoot } from 'react-dom/client';
import { HuddleApp } from './HuddleApp';
import { initSentryRenderer } from '@/lib/sentry';
import '@/styles/globals.css';
initSentryRenderer();
const root = createRoot(document.getElementById('root')!);
root.render(<HuddleApp />);