diff --git a/client/web/src/plugin/common/index.ts b/client/web/src/plugin/common/index.ts index d5eaca05..aa5dbd2a 100644 --- a/client/web/src/plugin/common/index.ts +++ b/client/web/src/plugin/common/index.ts @@ -53,6 +53,7 @@ export { useGroupPanelInfo, sendMessage, showMessageTime, + joinArray, } from 'tailchat-shared'; export { useLocation, useNavigate } from 'react-router'; diff --git a/client/web/tailchat.d.ts b/client/web/tailchat.d.ts index 0893ecc3..b93bbba9 100644 --- a/client/web/tailchat.d.ts +++ b/client/web/tailchat.d.ts @@ -164,6 +164,8 @@ declare module '@capital/common' { export const showMessageTime: any; + export const joinArray: any; + export const useLocation: any; export const useNavigate: any; @@ -244,21 +246,6 @@ declare module '@capital/common' { export const pluginPanelActions: any; - interface BasePluginPanelActionProps { - /** - * 唯一标识 - */ - name: string; - /** - * 显示名 - */ - label: string; - /** - * 来自iconify的图标标识 - */ - icon: string; - } - export const regPluginPanelAction: ( action: | { @@ -311,6 +298,10 @@ declare module '@capital/common' { export const regPluginGroupTextPanelExtraMenu: any; + export const pluginUserExtraInfo: any; + + export const regUserExtraInfo: any; + export const useGroupIdContext: () => string; export const useGroupPanelContext: () => { @@ -488,4 +479,8 @@ declare module '@capital/component' { }>; export const Markdown: any; + + export const Webview: any; + + export const WebviewKeepAlive: any; } diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/MeetingView.tsx b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/MeetingView.tsx index c3c2d247..0c99d396 100644 --- a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/MeetingView.tsx +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/MeetingView.tsx @@ -65,6 +65,15 @@ export const MeetingView: React.FC = React.memo((props) => { useMeetingStore.getState().removeUser(user); }); + client.on('volume-indicator', (volumes) => { + useMeetingStore.setState({ + volumes: volumes.map((v) => ({ + uid: String(v.uid), + level: v.level, + })), + }); + }); + try { const { _id } = await getJWTUserInfo(); const { data } = await request.post('generateJoinInfo', { @@ -75,6 +84,7 @@ export const MeetingView: React.FC = React.memo((props) => { await client.join(appId, channelName, token, _id); await client.enableDualStream(); + client.enableAudioVolumeIndicator(); setStart(true); } catch (err) { showErrorToasts(err); diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/SpeakerNames.tsx b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/SpeakerNames.tsx new file mode 100644 index 00000000..79e341ce --- /dev/null +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/SpeakerNames.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { joinArray } from '@capital/common'; +import { useMeetingStore } from './store'; +import { UserName } from '@capital/component'; +import { Translate } from '../translate'; + +export const SpeakerNames: React.FC = React.memo(() => { + const volumes = useMeetingStore((state) => state.volumes); + const activeUserNames = volumes + .filter((v) => v.level >= 60) + .map((v) => ); + + return ( + + {joinArray(activeUserNames, ',')} + + {activeUserNames.length > 0 + ? ' ' + Translate.isSpeaking + : Translate.nomanSpeaking} + + ); +}); +SpeakerNames.displayName = 'SpeakerNames'; diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/VideoView.tsx b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/VideoView.tsx index 77c948a8..858e5a23 100644 --- a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/VideoView.tsx +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/VideoView.tsx @@ -5,7 +5,9 @@ import styled from 'styled-components'; import { useClient, useMicrophoneAndCameraTracks } from './client'; import { useMeetingStore } from './store'; -const Root = styled.div` +const Root = styled.div<{ + active?: boolean; +}>` width: 95%; height: auto; position: relative; @@ -19,6 +21,10 @@ const Root = styled.div` justify-content: center; align-items: center; + border-width: 3px; + border-color: ${(props) => (props.active ? '#7ab157;' : 'transparent')}; + transition: border-color 0.2s; + .player { width: 100%; height: 100%; @@ -36,9 +42,10 @@ export const VideoView: React.FC<{ user: IAgoraRTCRemoteUser; }> = (props) => { const user = props.user; + const active = useVolumeActive(String(user.uid)); return ( - + {user.hasVideo ? ( ) : ( @@ -55,9 +62,10 @@ export const OwnVideoView: React.FC<{}> = React.memo(() => { const { ready, tracks } = useMicrophoneAndCameraTracks(); const client = useClient(); const mediaPerm = useMeetingStore((state) => state.mediaPerm); + const active = useVolumeActive(String(client.uid)); return ( - + {ready && mediaPerm.video ? ( ) : ( @@ -69,3 +77,12 @@ export const OwnVideoView: React.FC<{}> = React.memo(() => { ); }); OwnVideoView.displayName = 'OwnVideoView'; + +function useVolumeActive(uid: string) { + const volume = useMeetingStore((state) => + state.volumes.find((v) => v.uid === uid) + ); + const volumeLevel = volume?.level ?? 0; + + return volumeLevel >= 60; +} diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/store.ts b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/store.ts index 5e23194d..86568f2d 100644 --- a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/store.ts +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/store.ts @@ -15,6 +15,16 @@ interface MeetingState { * 本地媒体权限 */ mediaPerm: MediaPerm; + /** + * 音量信息 + */ + volumes: { + /** + * 音量, 0~100, 一般认为60以上视为正在发言. + */ + level: number; + uid: string; + }[]; appendUser: (user: IAgoraRTCRemoteUser) => void; removeUser: (user: IAgoraRTCRemoteUser) => void; /** @@ -29,6 +39,7 @@ interface MeetingState { export const useMeetingStore = create((set) => ({ users: [], mediaPerm: { video: false, audio: false }, + volumes: [], appendUser: (user: IAgoraRTCRemoteUser) => { set((state) => ({ users: [...state.users, user], diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/window.tsx b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/window.tsx index 4103b140..670fd472 100644 --- a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/window.tsx +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/FloatWindow/window.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import styled from 'styled-components'; import { ErrorBoundary } from '@capital/component'; import { MeetingView, MeetingViewProps } from './MeetingView'; +import { SpeakerNames } from './SpeakerNames'; const FloatWindow = styled.div` z-index: 100; @@ -17,18 +18,23 @@ const FloatWindow = styled.div` flex-direction: column; .folder-btn { - background-color: var(--tc-content-background-color); - box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); position: absolute; bottom: -30px; - height: 30px; - line-height: 30px; - left: 50%; - width: 60px; - margin-left: -30px; - text-align: center; - cursor: pointer; - border-radius: 0 0 3px 3px; + left: 0; + right: 0; + display: flex; + + > div { + background-color: var(--tc-content-background-color); + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2); + height: 30px; + line-height: 30px; + cursor: pointer; + border-radius: 0 0 3px 3px; + margin: auto; + display: inline-block; + padding: 0 8px; + } } `; @@ -50,7 +56,10 @@ export const FloatMeetingWindow: React.FC = React.memo(
setFolder(!folder)}> - {folder ? '展开' : '收起'} +
+ + {folder ? '展开' : '收起'} +
); diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/translate.ts b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/translate.ts index bf67b5ea..fdbceb2d 100644 --- a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/translate.ts +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/src/translate.ts @@ -9,4 +9,12 @@ export const Translate = { 'zh-CN': '下行网络', 'en-US': 'Downlink', }), + isSpeaking: localTrans({ + 'zh-CN': '正在发言', + 'en-US': 'is Speaking', + }), + nomanSpeaking: localTrans({ + 'zh-CN': '无人发言', + 'en-US': 'No one Speaking', + }), }; diff --git a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/types/tailchat.d.ts b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/types/tailchat.d.ts index e60fe980..b93bbba9 100644 --- a/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/types/tailchat.d.ts +++ b/server/plugins/com.msgbyte.agora/web/plugins/com.msgbyte.agora/types/tailchat.d.ts @@ -164,6 +164,8 @@ declare module '@capital/common' { export const showMessageTime: any; + export const joinArray: any; + export const useLocation: any; export const useNavigate: any; @@ -244,21 +246,6 @@ declare module '@capital/common' { export const pluginPanelActions: any; - interface BasePluginPanelActionProps { - /** - * 唯一标识 - */ - name: string; - /** - * 显示名 - */ - label: string; - /** - * 来自iconify的图标标识 - */ - icon: string; - } - export const regPluginPanelAction: ( action: | { @@ -311,6 +298,10 @@ declare module '@capital/common' { export const regPluginGroupTextPanelExtraMenu: any; + export const pluginUserExtraInfo: any; + + export const regUserExtraInfo: any; + export const useGroupIdContext: () => string; export const useGroupPanelContext: () => { @@ -474,12 +465,22 @@ declare module '@capital/component' { export const ErrorBoundary: any; - export const UserAvatar: any; + export const UserAvatar: React.FC<{ + userId: string; + className?: string; + style?: React.CSSProperties; + size?: 'large' | 'small' | 'default' | number; + }>; export const UserName: React.FC<{ userId: string; className?: string; + style?: React.CSSProperties; }>; export const Markdown: any; + + export const Webview: any; + + export const WebviewKeepAlive: any; }