feat: add livekit member panel

chore/devcontainer
moonrailgun 2 years ago
parent c937178956
commit 19a1e5dbe5

@ -65,6 +65,7 @@ export { ErrorBoundary } from '@/components/ErrorBoundary';
export { ErrorView } from '@/components/ErrorView'; export { ErrorView } from '@/components/ErrorView';
export { UserAvatar } from '@/components/UserAvatar'; export { UserAvatar } from '@/components/UserAvatar';
export { UserName } from '@/components/UserName'; export { UserName } from '@/components/UserName';
export { UserListItem } from '@/components/UserListItem';
export { Markdown, MarkdownEditor } from '@/components/Markdown'; export { Markdown, MarkdownEditor } from '@/components/Markdown';
export { Webview, WebviewKeepAlive } from '@/components/Webview'; export { Webview, WebviewKeepAlive } from '@/components/Webview';
export { Card } from '@/components/Card'; export { Card } from '@/components/Card';

@ -12,11 +12,6 @@ import * as React from 'react';
import { Translate } from '../../translate'; import { Translate } from '../../translate';
import { cloneSingleChild } from '../../utils/common'; import { cloneSingleChild } from '../../utils/common';
import { useObservableState } from '../../utils/useObservableState'; import { useObservableState } from '../../utils/useObservableState';
// import { useRoomContext } from '../context';
// import { useObservableState } from '../hooks/internal/useObservableState';
// import { cloneSingleChild } from '../utils';
// import type { MessageFormatter } from '../components/ChatEntry';
// import { ChatEntry } from '../components/ChatEntry';
export type { ChatMessage, ReceivedChatMessage }; export type { ChatMessage, ReceivedChatMessage };

