diff --git a/client/packages/design/components/Avatar/index.tsx b/client/packages/design/components/Avatar/index.tsx index 11ff9307..3b22a337 100644 --- a/client/packages/design/components/Avatar/index.tsx +++ b/client/packages/design/components/Avatar/index.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { ForwardRefExoticComponent, useMemo } from 'react'; import { Avatar as AntdAvatar, Badge } from 'antd'; import _head from 'lodash/head'; import _upperCase from 'lodash/upperCase'; @@ -17,7 +17,8 @@ export interface AvatarProps extends AntdAvatarProps { name?: string; isOnline?: boolean; } -export const Avatar: React.FC = React.memo((_props) => { + +const _Avatar: React.FC = React.memo((_props) => { const { isOnline, ...props } = _props; const src = isValidStr(props.src) ? imageUrlParser(props.src) : undefined; @@ -85,4 +86,11 @@ export const Avatar: React.FC = React.memo((_props) => { return inner; }); -Avatar.displayName = 'Avatar'; +_Avatar.displayName = 'Avatar'; + +type CompoundedComponent = ForwardRefExoticComponent & { + Group: typeof AntdAvatar.Group; +}; + +export const Avatar: CompoundedComponent = _Avatar as any; +Avatar.Group = AntdAvatar.Group; diff --git a/client/web/src/components/UserAvatar.tsx b/client/web/src/components/UserAvatar.tsx index c80f0409..2a669a64 100644 --- a/client/web/src/components/UserAvatar.tsx +++ b/client/web/src/components/UserAvatar.tsx @@ -1,25 +1,22 @@ +import type { AvatarProps } from 'antd'; import React from 'react'; import { Avatar } from 'tailchat-design'; import { useCachedUserInfo } from 'tailchat-shared'; -interface UserAvatarProps { +interface UserAvatarProps extends AvatarProps { userId: string; - className?: string; - style?: React.CSSProperties; - size?: 'large' | 'small' | 'default' | number; } /** * 用户头像组件 */ export const UserAvatar: React.FC = React.memo((props) => { - const cachedUserInfo = useCachedUserInfo(props.userId); + const { userId, ...avatarProps } = props; + const cachedUserInfo = useCachedUserInfo(userId); return ( diff --git a/server/plugins/com.msgbyte.livekit/services/livekit.service.dev.ts b/server/plugins/com.msgbyte.livekit/services/livekit.service.dev.ts index 98ce7e87..036da139 100644 --- a/server/plugins/com.msgbyte.livekit/services/livekit.service.dev.ts +++ b/server/plugins/com.msgbyte.livekit/services/livekit.service.dev.ts @@ -1,7 +1,7 @@ import type { TcContext } from 'tailchat-server-sdk'; import { TcService, TcDbService } from 'tailchat-server-sdk'; import type { LivekitDocument, LivekitModel } from '../models/livekit'; -import { AccessToken } from 'livekit-server-sdk'; +import { AccessToken, RoomServiceClient } from 'livekit-server-sdk'; /** * livekit @@ -12,6 +12,8 @@ interface LivekitService extends TcService, TcDbService {} class LivekitService extends TcService { + // roomServiceClient: RoomServiceClient = null; + get serviceName() { return 'plugin:com.msgbyte.livekit'; } @@ -39,6 +41,10 @@ class LivekitService extends TcService { return false; } + getRoomServiceClient() { + return new RoomServiceClient(this.livekitUrl, this.apiKey, this.apiSecret); + } + onInit() { this.registerAvailableAction(() => this.serverAvailable); @@ -52,7 +58,16 @@ class LivekitService extends TcService { this.registerLocalDb(require('../models/livekit').default); this.registerAction('url', this.url); - this.registerAction('generateToken', this.generateToken); + this.registerAction('generateToken', this.generateToken, { + params: { + roomName: 'string', + }, + }); + this.registerAction('roomMembers', this.roomMembers, { + params: { + roomName: 'string', + }, + }); } async url(ctx: TcContext) { @@ -90,6 +105,21 @@ class LivekitService extends TcService { accessToken, }; } + + async roomMembers(ctx: TcContext<{ roomName: string }>) { + if (!this.serverAvailable) { + throw new Error('livekit server not available'); + } + + try { + const client = this.getRoomServiceClient(); + const participants = await client.listParticipants(ctx.params.roomName); + + return participants; + } catch (err) { + return []; + } + } } export default LivekitService; diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/LivekitView.tsx b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/LivekitView.tsx index d26bab47..1ee4c5d0 100644 --- a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/LivekitView.tsx +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/LivekitView.tsx @@ -45,6 +45,7 @@ const _LivekitView: React.FC = React.memo((props) => { ) : (
= React.memo((props) => { + const containerEl = useRef(null); + const [{ value: participants = [] }, _handleFetchParticipants] = + useAsyncFn(async () => { + const { data } = await request.post('roomMembers', { + roomName: props.roomName, + }); + + return data ?? []; + }, [props.roomName]); + + const handleFetchParticipants = useEvent(_handleFetchParticipants); + + useEffect(() => { + let timer: number; + + const fn = async () => { + if (containerEl.current && containerEl.current.offsetWidth !== 0) { + // 该元素可见 + await handleFetchParticipants(); + } + + timer = window.setTimeout(fn, 3000); + }; + + timer = window.setTimeout(fn, 3000); + + return () => { + if (timer) { + clearTimeout(timer); + } + }; + }, []); + + let inner: React.ReactNode; + + if (participants.length === 0) { + inner = Translate.nobodyInMeeting; + } else { + inner = ( + <> +
{Translate.peopleInMeeting}
+ + {[ + ...participants, + ...participants, + ...participants, + ...participants, + ...participants, + ...participants, + ].map((info, i) => ( + } + placement="top" + > + + + ))} + + + ); + } + + return ( +
+ {inner} +
+ ); +}); +ParticipantAvatars.displayName = 'ParticipantAvatars'; diff --git a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/lib/PreJoinView.tsx b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/lib/PreJoinView.tsx index 783e33e2..9e6d1739 100644 --- a/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/lib/PreJoinView.tsx +++ b/server/plugins/com.msgbyte.livekit/web/plugins/com.msgbyte.livekit/src/components/lib/PreJoinView.tsx @@ -16,6 +16,7 @@ import * as React from 'react'; import { useEffect, useMemo, useRef, useState } from 'react'; import { log } from '@livekit/components-core'; import { Translate } from '../../translate'; +import { ParticipantAvatars } from '../ParticipantAvatars'; /** * Fork from "@livekit/components-react" @@ -43,6 +44,7 @@ export type PreJoinProps = Omit< React.HTMLAttributes, 'onSubmit' > & { + roomName: string; /** This function is called with the `LocalUserChoices` if validation is passed. */ onSubmit?: (values: LocalUserChoices) => void; /** @@ -77,6 +79,7 @@ export type PreJoinProps = Omit< */ export const PreJoinView: React.FC = React.memo( ({ + roomName, defaults = {}, onValidate, onSubmit, @@ -191,6 +194,8 @@ export const PreJoinView: React.FC = React.memo( return (
+ +
{videoTrack && (