Skip to content

Commit

Permalink
Merge pull request #97 from B03-Killer/feat/#82
Browse files Browse the repository at this point in the history
화상채팅 UI  수정
  • Loading branch information
MinKonKim authored Aug 1, 2024
2 parents fe58002 + 316ab24 commit fdf7f46
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import GearIcon from '@/icons/Gear.svg';
import UserIcon from '@/icons/User.svg';
import UserIcon from '@/icons/User2.svg';
import {
useLocalParticipantPermissions,
useMaybeLayoutContext,
usePersistentUserChoices
} from '@livekit/components-react';
import { Track } from 'livekit-client';
import React from 'react';
import { HTMLAttributes, useCallback, useEffect, useMemo, useState } from 'react';
import TrackToggle from '../../../_components/TrackToggle';
import { useMediaQuery } from '../../_hooks/useMediaQuery';
import { supportsScreenSharing } from '../../_utils/supportsScreenSharing';
Expand All @@ -20,7 +20,7 @@ export type ControlBarControls = {
settings?: boolean;
};

export interface ControlBarProps extends React.HTMLAttributes<HTMLDivElement> {
export interface ControlBarProps extends HTMLAttributes<HTMLDivElement> {
variation?: 'minimal' | 'verbose' | 'textOnly';
controls?: ControlBarControls;
/**
Expand All @@ -33,9 +33,10 @@ export interface ControlBarProps extends React.HTMLAttributes<HTMLDivElement> {
}

const BottomControlBar = ({ variation, controls, saveUserChoices = true, ...props }: ControlBarProps) => {
const [isChatOpen, setIsChatOpen] = React.useState(false);
const [isChatOpen, setIsChatOpen] = useState(false);

const layoutContext = useMaybeLayoutContext();
React.useEffect(() => {
useEffect(() => {
if (layoutContext?.widget.state?.showChat !== undefined) {
setIsChatOpen(layoutContext?.widget.state?.showChat);
}
Expand All @@ -61,14 +62,14 @@ const BottomControlBar = ({ variation, controls, saveUserChoices = true, ...prop
visibleControls.chat ??= localPermissions.canPublishData && controls?.chat;
}

const showIcon = React.useMemo(() => variation === 'minimal' || variation === 'verbose', [variation]);
const showText = React.useMemo(() => variation === 'textOnly' || variation === 'verbose', [variation]);
const showIcon = useMemo(() => variation === 'minimal' || variation === 'verbose', [variation]);
const showText = useMemo(() => variation === 'textOnly' || variation === 'verbose', [variation]);

const browserSupportsScreenSharing = supportsScreenSharing();

const [isScreenShareEnabled, setIsScreenShareEnabled] = React.useState(false);
const [isScreenShareEnabled, setIsScreenShareEnabled] = useState(false);

const onScreenShareChange = React.useCallback(
const onScreenShareChange = useCallback(
(enabled: boolean) => {
setIsScreenShareEnabled(enabled);
},
Expand All @@ -78,18 +79,18 @@ const BottomControlBar = ({ variation, controls, saveUserChoices = true, ...prop
const { saveAudioInputEnabled, saveVideoInputEnabled, saveAudioInputDeviceId, saveVideoInputDeviceId } =
usePersistentUserChoices({ preventSave: !saveUserChoices });

const microphoneOnChange = React.useCallback(
const microphoneOnChange = useCallback(
(enabled: boolean, isUserInitiated: boolean) => (isUserInitiated ? saveAudioInputEnabled(enabled) : null),
[saveAudioInputEnabled]
);

const cameraOnChange = React.useCallback(
const cameraOnChange = useCallback(
(enabled: boolean, isUserInitiated: boolean) => (isUserInitiated ? saveVideoInputEnabled(enabled) : null),
[saveVideoInputEnabled]
);

return (
<div className="flex border-t-2 justify-between mx-4 p-4 fixed bottom-1 right-0 left-0 bg-white w-[100vw] ">
<div className="flex border-t-2 justify-between mx-4 p-4 fixed bottom-1 bg-white w-[100vw] ">
{visibleControls.microphone && (
<div className="">
<TrackToggle source={Track.Source.Microphone} showIcon={showIcon} onChange={microphoneOnChange}></TrackToggle>
Expand Down Expand Up @@ -128,12 +129,6 @@ const BottomControlBar = ({ variation, controls, saveUserChoices = true, ...prop
<button className="w-12 h-6 flex justify-center">
<GearIcon />
</button>
{/* {visibleControls.leave && (
<DisconnectButton>
{showIcon && <LeaveIcon />}
{showText && 'Leave'}
</DisconnectButton>
)} */}
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import Typography from '@/components/Typography';
import useWorkspaceId from '@/hooks/useWorkspaceId';
import ArrowLeftIcon from '@/icons/ArrowLeft.svg';
import LeaveIcon from '@/icons/LogOut.svg';
import { useParams, useRouter } from 'next/navigation';
import DeviceMenuButton from '../../../_components/DeviceMenuButton';
import DisconnectButton from '../../../_components/DisconnectButton';
const VideoChannelHeader = () => {
const params = useParams();
Expand All @@ -11,13 +10,11 @@ const VideoChannelHeader = () => {
const workspaceId = useWorkspaceId();
return (
<div className="flex items-center justify-between px-4 py-3 mt-[2px]">
<ArrowLeftIcon className="size-7" onClick={() => router.back()} />
<DeviceMenuButton />
<Typography color="grey700Black" variant="Title20px" as="h2">
{name}
</Typography>
<DisconnectButton onClick={() => router.push(`/${workspaceId}/chat`)}>
<LeaveIcon />
</DisconnectButton>
<DisconnectButton onClick={() => router.push(`/${workspaceId}/chat`)}>{'종료'}</DisconnectButton>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ const CustomVideoConference = () => {
return (
<>
<VideoChannelHeader />
<div className="flex flex-col gap-2 h-[80vh] p-3">
<div className="flex flex-col items-center gap-2 h-[80vh] p-3">
<div className="flex p-4 h-full items-center">
<div className={`${focusedTrack ? 'sm:w-[80vw] m-5 lg:w-full' : 'none'} rounded-lg overflow-hidden mr-5`}>
<div className={`${focusedTrack ? 'sm:w-[full] m-5' : 'none'} rounded-lg overflow-hidden mr-5`}>
{focusedTrack && <FocusLayout trackRef={focusedTrack} className="fixed left-0 " />}
</div>
<div className={`${focusedTrack ? 'hidden md:block w-[300px]' : 'w-full'} h-full bg-slate-100`}>
<div className={`${focusedTrack ? 'hidden md:block w-[300px]' : 'w-full'} h-full`}>
<GridLayout tracks={tracks} style={{ height: 'calc(50vh 50vw - var(--lk-control-bar-height))' }}>
<ParticipantTile onParticipantClick={clickFocus} />
</GridLayout>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import useWorkspaceId from '@/hooks/useWorkspaceId';
import useEnterdChannelStore from '@/store/enteredChannelStore';
import useStreamSetStore from '@/store/streamSetStore';
import useUserStore from '@/store/userStore';
import { LiveKitRoom, RoomAudioRenderer } from '@livekit/components-react';
import { LiveKitRoom, RoomAudioRenderer, usePersistentUserChoices } from '@livekit/components-react';
import { RoomConnectOptions } from 'livekit-client';
import { redirect, useRouter, useSearchParams } from 'next/navigation';
import { redirect, useParams, useRouter, useSearchParams } from 'next/navigation';
import { useCallback, useEffect, useMemo, useState } from 'react';
import Loading from '../../../_components/Loading';
import { deleteChannel } from '../../_utils/videoChannelDelete';
Expand All @@ -20,22 +20,26 @@ type videoRoomProps = {
const VideoRoom = ({ name }: videoRoomProps) => {
const workspaceId = useWorkspaceId();
const router = useRouter();
const params = useSearchParams();
const params = useParams();
const searchParams = useSearchParams();
const [token, setToken] = useState('');

const { preJoinChoices, isSettingOk, setIsSettingOk } = useStreamSetStore();
const { userChoices } = usePersistentUserChoices();
const { isSettingOk, setIsSettingOk } = useStreamSetStore();
const { enteredChannelId } = useEnterdChannelStore();
const { leaveChannel } = useChannelUser({ channelId: enteredChannelId! });
const { workspaceUserId } = useUserStore();

useEffect(() => {
if (!params.get('username') || !isSettingOk) {
if (!searchParams.get('username') || !isSettingOk) {
redirect(`/${workspaceId}/video-channel/prejoin?room=${name}`);
return;
}
(async () => {
try {
const resp = await fetch(`/api/get-participant-token?room=${name}&username=${workspaceUserId}`);
const room = params.name;
console.log(room, workspaceUserId);
const resp = await fetch(`/api/get-participant-token?room=${room}&username=${userChoices.username}`);
const data = await resp.json();
setToken(data.token);
} catch (e) {
Expand Down Expand Up @@ -67,8 +71,8 @@ const VideoRoom = ({ name }: videoRoomProps) => {

return (
<LiveKitRoom
video={preJoinChoices.videoEnabled}
audio={preJoinChoices.audioEnabled}
video={userChoices.videoEnabled}
audio={userChoices.audioEnabled}
token={token}
serverUrl={process.env.NEXT_PUBLIC_LIVEKIT_URL}
style={{ height: '100vh' }}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use client';
import Button from '@/components/Button';
import Typography from '@/components/Typography';
import SpeakerIcon from '@/icons/Volume2.svg';
import SpeakerMuteIcon from '@/icons/VolumeX.svg';
import { useState } from 'react';
import useAudioOutput from '../../../_hooks/useAudioOutput';

type DeviceMenueProps = {
onClose: () => void;
};

const DeviceMenu = ({ onClose }: DeviceMenueProps) => {
const { devices, setAudioOutputDevice, toggleMute, isMuted } = useAudioOutput();
const [selectedDeviceId, setSelectedDeviceId] = useState<string | null>(null);

const handleDeviceChange = (deviceId: string) => {
setAudioOutputDevice(deviceId);
setSelectedDeviceId(deviceId);
};
console.log(devices);
return (
<div>
<Typography variant="Title18px" color="grey700Black">
오디오 장치 선택
</Typography>
<ul className="">
{devices.length === 0 && <li>오디오를 찾을 수 없습니다.</li>}
{devices.map((device) => (
<li
key={device.deviceId}
onClick={() => handleDeviceChange(device.deviceId)}
className={`cursor-pointer hover:bg-gray-200 p-2 rounded flex items-center gap-3 ${
selectedDeviceId === device.deviceId ? 'bg-blue-100' : ''
}`}
>
<SpeakerIcon className="w-[22px]" />
<Typography variant="Body14px" color="grey600">
{device.label || `Device ${device.deviceId}`}
</Typography>
</li>
))}
</ul>
<div onClick={toggleMute} className="cursor-pointer rounded flex items-center gap-3 p-2 hover:bg-gray-200">
<SpeakerMuteIcon className="w-[22px]" />
<Typography variant="Body14px" color="grey600">
오디오 끔
</Typography>
</div>
<Button theme="primary" isFullWidth className="mt-3" onClick={onClose}>
다음
</Button>
</div>
);
};

export default DeviceMenu;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './DeviceMenu';
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import BottomSheet from '@/components/BottomSheet';
import Typography from '@/components/Typography';
import VolumeIcon from '@/icons/Volume2.svg';
import { useState } from 'react';
import DeviceMenu from '../DeviceMenu/DeviceMenu';
const DeviceMenuButton = () => {
const [isOpen, setIsOpen] = useState<boolean>(false);

const handleClose = () => {
setIsOpen(false);
};

return (
<>
<button onClick={() => setIsOpen(true)} className="flex flex-col items-center justify-center">
<VolumeIcon className="w-[24px] h-[24px] bottom-0" />
<Typography variant="Body12px" color="grey700Black">
스피커
</Typography>
</button>
<BottomSheet isOpen={isOpen} onClose={handleClose}>
<DeviceMenu onClose={handleClose} />
</BottomSheet>
</>
);
};

export default DeviceMenuButton;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './DeviceMenuButton';
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
'use client';
import { defaultUserChoices, log } from '@livekit/components-core';
import { LocalUserChoices, useMediaDevices, usePersistentUserChoices } from '@livekit/components-react';

import Button from '@/components/Button';
import PersonFilledIcon from '@/icons/PersonFilled.svg';
import useStreamSetStore from '@/store/streamSetStore';
import type { CreateLocalTracksOptions, LocalAudioTrack, LocalTrack, LocalVideoTrack } from 'livekit-client';
import {
createLocalAudioTrack,
Expand All @@ -15,6 +15,7 @@ import {
VideoPresets
} from 'livekit-client';
import React from 'react';
import DeviceMenuButton from '../../../_components/DeviceMenuButton';
import TrackToggle from '../../../_components/TrackToggle';

export interface PreJoinProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSubmit' | 'onError'> {
Expand Down Expand Up @@ -183,14 +184,13 @@ const CustomPrejoin = ({
onError,
debug,
joinLabel = 'Join Room',
micLabel = 'Microphone',
camLabel = 'Camera',
micLabel = '마이크',
camLabel = '카메라',
userLabel = 'Username',
persistUserChoices = true,
...htmlProps
}: PreJoinProps) => {
const [userChoices, setUserChoices] = React.useState(defaultUserChoices);
const { setPreJoinChoices } = useStreamSetStore();

// TODO: Remove and pipe `defaults` object directly into `usePersistentUserChoices` once we fully switch from type `LocalUserChoices` to `UserChoices`.
const partialDefaults: Partial<LocalUserChoices> = {
Expand Down Expand Up @@ -351,6 +351,9 @@ const CustomPrejoin = ({
>
{camLabel}
</TrackToggle>
<div>
<DeviceMenuButton />
</div>
</div>

<form className="flex flex-col items-center gap-3 ">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { RefObject, useEffect, useRef, useState } from 'react';

interface Device {
deviceId: string;
kind: string;
label: string;
}

const useAudioOutput = () => {
const [devices, setDevices] = useState<Device[]>([]);
const [isMuted, setIsMuted] = useState(false);
const audioRef: RefObject<HTMLAudioElement> = useRef(null);

useEffect(() => {
navigator.mediaDevices
.enumerateDevices()
.then((devices) => {
const audioOutputDevices = devices.filter((device) => device.kind === 'audiooutput');
setDevices(audioOutputDevices);
})
.catch((err) => {
console.error(`Error: ${err.name}: ${err.message}`);
});
}, []);

const setAudioOutputDevice = (deviceId: string) => {
if (audioRef.current && typeof audioRef.current.sinkId !== 'undefined') {
audioRef.current
.setSinkId(deviceId)
.then(() => {
console.log(`Success, audio output device attached: ${deviceId}`);
})
.catch((error) => {
console.error(`Error: ${error.name}: ${error.message}`);
});
} else {
console.warn('Browser does not support output device selection.');
}
};

const toggleMute = () => {
if (audioRef.current) {
audioRef.current.muted = !audioRef.current.muted;
setIsMuted(audioRef.current.muted);
}
};

return { audioRef, devices, setAudioOutputDevice, toggleMute, isMuted };
};

export default useAudioOutput;

0 comments on commit fdf7f46

Please sign in to comment.