feat: DM voice (draft)

This commit is contained in:
Tristan Yang
2023-05-09 21:35:22 +08:00
parent 6055a2d1e2
commit 118f802e86
9 changed files with 411 additions and 161 deletions
+15 -3
View File
@@ -19,7 +19,8 @@ import {
RenewLicenseResponse,
AgoraTokenResponse,
AgoraVoicingListResponse,
SystemCommon
SystemCommon,
AgoraChannelUsersResponse
} from "../../types/server";
import { Channel } from "../../types/channel";
import { ContentTypeKey } from "../../types/message";
@@ -130,7 +131,8 @@ export const serverApi = createApi({
return {
id: +id,
context,
memberCount: count
memberCount: count,
channelName: data.channel_name
};
});
dispatch(upsertVoiceList(arr));
@@ -140,6 +142,15 @@ export const serverApi = createApi({
}
}
}),
getAgoraUsersByChannel: builder.query<number[], string>({
query: (channel_name) => ({ url: `/admin/agora/channel/user/${channel_name}/false` }),
transformResponse: (resp: AgoraChannelUsersResponse) => {
if (resp.success && resp.data.channel_exist) {
return resp.data.users ?? [];
}
return [];
}
}),
updateAgoraConfig: builder.mutation<void, AgoraConfig>({
query: (data) => ({
url: `/admin/agora/config`,
@@ -405,5 +416,6 @@ export const {
useUpdateSystemCommonMutation,
useLazyGetSystemCommonQuery,
useGetSystemCommonQuery,
useGenerateAgoraTokenMutation
useGenerateAgoraTokenMutation,
useLazyGetAgoraUsersByChannelQuery
} = serverApi;
+20 -5
View File
@@ -4,8 +4,9 @@ import { ConnectionState } from "agora-rtc-sdk-ng";
import { resetAuthData } from "./auth.data";
export type VoiceBasicInfo = {
context: "channel" | "dm"
id: number,
context: "channel" | "dm",
from?: number,
id: number,// means to in dm context
}
export type VoicingInfo = {
@@ -34,9 +35,12 @@ export type VoicingMembers = {
pin?: number
}
export type VoiceInfo = {
channelName: string,
memberCount: number
} & VoiceBasicInfo
interface State {
callingFrom: number,
callingTo: number,
voicing: VoicingInfo | null,
voicingMembers: VoicingMembers,
list: VoiceInfo[]
@@ -47,6 +51,8 @@ interface State {
// members: []
// };
const initialState: State = {
callingFrom: 0,
callingTo: 0,
voicing: null,
voicingMembers: {
ids: [],
@@ -61,6 +67,11 @@ const voiceSlice = createSlice({
name: "voice",
initialState,
reducers: {
updateCalling(state, { payload }: PayloadAction<{ from: number, to?: number }>) {
const { from, to = 0 } = payload;
state.callingFrom = from;
state.callingTo = to;
},
updateVoicingInfo(state, { payload }: PayloadAction<VoicingInfo | null>) {
if (payload) {
state.voicing = { ...(state.voicing ?? {}), ...payload };
@@ -157,11 +168,15 @@ const voiceSlice = createSlice({
if (window.VOICE_CLIENT) {
window.VOICE_CLIENT.leave();
Object.entries(window.VOICE_TRACK_MAP).forEach(([uid, track]) => {
track?.close();
if (track && 'close' in track) {
track.close();
}
delete window.VOICE_TRACK_MAP[+uid];
});
Object.entries(window.VIDEO_TRACK_MAP).forEach(([uid, track]) => {
track?.close();
if (track && 'close' in track) {
track?.close();
}
delete window.VOICE_TRACK_MAP[+uid];
});
}
@@ -174,5 +189,5 @@ const voiceSlice = createSlice({
}
});
export const { updatePin, updateConnectionState, addVoiceMember, removeVoiceMember, upsertVoiceList, updateVoicingInfo, updateVoicingNetworkQuality, updateMuteStatus, updateVoicingMember, updateDeafenStatus } = voiceSlice.actions;
export const { updateCalling, updatePin, updateConnectionState, addVoiceMember, removeVoiceMember, upsertVoiceList, updateVoicingInfo, updateVoicingNetworkQuality, updateMuteStatus, updateVoicingMember, updateDeafenStatus } = voiceSlice.actions;
export default voiceSlice.reducer;
+146
View File
@@ -0,0 +1,146 @@
// import React from 'react';
import { useEffect, useRef } from 'react';
import clsx from 'clsx';
import { useTranslation } from 'react-i18next';
import { Waveform } from '@uiball/loaders';
import { useDispatch } from 'react-redux';
import { motion } from 'framer-motion';
import { useAppSelector } from '../../../app/store';
import IconCallOff from '../../../assets/icons/call.off.svg';
import IconCallAnswer from '../../../assets/icons/call.svg';
import IconMicOff from '../../../assets/icons/mic.off.svg';
import IconMic from '../../../assets/icons/mic.on.svg';
import IconCamera from '../../../assets/icons/camera.svg';
import IconCameraOff from '../../../assets/icons/camera.off.svg';
import IconScreen from '../../../assets/icons/share.screen.svg';
import Avatar from '../Avatar';
import { VoicingInfo, updateCalling } from '../../../app/slices/voice';
import useVoice from './useVoice';
import { playAgoraVideo } from '../../utils';
import Tooltip from '../Tooltip';
type Props = {
from: number
to?: number
}
const DMCalling = ({ from, to = 0 }: Props) => {
const { t } = useTranslation("chat");
const { leave, joinVoice, joining, setMute, closeCamera, openCamera, startShareScreen, stopShareScreen } = useVoice({ id: to, context: "dm" });
const containerRef = useRef(null);
const dispatch = useDispatch();
const { voicingInfo, voicingMembers, fromUser, toUser, loginUser } = useAppSelector(store => {
return {
voicingInfo: store.voice.voicing ?? {},
voicingMembers: store.voice.voicingMembers,
fromUser: store.users.byId[from],
toUser: store.users.byId[to],
loginUser: store.authData.user
};
});
const sendByMe = loginUser?.uid !== toUser.uid;
const handleCancel = () => {
console.log('cancel');
if (sendByMe || voicingMembers.ids.length == 2) {
leave();
}
dispatch(updateCalling({ from: 0, to: 0 }));
};
const handleAnswer = () => {
joinVoice();
};
useEffect(() => {
const ids = voicingMembers.ids;
ids.forEach(id => {
playAgoraVideo(id);
});
}, [voicingMembers.ids]);
if (!fromUser || !toUser) return null;
const connected = voicingMembers.ids.length == 2;
const { name, avatar } = sendByMe ? toUser : fromUser;
const { muted, shareScreen, video } = voicingInfo as VoicingInfo;
const { speakingVolume = 0 } = voicingMembers.byId[sendByMe ? to : from] ?? {};
const speaking = speakingVolume > 50;
return (
<div ref={containerRef} className="fixed top-0 left-0 w-full h-screen z-[999] pointer-events-none flex items-center justify-end pr-10">
<motion.aside drag dragConstraints={containerRef} dragMomentum={false} whileDrag={{ scale: 1.1 }} className={clsx(`pointer-events-auto
rounded bg-gray-800 relative
shadow-lg shadow-slate-200 dark:shadow-slate-800
cursor-move overflow-hidden
`, connected ? "w-80 h-96" : "w-64 h-80")}>
<div className={clsx("w-20 h-20 flex shrink-0 relative transition-opacity mt-32 m-auto")}>
{speaking && <div className={clsx("absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 rounded-full bg-green-500 animate-speaking", "w-[86px] h-[86px]")}></div>}
<Avatar
width={80}
height={80}
className="w-full h-full rounded-full object-cover"
src={avatar}
name={name}
alt="avatar"
/>
</div>
<div className="absolute left-0 top-0 w-full h-full" id={`CAMERA_${from}`}>
{/* from camera video */}
</div>
<div className="absolute right-0-0 top-0 w-40 h-32" id={`CAMERA_${to}`}>
{/* to camera video */}
</div>
<div className={clsx("absolute left-0 top-0 py-5 w-full h-full flex flex-col justify-between items-center", connected ? "bg-transparent" : "bg-gray-800")}>
{!connected && <>
<div className="flex flex-col gap-2 items-center">
<div className="rounded-full overflow-hidden w-20 h-20 shrink-0">
<Avatar name={name} src={avatar} width={80} height={80} className='h-20' />
</div>
<span className='text-white mb-2'>{name}</span>
</div>
<div className='flex flex-col gap-1 items-center my-4'>
<Waveform
size={18}
lineWeight={3}
speed={1}
color='#aaa'
/>
<span className='text-xs text-gray-600 dark:text-gray-400'>{sendByMe ? `Calling` : `Incoming call`}</span>
</div>
</>
}
<div className={clsx("flex gap-3", connected ? "h-full items-end" : "")}>
<Tooltip tip={"Disconnect"} placement="top">
<button onClick={handleCancel} className='flex-center bg-red-600 hover:bg-red-700 py-2 px-3 rounded-lg'>
<IconCallOff className="w-6 h-6" />
</button>
</Tooltip>
{!sendByMe && !connected &&
<Tooltip tip={"Answer"} placement="top">
<button disabled={joining} onClick={handleAnswer} className='flex-center bg-green-600 hover:bg-green-700 py-2 px-3 rounded-lg'>
<IconCallAnswer className="w-6 h-6 fill-white" />
</button>
</Tooltip>
}
{connected && <>
<Tooltip tip={muted ? t("unmute") : t("mute")} placement="top">
<button onClick={setMute.bind(null, !muted)} className="flex-center py-2 px-3 rounded-lg bg-gray-100 dark:bg-gray-900">
{muted ? <IconMicOff className="fill-gray-700 dark:fill-gray-200" /> : <IconMic className="fill-gray-700 dark:fill-gray-200" />}
</button>
</Tooltip>
<Tooltip tip={video ? t("camera_off") : t("camera_on")} placement="top">
<button onClick={video ? closeCamera : openCamera} className="flex-center py-2 px-3 rounded-lg bg-gray-100 dark:bg-gray-900">
{video ? <IconCamera className="fill-gray-700 dark:fill-gray-200" /> : <IconCameraOff className="fill-gray-700 dark:fill-gray-200" />}
</button>
</Tooltip>
<Tooltip tip={"Share Screen"} placement="top">
<button onClick={shareScreen ? stopShareScreen : startShareScreen} className={clsx("flex-center py-2 px-3 rounded-lg ", shareScreen ? "bg-green-700" : "bg-gray-100 dark:bg-gray-900")}>
<IconScreen className={clsx("dark:fill-gray-200 w-6 h-6", shareScreen ? "fill-gray-200" : "fill-gray-700")} />
</button>
</Tooltip>
</>}
</div>
</div>
</motion.aside>
</div>
);
};
export default DMCalling;
+173
View File
@@ -0,0 +1,173 @@
import AgoraRTC from 'agora-rtc-sdk-ng';
import { memo, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { useGetAgoraChannelsQuery, useGetAgoraStatusQuery, useLazyGetAgoraUsersByChannelQuery } from '../../../app/services/server';
import { addVoiceMember, removeVoiceMember, updateCalling, updateConnectionState, updateVoicingMember, updateVoicingNetworkQuality } from '../../../app/slices/voice';
import { useAppSelector } from '../../../app/store';
import { playAgoraVideo } from '../../utils';
import useVoice from './useVoice';
import DMCalling from './DMCalling';
AgoraRTC.setLogLevel(process.env.NODE_ENV === 'development' ? 0 : 4);
window.VOICE_TRACK_MAP = window.VOICE_TRACK_MAP ?? {};
window.VIDEO_TRACK_MAP = window.VIDEO_TRACK_MAP ?? {};
// let tmpUids: number[] = [];
const Voice = () => {
const { from, to, voiceList, loginUid } = useAppSelector(store => {
return {
voiceList: store.voice.list,
from: store.voice.callingFrom,
to: store.voice.callingTo,
loginUid: store.authData.user?.uid
};
});
const { data: enabled } = useGetAgoraStatusQuery();
const [getUsersByChannel] = useLazyGetAgoraUsersByChannelQuery();
useGetAgoraChannelsQuery({ page_no: 0, page_size: 100 }, {
skip: !enabled || !navigator.onLine,
pollingInterval: 5000
});
const dispatch = useDispatch();
useEffect(() => {
const initializeAgoraClient = async () => {
// 创建agora客户端实例
const agoraEngine = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });
// 无论频道内是否有人说话,都会每两秒返回提示音量
agoraEngine.enableAudioVolumeIndicator();
// Listen for the "user-published" event to retrieve an AgoraRTCRemoteUser object.
agoraEngine.on("user-published", async (user, mediaType) => {
// Subscribe to the remote user when the SDK triggers the "user-published" event.
await agoraEngine.subscribe(user, mediaType);
console.log(user, " has published at the channel");
if (mediaType == "audio") {
// 播放远端音频
user.audioTrack?.play();
window.VOICE_TRACK_MAP[+user.uid] = user.audioTrack;
}
if (mediaType == "video") {
// const label = user.videoTrack?.getMediaStreamTrack()?.label;
// console.log("labell", label);
// if (label?.includes("screen")) {
// // 远端用户共享屏幕
// // const screenTrack = user.videoTrack as ShareScreenTrack;
// // screenTrack.on("track-ended", () => {
// // // 远端用户停止共享屏幕
// dispatch(updateVoicingMember({ uid: +user.uid, info: { shareScreen: true } }));
// }
window.VIDEO_TRACK_MAP[+user.uid] = user.videoTrack;
playAgoraVideo(+user.uid);
}
agoraEngine.on("user-unpublished", (user) => {
if (!user.hasAudio) {
// 远端用户取消了音频(muted)
dispatch(updateVoicingMember({ uid: +user.uid, info: { muted: true } }));
}
if (!user.hasVideo) {
// 远端用户取消了视频
dispatch(updateVoicingMember({ uid: +user.uid, info: { video: false, shareScreen: false } }));
// 注销本地视频变量
window.VIDEO_TRACK_MAP[+user.uid] = null;
// 关闭视频后,需要把视频的高度设置回去
const playerEle = document.querySelector(`#CAMERA_${user.uid}`) as HTMLElement;
playerEle.classList.remove("h-[120px]");
}
});
//remote user leave
agoraEngine.on("user-left", (user, reason) => {
console.log(user, "has left the channel", reason);
switch (reason) {
case "Quit":
case "ServerTimeOut": {
dispatch(removeVoiceMember(+user.uid as number));
// clear tracks
window.VOICE_TRACK_MAP[+user.uid] = null;
window.VIDEO_TRACK_MAP[+user.uid] = null;
}
break;
default:
break;
}
});
// 报告频道内正在说话的远端用户及其音量的回调。
agoraEngine.on("volume-indicator", (vols) => {
vols.forEach((vol, index) => {
console.log(`${index} UID ${vol.uid} Level ${vol.level}`);
const { uid, level } = vol;
dispatch(updateVoicingMember({ uid: +uid, info: { speakingVolume: level } }));
});
});
// 信号强度
agoraEngine.on("network-quality", (qlt) => {
const { downlinkNetworkQuality } = qlt;
dispatch(updateVoicingNetworkQuality(downlinkNetworkQuality));
});
// 连接状态有变化
agoraEngine.on("connection-state-change", (state, prevState, reason) => {
console.log("connection-state-change", state, prevState, reason);
dispatch(updateConnectionState(state));
});
// 用户状态变化
agoraEngine.on("user-info-updated", (uid, msg) => {
console.log("user-info-updated", uid, msg);
switch (msg) {
case "mute-audio":
// todo
dispatch(updateVoicingMember({ uid: +uid, info: { muted: true } }));
break;
case "unmute-audio":
// todo
dispatch(updateVoicingMember({ uid: +uid, info: { muted: false } }));
break;
default:
break;
}
});
});
// 有新用户加入
agoraEngine.on("user-joined", async (user) => {
console.log(user.uid, agoraEngine.channelName, " has joined the channel");
dispatch(addVoiceMember(+user.uid));
});
window.VOICE_CLIENT = agoraEngine;
};
const handlePageUnload = (evt: BeforeUnloadEvent) => {
console.log("unload");
if (window.VOICE_CLIENT?.connectionState === "CONNECTED") {
evt.preventDefault();
return (evt.returnValue = "");
}
};
window.addEventListener("beforeunload", handlePageUnload, { capture: true });
if (!window.VOICE_CLIENT) {
initializeAgoraClient();
}
return () => {
window.removeEventListener("beforeunload", handlePageUnload, { capture: true });
};
}, []);
useEffect(() => {
// 有人呼叫我
const callMeList = voiceList.filter(item => item.context == "dm" && item.id == loginUid);
if (callMeList.length) {
const [firstCall] = callMeList;
const { memberCount, channelName, id } = firstCall;
if (memberCount == 1) {
getUsersByChannel(channelName).then(resp => {
const [uid] = resp.data ?? [];
if (uid) {
dispatch(updateCalling({ from: uid, to: id }));
}
});
}
}
}, [voiceList, loginUid]);
// return <DMCalling uid={1} sendByMe={calling !== loginUid} />;
if (from !== 0) return <DMCalling from={from} to={to} />;
return null;
};
export { useVoice };
export default memo(Voice);
@@ -1,147 +1,16 @@
import AgoraRTC, { ICameraVideoTrack, IMicrophoneAudioTrack } from 'agora-rtc-sdk-ng';
import { memo, useEffect } from 'react';
import { ShareScreenTrack } from '../../../types/global';
import AudioJoin from '../../../assets/join.wav';
import { useDispatch } from 'react-redux';
import { useGetAgoraChannelsQuery, useGetAgoraStatusQuery, useGenerateAgoraTokenMutation } from '../../app/services/server';
import { updateChannelVisibleAside } from '../../app/slices/footprint';
import { addVoiceMember, removeVoiceMember, updateConnectionState, updateDeafenStatus, updateMuteStatus, updatePin, updateVoicingInfo, updateVoicingMember, updateVoicingNetworkQuality, upsertVoiceList } from '../../app/slices/voice';
import { useAppSelector } from '../../app/store';
import AudioJoin from '../../assets/join.wav';
import { playAgoraVideo } from '../utils';
import { ShareScreenTrack } from '../../types/global';
AgoraRTC.setLogLevel(process.env.NODE_ENV === 'development' ? 0 : 4);
window.VOICE_TRACK_MAP = window.VOICE_TRACK_MAP ?? {};
window.VIDEO_TRACK_MAP = window.VIDEO_TRACK_MAP ?? {};
// let tmpUids: number[] = [];
const Voice = () => {
const { data: enabled } = useGetAgoraStatusQuery();
useGetAgoraChannelsQuery({ page_no: 0, page_size: 100 }, {
skip: !enabled || !navigator.onLine,
pollingInterval: 5000
});
const dispatch = useDispatch();
useEffect(() => {
const initializeAgoraClient = async () => {
// 创建agora客户端实例
const agoraEngine = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });
// 无论频道内是否有人说话,都会每两秒返回提示音量
agoraEngine.enableAudioVolumeIndicator();
// Listen for the "user-published" event to retrieve an AgoraRTCRemoteUser object.
agoraEngine.on("user-published", async (user, mediaType) => {
// Subscribe to the remote user when the SDK triggers the "user-published" event.
await agoraEngine.subscribe(user, mediaType);
console.log(user, " has published at the channel");
if (mediaType == "audio") {
// 播放远端音频
user.audioTrack?.play();
window.VOICE_TRACK_MAP[+user.uid] = user.audioTrack;
}
if (mediaType == "video") {
const label = user.videoTrack?.getMediaStreamTrack()?.label;
console.log("labell", label);
import { useGenerateAgoraTokenMutation } from '../../../app/services/server';
import { updateChannelVisibleAside, updateDMVisibleAside } from '../../../app/slices/footprint';
if (label?.includes("screen")) {
// 远端用户共享屏幕
// const screenTrack = user.videoTrack as ShareScreenTrack;
// screenTrack.on("track-ended", () => {
// // 远端用户停止共享屏幕
dispatch(updateVoicingMember({ uid: +user.uid, info: { shareScreen: true } }));
}
import { addVoiceMember, updateDeafenStatus, updateMuteStatus, updatePin, updateVoicingInfo, upsertVoiceList } from '../../../app/slices/voice';
import { useAppSelector } from '../../../app/store';
import { playAgoraVideo } from '../../utils';
window.VIDEO_TRACK_MAP[+user.uid] = user.videoTrack;
playAgoraVideo(+user.uid);
}
agoraEngine.on("user-unpublished", (user) => {
if (!user.hasAudio) {
// 远端用户取消了音频(muted)
dispatch(updateVoicingMember({ uid: +user.uid, info: { muted: true } }));
}
if (!user.hasVideo) {
// 远端用户取消了视频
dispatch(updateVoicingMember({ uid: +user.uid, info: { video: false, shareScreen: false } }));
// 注销本地视频变量
window.VIDEO_TRACK_MAP[+user.uid] = null;
// 关闭视频后,需要把视频的高度设置回去
const playerEle = document.querySelector(`#CAMERA_${user.uid}`) as HTMLElement;
playerEle.classList.remove("h-[120px]");
}
});
//remote user leave
agoraEngine.on("user-left", (user, reason) => {
console.log(user, "has left the channel");
switch (reason) {
case "Quit":
case "ServerTimeOut": {
dispatch(removeVoiceMember(+user.uid as number));
// clear tracks
window.VOICE_TRACK_MAP[+user.uid] = null;
window.VIDEO_TRACK_MAP[+user.uid] = null;
}
break;
default:
break;
}
});
// 报告频道内正在说话的远端用户及其音量的回调。
agoraEngine.on("volume-indicator", (vols) => {
vols.forEach((vol, index) => {
console.log(`${index} UID ${vol.uid} Level ${vol.level}`);
const { uid, level } = vol;
dispatch(updateVoicingMember({ uid: +uid, info: { speakingVolume: level } }));
});
});
// 信号强度
agoraEngine.on("network-quality", (qlt) => {
const { downlinkNetworkQuality } = qlt;
dispatch(updateVoicingNetworkQuality(downlinkNetworkQuality));
});
// 连接状态有变化
agoraEngine.on("connection-state-change", (state, prevState, reason) => {
console.log("connection-state-change", state, prevState, reason);
dispatch(updateConnectionState(state));
});
// 用户状态变化
agoraEngine.on("user-info-updated", (uid, msg) => {
console.log("user-info-updated", uid, msg);
switch (msg) {
case "mute-audio":
// todo
dispatch(updateVoicingMember({ uid: +uid, info: { muted: true } }));
break;
case "unmute-audio":
// todo
dispatch(updateVoicingMember({ uid: +uid, info: { muted: false } }));
break;
default:
break;
}
});
});
// 有新用户加入
agoraEngine.on("user-joined", async (user) => {
console.log(user.uid, agoraEngine.channelName, " has joined the channel");
dispatch(addVoiceMember(+user.uid));
});
window.VOICE_CLIENT = agoraEngine;
};
const handlePageUnload = (evt: BeforeUnloadEvent) => {
console.log("unload");
if (window.VOICE_CLIENT?.connectionState === "CONNECTED") {
evt.preventDefault();
return (evt.returnValue = "");
}
};
window.addEventListener("beforeunload", handlePageUnload, { capture: true });
if (!window.VOICE_CLIENT) {
initializeAgoraClient();
}
return () => {
window.removeEventListener("beforeunload", handlePageUnload, { capture: true });
};
}, []);
return null;
};
type VoiceProps = {
id: number,
context?: "channel" | "dm"
@@ -305,17 +174,16 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
localVideoTrack.close();
window.VIDEO_TRACK_MAP[loginUid] = null;
}
if (context == "channel") {
dispatch(updateChannelVisibleAside({
id, aside: null
}));
// 即时更新对应的活跃列表信息
dispatch(upsertVoiceList({
id,
context,
memberCount: 0
}));
}
const updateAside = context == "channel" ? updateChannelVisibleAside : updateDMVisibleAside;
dispatch(updateAside({ id, aside: null }));
// 即时更新对应的活跃列表信息
dispatch(upsertVoiceList({
id,
context,
memberCount: 0,
// will fix it
channelName: `vocechat:${context}:${id}`,
}));
}
};
const setMute = (mute: boolean) => {
@@ -366,5 +234,5 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
stopShareScreen,
};
};
export { useVoice };
export default memo(Voice);
export default useVoice;
+5
View File
@@ -10,6 +10,7 @@ import Tooltip from '../../../common/component/Tooltip';
import { useVoice } from '../../../common/component/Voice';
import { useGetAgoraStatusQuery } from '../../../app/services/server';
import { ChatContext } from '../../../types/common';
import { updateCalling } from '../../../app/slices/voice';
type Props = {
context?: ChatContext
@@ -47,6 +48,10 @@ const VoiceChat = ({ id, context = "channel" }: Props) => {
aside: "voice" as const
};
dispatch(context == "channel" ? updateChannelVisibleAside(data) : updateDMVisibleAside(data));
// 实时显示calling box
if (!joinedAtThisContext && context == "dm") {
dispatch(updateCalling({ from: loginUser?.uid ?? 0, to: id }));
}
};
if (!loginUser || !enabled) return null;
const visible = visibleAside == "voice";
+9
View File
@@ -38,6 +38,15 @@ export interface AgoraVoicingListResponse {
total_size: number
}
}
export interface AgoraChannelUsersResponse {
success: boolean,
data: {
channel_exist: boolean,
mode?: number,
total?: number,
users?: number[]
}
}
export interface AgoraTokenResponse {
agora_token: string,
uid: number,