meet/app/custom/CustomControlBar.tsx
2025-03-20 22:22:01 +05:30

188 lines
5.9 KiB
TypeScript

'use client';
import React, { useState, useEffect } from 'react';
import { DisconnectButton, useLayoutContext, useRoomContext } from '@livekit/components-react';
import { Room, RoomEvent, Track } from 'livekit-client';
import { mergeClasses } from '@/lib/client-utils';
import { ToggleSource } from '@livekit/components-core';
import '../../styles/CustomControlBar.css';
import { CameraOffSVG, CameraOnSVG } from '../svg/camera';
import { MicOffSVG, MicOnSVG } from '../svg/mic';
import { ScreenShareOnSVG } from '../svg/screen-share';
import { useCustomLayoutContext } from '../contexts/layout-context';
interface CustomControlBarProps {
room: Room;
roomName: string;
}
export function CustomControlBar({ room, roomName }: CustomControlBarProps) {
const [recording, setRecording] = useState(false);
const [participantCount, setParticipantCount] = useState(1);
const { dispatch } = useLayoutContext().widget;
const { isParticipantsListOpen } = useCustomLayoutContext();
function ToggleParticipantsList() {
if (isParticipantsListOpen.dispatch)
isParticipantsListOpen.dispatch({ msg: 'toggle_participants_list' });
}
useEffect(() => {
if (room) {
const updateRecordingStatus = () => setRecording(room.isRecording);
const updateParticipantCount = () => {
setParticipantCount(room.numParticipants);
};
room.on(RoomEvent.Connected, updateParticipantCount);
room.on(RoomEvent.ParticipantConnected, updateParticipantCount);
room.on(RoomEvent.ParticipantDisconnected, updateParticipantCount);
room.on(RoomEvent.RecordingStatusChanged, updateRecordingStatus);
return () => {
room.off(RoomEvent.Connected, updateParticipantCount);
room.off(RoomEvent.ParticipantConnected, updateParticipantCount);
room.off(RoomEvent.ParticipantDisconnected, updateParticipantCount);
room.off(RoomEvent.RecordingStatusChanged, updateRecordingStatus);
};
}
}, [room]);
const handleCopyLink = () => {
navigator.clipboard
.writeText(window.location.href)
.then(() => alert('Link copied to clipboard!'))
.catch((err) => console.error('Failed to copy link:', err));
};
return (
<div className="custom-control-bar">
<div className="room-name-box">
<span className="room-name">{roomName}</span>
<button className="copy-link-button" onClick={handleCopyLink}>
<span className="material-symbols-outlined">content_copy</span>
</button>
</div>
{/* Center: Control Buttons */}
<div className="control-bar control-buttons">
<TrackToggle source={Track.Source.Microphone} />
<TrackToggle source={Track.Source.Camera} />
<div className={`control-btn ${recording ? '' : 'disabled'}`}>
<span className="material-symbols-outlined">radio_button_checked</span>
</div>
<TrackToggle source={Track.Source.ScreenShare} />
<DisconnectButton className="end-call-button">
<span className="material-symbols-outlined">call_end</span>
</DisconnectButton>
</div>
{/* Participants, Settings btn */}
<div className="top-right-controls">
<div className="participant-box" onClick={ToggleParticipantsList}>
<span className="material-symbols-outlined">people</span>
<span className="participant-count">{participantCount}</span>
</div>
<div
className="settings-box"
onClick={() => {
if (dispatch) dispatch({ msg: 'toggle_settings' });
}}
>
<span className="material-symbols-outlined">settings</span>
</div>
</div>
</div>
);
}
interface ControlButtonProps {
enabled?: boolean;
icon: React.ReactNode;
className?: string;
onClick?: () => void;
}
function ControlButton({ enabled = true, icon, className, onClick }: ControlButtonProps) {
return (
<button
className={mergeClasses('control-btn', className)}
data-lk-active={!enabled}
onClick={onClick}
>
{icon}
</button>
);
}
function TrackToggle({ source }: { source: ToggleSource }) {
const { enabled, toggle } = useTrackToggle({ source });
const isScreenShare = source === Track.Source.ScreenShare;
return (
<ControlButton
onClick={toggle}
enabled={isScreenShare ? !enabled : enabled}
icon={<TrackIcon trackSource={source} enabled={isScreenShare ? !enabled : enabled} />}
/>
);
}
interface TrackIconProps {
trackSource: ToggleSource;
enabled: boolean;
}
function TrackIcon({ trackSource, enabled }: TrackIconProps) {
switch (trackSource) {
case Track.Source.Camera:
return enabled ? <CameraOnSVG /> : <CameraOffSVG />;
case Track.Source.Microphone:
return enabled ? <MicOnSVG /> : <MicOffSVG />;
case Track.Source.ScreenShare:
return enabled ? (
<ScreenShareOnSVG />
) : (
<span
button-state="inactive"
data-lk-screen-share-enabled="true"
className="material-symbols-outlined"
>
stop_screen_share
</span>
);
}
}
// Custom hook for track toggle
function useTrackToggle({ source }: { source: ToggleSource }) {
const { localParticipant } = useRoomContext();
const toggle = () => {
switch (source) {
case Track.Source.Camera:
return localParticipant.setCameraEnabled(!enabled);
case Track.Source.Microphone:
return localParticipant.setMicrophoneEnabled(!enabled);
case Track.Source.ScreenShare:
return localParticipant.setScreenShareEnabled(!enabled);
}
};
const enabled = (() => {
switch (source) {
case Track.Source.Camera:
return localParticipant.isCameraEnabled;
case Track.Source.Microphone:
return localParticipant.isMicrophoneEnabled;
case Track.Source.ScreenShare:
return localParticipant.isScreenShareEnabled;
}
})();
return { enabled, toggle };
}