@ -3,7 +3,6 @@ import * as React from 'react';
import { supportsScreenSharing } from '@livekit/components-core'; import { supportsScreenSharing } from '@livekit/components-core';
import { import {
ChatToggle,
DisconnectButton, DisconnectButton,
MediaDeviceMenu, MediaDeviceMenu,
StartAudio, StartAudio,
@ -13,15 +12,15 @@ import {
} from '@livekit/components-react'; } from '@livekit/components-react';
import { Translate } from '../../translate'; import { Translate } from '../../translate';
import { useMediaQuery } from '../../utils/useMediaQuery'; import { useMediaQuery } from '../../utils/useMediaQuery';
import { ChatIcon } from './icons/ChatIcon';
import { LeaveIcon } from './icons/LeaveIcon';
import { useMeetingContextState } from '../../context/MeetingContext'; import { useMeetingContextState } from '../../context/MeetingContext';
import { Icon } from '@capital/component';
/** @public */ /** @public */
export type ControlBarControls = { export type ControlBarControls = {
microphone?: boolean; microphone?: boolean;
camera?: boolean; camera?: boolean;
chat?: boolean; chat?: boolean;
member?: boolean;
screenShare?: boolean; screenShare?: boolean;
leave?: boolean; leave?: boolean;
}; };
@ -71,13 +70,15 @@ export function ControlBar({ variation, controls, ...props }: ControlBarProps) {
if (!localPermissions) { if (!localPermissions) {
visibleControls.camera = false; visibleControls.camera = false;
visibleControls.chat = false; visibleControls.chat = false;
visibleControls.member = false;
visibleControls.microphone = false; visibleControls.microphone = false;
visibleControls.screenShare = false; visibleControls.screenShare = false;
} else { } else {
visibleControls.camera ??= localPermissions.canPublish; visibleControls.camera ??= localPermissions.canPublish;
visibleControls.microphone ??= localPermissions.canPublish; visibleControls.microphone ??= localPermissions.canPublish;
visibleControls.screenShare ??= localPermissions.canPublish; visibleControls.screenShare ??= localPermissions.canPublish;
visibleControls.chat ??= localPermissions.canPublishData && controls?.chat; visibleControls.chat ??= localPermissions.canPublishData;
visibleControls.member ??= localPermissions.canSubscribe;
} }
const showIcon = React.useMemo( const showIcon = React.useMemo(
@ -137,14 +138,21 @@ export function ControlBar({ variation, controls, ...props }: ControlBarProps) {
{visibleControls.chat && ( {visibleControls.chat && (
<button className="lk-button" onClick={() => setRightPanel('chat')}> <button className="lk-button" onClick={() => setRightPanel('chat')}>
{showIcon && <ChatIcon />} {showIcon && <Icon icon="mdi:message-reply-text-outline" />}
{showText && Translate.chat} {showText && Translate.chat}
</button> </button>
)} )}
{visibleControls.member && (
<button className="lk-button" onClick={() => setRightPanel('member')}>
{showIcon && <Icon icon="mdi:account-multiple" />}
{showText && Translate.member}
</button>
)}
{visibleControls.leave && ( {visibleControls.leave && (
<DisconnectButton> <DisconnectButton>
{showIcon && <LeaveIcon />} {showIcon && <Icon icon="mdi:logout-variant" />}
{showText && Translate.leave} {showText && Translate.leave}
</DisconnectButton> </DisconnectButton>
)} )}

@ -0,0 +1,54 @@
import { useParticipants } from '@livekit/components-react';
import * as React from 'react';
import styled from 'styled-components';
import { Icon, UserListItem } from '@capital/component';
import { useEvent } from '@capital/common';
import type { Participant } from 'livekit-client';
import { Translate } from '../../translate';
const MemberList = styled.div`
display: flex;
flex-direction: column;
align-items: stretch;
width: clamp(200px, 55ch, 60ch);
background-color: var(--lk-bg2);
border-left: 1px solid var(--lk-border-color);
padding: 8px;
`;
const IsSpeakingTip = styled.div`
font-size: 12px;
opacity: 0.6;
`;
export const Member: React.FC = React.memo(() => {
const participants = useParticipants();
const getAction = useEvent((participant: Participant) => {
return [
!participant.isSpeaking && (
<IsSpeakingTip>({Translate.isSpeaking})</IsSpeakingTip>
),
<div key="mic-state">
{participant.isMicrophoneEnabled ? (
<Icon icon="mdi:microphone" />
) : (
<Icon icon="mdi:microphone-off" />
)}
</div>,
];
});
return (
<MemberList>
{participants.map((member) => (
<UserListItem
key={member.sid}
userId={member.identity}
actions={getAction(member)}
/>
))}
</MemberList>
);
});
Member.displayName = 'Member';

@ -29,6 +29,7 @@ import { ControlBar } from './ControlBar';
import { Chat } from './Chat'; import { Chat } from './Chat';
import { FocusLayout } from './FocusLayout'; import { FocusLayout } from './FocusLayout';
import { useMeetingContextState } from '../../context/MeetingContext'; import { useMeetingContextState } from '../../context/MeetingContext';
import { Member } from './Member';
/** /**
* @public * @public
@ -145,12 +146,14 @@ export const VideoConference: React.FC<VideoConferenceProps> = React.memo(
</div> </div>
)} )}
<ControlBar controls={{ chat: true }} /> <ControlBar />
</div> </div>
{rightPanel === 'chat' && ( {rightPanel === 'chat' && (
<Chat messageFormatter={chatMessageFormatter} /> <Chat messageFormatter={chatMessageFormatter} />
)} )}
{rightPanel === 'member' && <Member />}
</LayoutContextProvider> </LayoutContextProvider>
)} )}

@ -1,25 +0,0 @@
import * as React from 'react';
import type { SVGProps } from 'react';
export const ChatIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={16}
height={18}
fill="none"
{...props}
>
<path
fill="currentColor"
fillRule="evenodd"
d="M0 2.75A2.75 2.75 0 0 1 2.75 0h10.5A2.75 2.75 0 0 1 16 2.75v13.594a.75.75 0 0 1-1.234.572l-3.691-3.12a1.25 1.25 0 0 0-.807-.296H2.75A2.75 2.75 0 0 1 0 10.75v-8ZM2.75 1.5c-.69 0-1.25.56-1.25 1.25v8c0 .69.56 1.25 1.25 1.25h7.518c.65 0 1.279.23 1.775.65l2.457 2.077V2.75c0-.69-.56-1.25-1.25-1.25H2.75Z"
clipRule="evenodd"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M3 4.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5Zm0 2a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5Zm0 2a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5Z"
clipRule="evenodd"
/>
</svg>
);

@ -1,25 +0,0 @@
import * as React from 'react';
import type { SVGProps } from 'react';
export const LeaveIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={16}
height={16}
fill="none"
{...props}
>
<path
fill="currentColor"
fillRule="evenodd"
d="M2 2.75A2.75 2.75 0 0 1 4.75 0h6.5A2.75 2.75 0 0 1 14 2.75v10.5A2.75 2.75 0 0 1 11.25 16h-6.5A2.75 2.75 0 0 1 2 13.25v-.5a.75.75 0 0 1 1.5 0v.5c0 .69.56 1.25 1.25 1.25h6.5c.69 0 1.25-.56 1.25-1.25V2.75c0-.69-.56-1.25-1.25-1.25h-6.5c-.69 0-1.25.56-1.25 1.25v.5a.75.75 0 0 1-1.5 0v-.5Z"
clipRule="evenodd"
/>
<path
fill="currentColor"
fillRule="evenodd"
d="M8.78 7.47a.75.75 0 0 1 0 1.06l-2.25 2.25a.75.75 0 1 1-1.06-1.06l.97-.97H1.75a.75.75 0 0 1 0-1.5h4.69l-.97-.97a.75.75 0 0 1 1.06-1.06l2.25 2.25Z"
clipRule="evenodd"
/>
</svg>
);

@ -41,6 +41,10 @@ export const Translate = {
'zh-CN': '聊天', 'zh-CN': '聊天',
'en-US': 'Chat', 'en-US': 'Chat',
}), }),
member: localTrans({
'zh-CN': '成员',
'en-US': 'Member',
}),
leave: localTrans({ leave: localTrans({
'zh-CN': '离开', 'zh-CN': '离开',
'en-US': 'Leave', 'en-US': 'Leave',
@ -53,6 +57,10 @@ export const Translate = {
'zh-CN': '输入消息...', 'zh-CN': '输入消息...',
'en-US': 'Enter a message...', 'en-US': 'Enter a message...',
}), }),
isSpeaking: localTrans({
'zh-CN': '正在发言',
'en-US': 'Is speaking',
}),
nobodyInMeeting: localTrans({ nobodyInMeeting: localTrans({
'zh-CN': '当前无人在会...', 'zh-CN': '当前无人在会...',
'en-US': 'Nobody in Meeting...', 'en-US': 'Nobody in Meeting...',

@ -10,11 +10,13 @@ export function useObservableState<T>(
startWith: T startWith: T
) { ) {
const [state, setState] = React.useState<T>(startWith); const [state, setState] = React.useState<T>(startWith);
React.useEffect(() => { React.useEffect(() => {
// observable state doesn't run in SSR // observable state doesn't run in SSR
if (typeof window === 'undefined' || !observable) return; if (typeof window === 'undefined' || !observable) return;
const subscription = observable.subscribe(setState); const subscription = observable.subscribe(setState);
return () => subscription.unsubscribe(); return () => subscription.unsubscribe();
}, [observable]); }, [observable]);
return state; return state;
} }

Loading…
Cancel
Save