Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

화상채팅 UI 수정 #97

Merged
merged 5 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;