From 92411d0468010e6f752dd1d733c3050e2ea56ea3 Mon Sep 17 00:00:00 2001 From: Tristan Yang Date: Fri, 24 Mar 2023 23:07:56 +0800 Subject: [PATCH] feat: useVoice --- package.json | 2 +- src/app/services/base.query.ts | 3 +- src/app/services/server.ts | 33 ++++- src/app/slices/auth.data.ts | 5 +- src/app/slices/voice.ts | 63 +++++++++ src/app/store.ts | 2 + src/assets/icons/signal.svg | 3 + src/assets/icons/voicing.svg | 5 + src/common/component/Voice.tsx | 122 ++++++++++++++++++ src/routes/chat/RTCWidget.tsx | 33 +++-- src/routes/chat/SessionList/Session.tsx | 7 +- src/routes/chat/VoiceChat/Dashboard.tsx | 72 ++--------- src/routes/chat/VoiceChat/JoinVoice.tsx | 15 ++- src/routes/chat/VoiceChat/VoiceManagement.tsx | 24 +++- src/routes/chat/VoiceChat/index.tsx | 32 ++--- src/routes/chat/VoiceChat/useVoice.tsx | 23 ---- src/routes/chat/index.tsx | 4 +- src/routes/home/index.tsx | 2 + src/types/global.d.ts | 6 + src/types/server.ts | 7 + 20 files changed, 331 insertions(+), 132 deletions(-) create mode 100644 src/app/slices/voice.ts create mode 100644 src/assets/icons/signal.svg create mode 100644 src/assets/icons/voicing.svg create mode 100644 src/common/component/Voice.tsx delete mode 100644 src/routes/chat/VoiceChat/useVoice.tsx diff --git a/package.json b/package.json index b68da4f5..202b7cc7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vocechat-web", - "version": "0.3.55", + "version": "0.3.56", "private": true, "homepage": "https://voce.chat", "dependencies": { diff --git a/src/app/services/base.query.ts b/src/app/services/base.query.ts index 7383eabd..11d9c03d 100644 --- a/src/app/services/base.query.ts +++ b/src/app/services/base.query.ts @@ -27,7 +27,8 @@ const whiteList = [ "createAdmin", "getBotRelatedChannels", "sendMessageByBot", - "replyWithChatGPT" + "replyWithChatGPT", + "getAgoraVoicingList" ]; const baseQuery = fetchBaseQuery({ diff --git a/src/app/services/server.ts b/src/app/services/server.ts index aafbd33e..3f126d0f 100644 --- a/src/app/services/server.ts +++ b/src/app/services/server.ts @@ -17,10 +17,12 @@ import { LicenseResponse, RenewLicense, RenewLicenseResponse, - AgoraTokenResponse + AgoraTokenResponse, + AgoraVoicingListResponse } from "../../types/server"; import { Channel } from "../../types/channel"; import { ContentTypeKey } from "../../types/message"; +import { updateVoiceInfo } from "../slices/voice"; const defaultExpireDuration = 2 * 24 * 60 * 60; @@ -115,6 +117,31 @@ export const serverApi = createApi({ url: `group/${id}/agora_token`, }) }), + getAgoraVoicingList: builder.query({ + query: ({ appid, key, secret }) => ({ + headers: { + Authorization: `Basic ${btoa(`${key}:${secret}`)}` + }, + url: `https://api.agora.io/dev/v1/channel/${appid}`, + }), + async onQueryStarted(data, { dispatch, queryFulfilled }) { + try { + const { data: resp } = await queryFulfilled; + const { success } = resp; + if (success && resp.data.channels.length > 0) { + const [channel] = resp.data.channels; + const [id] = channel.channel_name.split(":").slice(-1); + // todo + dispatch(updateVoiceInfo({ + id: +id, + context: "channel" + })); + } + } catch { + console.error("get voice list error"); + } + } + }), getSMTPConfig: builder.query({ query: () => ({ url: `admin/smtp/config` }) }), @@ -327,5 +354,7 @@ export const { useSendMessageByBotMutation, useUpdateFrontendUrlMutation, useGetFrontendUrlQuery, - useLazyGetAgoraTokenQuery + useLazyGetAgoraTokenQuery, + useGetAgoraConfigQuery, + useGetAgoraVoicingListQuery } = serverApi; diff --git a/src/app/slices/auth.data.ts b/src/app/slices/auth.data.ts index 34592bc2..52c4331c 100644 --- a/src/app/slices/auth.data.ts +++ b/src/app/slices/auth.data.ts @@ -85,9 +85,6 @@ const authDataSlice = createSlice({ updateRoleChanged(state, action: PayloadAction) { state.roleChanged = action.payload; }, - updateVoiceStatus(state, action: PayloadAction) { - state.voice = action.payload; - }, resetAuthData() { // remove local data localStorage.removeItem(KEY_EXPIRE); @@ -134,5 +131,5 @@ const authDataSlice = createSlice({ // } }); -export const { updateInitialized, updateLoginUser, setAuthData, resetAuthData, updateToken, updateRoleChanged, updateVoiceStatus } = authDataSlice.actions; +export const { updateInitialized, updateLoginUser, setAuthData, resetAuthData, updateToken, updateRoleChanged } = authDataSlice.actions; export default authDataSlice.reducer; diff --git a/src/app/slices/voice.ts b/src/app/slices/voice.ts new file mode 100644 index 00000000..4205ac62 --- /dev/null +++ b/src/app/slices/voice.ts @@ -0,0 +1,63 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +export type VoiceInfo = { + context: "channel" | "dm" + id: number | null, + members: number[] +} +interface State { + joined: boolean, + voicing: VoiceInfo +} +const initialInfo = { + context: "channel" as const, + id: null, + members: [] +}; +const initialState: State = { + joined: false, + voicing: initialInfo +}; + + + +const voiceSlice = createSlice({ + name: "voice", + initialState, + reducers: { + updateJoinStatus(state, { payload }: PayloadAction) { + state.joined = payload; + }, + updateVoiceInfo(state, { payload }: PayloadAction) { + if (!payload) { + // reset + state.voicing = initialInfo; + } else { + if (state.voicing) { + state.voicing = { ...state.voicing, ...payload }; + } else { + state.voicing = payload; + } + + } + }, + addVoiceMember(state, { payload }: PayloadAction) { + if (state.voicing) { + if (!state.voicing.members.includes(payload)) { + state.voicing.members.push(payload); + } + } + }, + removeVoiceMember(state, { payload }: PayloadAction) { + if (state.voicing) { + const idx = state.voicing.members.findIndex((uid) => uid == payload); + if (idx > -1) { + state.voicing.members.splice(idx, 1); + } + } + } + }, +}); + +export const { updateJoinStatus, updateVoiceInfo, addVoiceMember, removeVoiceMember } = voiceSlice.actions; +export default voiceSlice.reducer; diff --git a/src/app/store.ts b/src/app/store.ts index 8f591f75..0f762d85 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -3,6 +3,7 @@ import { setupListeners } from "@reduxjs/toolkit/query"; import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; import listenerMiddleware from "./listener.middleware"; import authDataReducer from "./slices/auth.data"; +import voiceReducer from "./slices/voice"; import footprintReducer from "./slices/footprint"; import serverReducer from "./slices/server"; import uiReducer from "./slices/ui"; @@ -23,6 +24,7 @@ import { serverApi } from "./services/server"; const reducer = combineReducers({ authData: authDataReducer, + voice: voiceReducer, ui: uiReducer, footprint: footprintReducer, server: serverReducer, diff --git a/src/assets/icons/signal.svg b/src/assets/icons/signal.svg new file mode 100644 index 00000000..359015e2 --- /dev/null +++ b/src/assets/icons/signal.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/icons/voicing.svg b/src/assets/icons/voicing.svg new file mode 100644 index 00000000..7e5dae17 --- /dev/null +++ b/src/assets/icons/voicing.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/common/component/Voice.tsx b/src/common/component/Voice.tsx new file mode 100644 index 00000000..ae8a9f2d --- /dev/null +++ b/src/common/component/Voice.tsx @@ -0,0 +1,122 @@ +import AgoraRTC from 'agora-rtc-sdk-ng'; +import { useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { useGetAgoraConfigQuery, useGetAgoraVoicingListQuery, useLazyGetAgoraTokenQuery } from '../../app/services/server'; +import { addVoiceMember, removeVoiceMember, updateJoinStatus, updateVoiceInfo } from '../../app/slices/voice'; +import { useAppSelector } from '../../app/store'; + +// type Props = {} +window.VOICE_TRACK_MAP = window.VOICE_TRACK_MAP ?? {}; +const Voice = () => { + const isAdmin = useAppSelector(store => store.authData.user?.is_admin); + const { data: agoraConfig } = useGetAgoraConfigQuery(undefined, { + skip: !isAdmin + }); + useGetAgoraVoicingListQuery({ + appid: agoraConfig?.app_id ?? "", + key: agoraConfig?.rtm_key ?? "", + secret: agoraConfig?.rtm_secret ?? "", + + }, { + skip: !isAdmin || !agoraConfig, + pollingInterval: 5000 + }); + const dispatch = useDispatch(); + + useEffect(() => { + const initializeAgoraClient = async () => { + // Create an instance of the Agora Engine + const agoraEngine = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" }); + // 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); + dispatch(addVoiceMember(+user.uid)); + // console.log("subscribe success"); + // Subscribe and play the remote audio track. + if (mediaType == "audio") { + // Play the remote audio track. + user.audioTrack?.play(); + window.VOICE_TRACK_MAP[+user.uid] = user.audioTrack; + // console.log("Remote user connected: " + user.uid); + } + // Listen for the "user-unpublished" event. + agoraEngine.on("user-unpublished", user => { + dispatch(removeVoiceMember(+user.uid as number)); + console.log(user.uid + "has left the channel"); + // console.log("Remote user has left the channel"); + }); + }); + window.VOICE_CLIENT = agoraEngine; + }; + if (!window.VOICE_CLIENT) { + initializeAgoraClient(); + + } + + return () => { + if (window.VOICE_CLIENT) { + window.VOICE_CLIENT.leave(); + } + // window.VOICE_CLIENT=null + }; + }, []); + + + return null; +}; +type VoiceProps = { + id: number, + context?: "channel" | "dm" +} +const useVoice = ({ id, context = "channel" }: VoiceProps) => { + const dispatch = useDispatch(); + const { voiceInfo, joined } = useAppSelector(store => { + return { + voiceInfo: store.voice.voicing, + joined: store.voice.joined + }; + }); + const [generateToken] = useLazyGetAgoraTokenQuery(); + const [joining, setJoining] = useState(false); + const joinVoice = async () => { + setJoining(true); + const { isError, data } = await generateToken(id); + if (!isError && data) { + const { channel_name, app_id, agora_token, uid } = data; + if (window.VOICE_CLIENT) { + await window.VOICE_CLIENT.join(app_id, channel_name, agora_token, uid); + console.table(data); + // Create a local audio track from the microphone audio. + const localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack(); + // Publish the local audio track in the channel. + await window.VOICE_CLIENT.publish(localAudioTrack); + console.log("Publish success!"); + dispatch(updateVoiceInfo({ + id, + context, + members: [uid] + })); + dispatch(updateJoinStatus(true)); + } + } + setJoining(false); + }; + const leave = async () => { + if (window.VOICE_CLIENT) { + await window.VOICE_CLIENT.leave(); + dispatch(updateJoinStatus(false)); + // window.VOICE_TRACK_MAP + } + }; + return { + leave, + // canVoice, + voiceInfo, + joining, + joined, + joinVoice + }; +}; +export { useVoice }; +export default Voice; \ No newline at end of file diff --git a/src/routes/chat/RTCWidget.tsx b/src/routes/chat/RTCWidget.tsx index a256d605..c100769a 100644 --- a/src/routes/chat/RTCWidget.tsx +++ b/src/routes/chat/RTCWidget.tsx @@ -3,20 +3,33 @@ import { useAppSelector } from '../../app/store'; import User from '../../common/component/User'; import IconHeadphone from '../../assets/icons/headphone.svg'; import IconMic from '../../assets/icons/mic.on.svg'; +import IconSoundOn from '../../assets/icons/sound.on.svg'; +import IconSignal from '../../assets/icons/signal.svg'; +import { useVoice } from '../../common/component/Voice'; -type Props = {} +type Props = { + id: number, + context?: "channel" | "dm" +} -const RTCWidget = (props: Props) => { - const { loginUser, voicing } = useAppSelector(store => { return { loginUser: store.authData.user, voicing: store.authData.voice }; }); +const RTCWidget = ({ id, context = "channel" }: Props) => { + const { leave } = useVoice({ context, id }); + const { loginUser, joined } = useAppSelector(store => { return { loginUser: store.authData.user, joined: store.voice.joined }; }); if (!loginUser) return null; - return ( -
- {voicing &&
-
- Voice Connected +
+ {joined && +
+
+ +
+ Voice Connected + Channel / name +
+
+
-
} + }
@@ -26,7 +39,7 @@ const RTCWidget = (props: Props) => {
- +
diff --git a/src/routes/chat/SessionList/Session.tsx b/src/routes/chat/SessionList/Session.tsx index e44b293f..a3d00951 100644 --- a/src/routes/chat/SessionList/Session.tsx +++ b/src/routes/chat/SessionList/Session.tsx @@ -9,6 +9,7 @@ import getUnreadCount, { renderPreviewMessage } from "../utils"; import User from "../../../common/component/User"; import Avatar from "../../../common/component/Avatar"; import IconLock from "../../../assets/icons/lock.svg"; +import IconVoicing from "../../../assets/icons/voicing.svg"; import useContextMenu from "../../../common/hook/useContextMenu"; import useUploadFile from "../../../common/hook/useUploadFile"; import { useAppSelector } from "../../../app/store"; @@ -61,9 +62,10 @@ const Session: FC = ({ mid: number; is_public: boolean; }>(); - const { messageData, userData, channelData, readIndex, loginUid, mids, muted } = useAppSelector( + const { messageData, userData, channelData, readIndex, loginUid, mids, muted, voiceInfo } = useAppSelector( (store) => { return { + voiceInfo: store.voice.voicing, mids: type == "user" ? store.userMessage.byId[id] : store.channelMessage[id], loginUid: store.authData.user?.uid || 0, readIndex: @@ -115,7 +117,7 @@ const Session: FC = ({ to={navPath} onContextMenu={handleContextMenuEvent} > -
+
{type == "user" ? ( ) : ( @@ -128,6 +130,7 @@ const Session: FC = ({ src={icon} /> )} + {type == "channel" && voiceInfo.id == id && }
diff --git a/src/routes/chat/VoiceChat/Dashboard.tsx b/src/routes/chat/VoiceChat/Dashboard.tsx index 25e5cc90..f4ef0f0e 100644 --- a/src/routes/chat/VoiceChat/Dashboard.tsx +++ b/src/routes/chat/VoiceChat/Dashboard.tsx @@ -1,76 +1,20 @@ -import { useEffect } from 'react'; -import AgoraRTC from "agora-rtc-sdk-ng"; -import useConfig from '../../../common/hook/useConfig'; -import { useDispatch } from 'react-redux'; -import { updateVoiceStatus } from '../../../app/slices/auth.data'; -import { useLazyGetAgoraTokenQuery } from '../../../app/services/server'; +// import { useEffect } from 'react'; +// import { useDispatch } from 'react-redux'; import VoiceManagement from './VoiceManagement'; import JoinVoice from './JoinVoice'; +import { useVoice } from '../../../common/component/Voice'; type Props = { - channel: string, + context?: "channel" | "dm", id: number, - uid: number, - voicing?: boolean; } -const Dashboard = ({ id, voicing, uid, channel }: Props) => { - const dispatch = useDispatch(); - const [generateToken] = useLazyGetAgoraTokenQuery(); +const Dashboard = ({ context = "channel", id }: Props) => { + const { joinVoice, joined, joining, voiceInfo } = useVoice({ id, context }); + // const dispatch = useDispatch(); - const { agoraConfig } = useConfig("agora"); - useEffect(() => { - console.log("agora config", agoraConfig); - const startConnect = async () => { - // Create an instance of the Agora Engine - const agoraEngine = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" }); - // 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("subscribe success"); - - // Subscribe and play the remote audio track. - if (mediaType == "audio") { - // channelParameters.remoteUid=user.uid; - // // Get the RemoteAudioTrack object from the AgoraRTCRemoteUser object. - // channelParameters.remoteAudioTrack = user.audioTrack; - // Play the remote audio track. - user.audioTrack?.play(); - console.log("Remote user connected: " + user.uid); - - } - - // Listen for the "user-unpublished" event. - agoraEngine.on("user-unpublished", user => { - console.log(user.uid + "has left the channel"); - console.log("Remote user has left the channel"); - }); - }); - // Join a channel. - console.log("join data ", agoraConfig); - const { isError, data } = await generateToken(id); - if (!isError && data) { - const { channel_name, app_id, agora_token, uid } = data; - await agoraEngine.join(app_id, channel_name, agora_token, uid); - console.table(data); - // Create a local audio track from the microphone audio. - const localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack(); - // Publish the local audio track in the channel. - await agoraEngine.publish(localAudioTrack); - console.log("Publish success!"); - dispatch(updateVoiceStatus(true)); - } - - }; - - if (!voicing && agoraConfig) { - startConnect(); - } - }, [voicing, agoraConfig, uid, channel]); - - return voicing ? : ; + return
{joined ? : }
; }; export default Dashboard; \ No newline at end of file diff --git a/src/routes/chat/VoiceChat/JoinVoice.tsx b/src/routes/chat/VoiceChat/JoinVoice.tsx index 8083d21d..be65d5c7 100644 --- a/src/routes/chat/VoiceChat/JoinVoice.tsx +++ b/src/routes/chat/VoiceChat/JoinVoice.tsx @@ -1,10 +1,19 @@ import React from 'react'; +import StyledButton from '../../../common/component/styled/Button'; -type Props = {} +type Props = { + join: () => void, + joining: boolean +} -const JoinVoice = (props: Props) => { +const JoinVoice = ({ joining, join }: Props) => { + if (joining) return
+ Joining +
; return ( -
JoinVoice
+
+ Join +
); }; diff --git a/src/routes/chat/VoiceChat/VoiceManagement.tsx b/src/routes/chat/VoiceChat/VoiceManagement.tsx index dbe8c24c..686c2624 100644 --- a/src/routes/chat/VoiceChat/VoiceManagement.tsx +++ b/src/routes/chat/VoiceChat/VoiceManagement.tsx @@ -1,10 +1,28 @@ import React from 'react'; +import { VoiceInfo } from '../../../app/slices/voice'; +import { useAppSelector } from '../../../app/store'; +import User from '../../../common/component/User'; -type Props = {} +type Props = { + info: VoiceInfo | null +} -const VoiceManagement = (props: Props) => { +const VoiceManagement = ({ info }: Props) => { + const userData = useAppSelector(store => store.users.byId); + if (!info) return null; + const { context, id, members } = info; return ( -
VoiceManagement
+
+
    + {members.map((uid) => { + return
  • + + {/* {userData[uid]?.name} */} +
  • ; + })} + +
+
); }; diff --git a/src/routes/chat/VoiceChat/index.tsx b/src/routes/chat/VoiceChat/index.tsx index a6d626b3..05b3bdbc 100644 --- a/src/routes/chat/VoiceChat/index.tsx +++ b/src/routes/chat/VoiceChat/index.tsx @@ -1,5 +1,6 @@ // import React from 'react'; -import Tippy from '@tippyjs/react'; +// import Tippy from '@tippyjs/react'; +import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAppSelector } from '../../../app/store'; import IconHeadphone from '../../../assets/icons/headphone.svg'; @@ -7,29 +8,24 @@ import Tooltip from '../../../common/component/Tooltip'; import Dashboard from './Dashboard'; type Props = { + context?: "channel" | "dm" id: number, - channel: string } -const VoiceChat = ({ channel, id }: Props) => { - const { loginUser, voice } = useAppSelector(store => { return { loginUser: store.authData.user, voice: store.authData.voice }; }); +const VoiceChat = ({ id, context = "channel" }: Props) => { + const [dashboardVisible, setDashboardVisible] = useState(false); + const { loginUser } = useAppSelector(store => { return { loginUser: store.authData.user }; }); const { t } = useTranslation("chat"); - const toolClass = `relative cursor-pointer`; + const toggleDashboard = () => { + setDashboardVisible(prev => !prev); + }; if (!loginUser) return null; return ( - - } - > -
  • - -
  • -
    + +
  • + + {dashboardVisible && } +
  • ); }; diff --git a/src/routes/chat/VoiceChat/useVoice.tsx b/src/routes/chat/VoiceChat/useVoice.tsx deleted file mode 100644 index c69a31d0..00000000 --- a/src/routes/chat/VoiceChat/useVoice.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useState } from "react"; -import { useAppSelector } from "../../../app/store"; - - - -const useVoice = (uid: number) => { - const [engine, setEngine] = useState(window.VoiceEngine); - const { voicing } = useAppSelector(store => { - - return { - voicing: store.users.byId[uid].voice ?? false - }; - }); - - - return { - voicing, - - }; - -}; - -export default useVoice; \ No newline at end of file diff --git a/src/routes/chat/index.tsx b/src/routes/chat/index.tsx index dc900738..e2db40f6 100644 --- a/src/routes/chat/index.tsx +++ b/src/routes/chat/index.tsx @@ -50,6 +50,8 @@ function ChatPage() { // console.log("temp uid", tmpUid); const placeholderVisible = channel_id == 0 && user_id == 0; const isMainPath = isHomePath || isChatHomePath; + const context = channel_id !== 0 ? "channel" : "dm"; + const contextId = (channel_id || user_id) ?? 0; return ( {channelModalVisible && ( @@ -66,7 +68,7 @@ function ChatPage() { {isGuest ? : } + : }
    {placeholderVisible && (isGuest ? : )} diff --git a/src/routes/home/index.tsx b/src/routes/home/index.tsx index 4d0adeeb..ef21ff6a 100644 --- a/src/routes/home/index.tsx +++ b/src/routes/home/index.tsx @@ -19,6 +19,7 @@ import MobileNavs from "./MobileNavs"; import { updateRememberedNavs } from "../../app/slices/ui"; import UnreadTabTip from "../../common/component/UnreadTabTip"; import ReLoginModal from "../../common/component/ReLoginModal"; +import Voice from "../../common/component/Voice"; function HomePage() { @@ -67,6 +68,7 @@ function HomePage() { <> {roleChanged && } {!guest && } + {!guest && } {!guest && }
    diff --git a/src/types/global.d.ts b/src/types/global.d.ts index 918a9434..101f0482 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -1,3 +1,5 @@ +import { IAgoraRTCClient, IRemoteAudioTrack } from "agora-rtc-sdk-ng"; + interface BeforeInstallPromptEvent extends Event { /** * Returns an array of DOMString items containing the platforms on which the event was dispatched. @@ -29,6 +31,10 @@ export declare global { __WB_MANIFEST: Array; skipWaiting: () => void; CACHE: { [key: string]: typeof localforage | undefined }; + VOICE_CLIENT?: IAgoraRTCClient; + VOICE_TRACK_MAP: { + [key: number]: IRemoteAudioTrack | undefined + } } interface WindowEventMap { beforeinstallprompt: BeforeInstallPromptEvent; diff --git a/src/types/server.ts b/src/types/server.ts index 08c71372..37fbe6bf 100644 --- a/src/types/server.ts +++ b/src/types/server.ts @@ -27,6 +27,13 @@ export interface AgoraConfig { rtm_key: string; rtm_secret: string; } +export interface AgoraVoicingListResponse { + success: boolean, + data: { + channels: { channel_name: string, user_count: number }[], + total_size: number + } +} export interface AgoraTokenResponse { agora_token: string, uid: number,