From c0c1dc8bf5c07b3ddd5d875015f3e5cbe0417e80 Mon Sep 17 00:00:00 2001 From: Tristan Yang Date: Wed, 17 May 2023 17:32:33 +0800 Subject: [PATCH] refactor: voice operations --- src/app/services/server.ts | 11 +- src/app/slices/footprint.ts | 2 +- src/app/slices/voice.ts | 66 +++++++-- src/components/Voice/Operations.tsx | 127 ++++++++++++++++++ src/components/Voice/index.tsx | 31 +++-- src/components/Voice/useVoice.ts | 40 +++++- src/routes/chat/Layout/DMVoicing.tsx | 76 +++++------ src/routes/chat/RTCWidget.tsx | 10 +- src/routes/chat/VoiceChat/Dashboard.tsx | 4 +- src/routes/chat/VoiceChat/VoiceManagement.tsx | 55 +------- src/routes/chat/VoiceChat/index.tsx | 2 +- src/routes/chat/VoiceFullscreen.tsx | 44 +----- src/routes/chat/index.tsx | 7 +- 13 files changed, 312 insertions(+), 163 deletions(-) create mode 100644 src/components/Voice/Operations.tsx diff --git a/src/app/services/server.ts b/src/app/services/server.ts index ad30bee1..f3ba57a9 100644 --- a/src/app/services/server.ts +++ b/src/app/services/server.ts @@ -24,7 +24,7 @@ import { } from "@/types/server"; import { Channel } from "@/types/channel"; import { ContentTypeKey } from "@/types/message"; -import { upsertVoiceList } from "../slices/voice"; +import { updateCallInfo, upsertVoiceList } from "../slices/voice"; const defaultExpireDuration = 2 * 24 * 60 * 60; @@ -119,8 +119,9 @@ export const serverApi = createApi({ }), getAgoraChannels: builder.query({ query: (param = { page_no: 0, page_size: 100 }) => ({ url: `/admin/agora/channel/${param.page_no}/${param.page_size}` }), - async onQueryStarted(data, { dispatch, queryFulfilled }) { + async onQueryStarted(data, { dispatch, queryFulfilled, getState }) { try { + const { voice: { callingFrom }, authData } = getState() as RootState; const { data: resp } = await queryFulfilled; const { success } = resp; if (success) { @@ -136,6 +137,12 @@ export const serverApi = createApi({ }; }); dispatch(upsertVoiceList(arr)); + const hasMyself = arr.some(data => data.context === "dm" && data.id == authData?.user?.uid); + const sendByMe = callingFrom && callingFrom === authData?.user?.uid; + // reset dm call setting + if (callingFrom && !sendByMe && !hasMyself) { + dispatch(updateCallInfo({ from: 0, to: 0, calling: false })); + } } } catch { console.error("get voice list error"); diff --git a/src/app/slices/footprint.ts b/src/app/slices/footprint.ts index 6012afdb..f0aa9cf3 100644 --- a/src/app/slices/footprint.ts +++ b/src/app/slices/footprint.ts @@ -6,7 +6,7 @@ import { resetAuthData } from "./auth.data"; import { ChatContext } from "@/types/common"; type ChannelAside = "members" | "voice" | "voice_fullscreen" | null; -type DMAside = "voice" | null; +type DMAside = "voice" | "voice_fullscreen" | null; export interface State { og: { [url: string]: OG } usersVersion: number; diff --git a/src/app/slices/voice.ts b/src/app/slices/voice.ts index b932c118..6a261ca7 100644 --- a/src/app/slices/voice.ts +++ b/src/app/slices/voice.ts @@ -35,19 +35,18 @@ export type VoiceInfo = { channelName: string, memberCount: number } & VoiceBasicInfo +export type DeviceInfo = Pick; interface State { callingFrom: number, callingTo: number, calling: boolean, voicing: VoicingInfo | null, voicingMembers: VoicingMembers, - list: VoiceInfo[] + list: VoiceInfo[], + devices: DeviceInfo[], + audioInputDeviceId: string, + videoInputDeviceId: string, } -// const initialInfo = { -// context: "channel" as const, -// id: 0, -// members: [] -// }; const initialState: State = { calling: false, callingFrom: 0, @@ -57,11 +56,11 @@ const initialState: State = { ids: [], byId: {} }, - list: [] + list: [], + devices: [], + audioInputDeviceId: "", + videoInputDeviceId: "" }; - - - const voiceSlice = createSlice({ name: "voice", initialState, @@ -74,9 +73,30 @@ const voiceSlice = createSlice({ state.calling = calling; } }, + updateDevices(state, { payload }: PayloadAction) { + state.devices = payload; + if (payload.length > 0) { + const audioInputDevice = payload.find(v => v.kind == "audioinput" && v.deviceId == "default"); + const videoInputDevice = payload.find(v => v.kind == "videoinput"); + if (audioInputDevice) { + state.audioInputDeviceId = audioInputDevice.deviceId; + } + if (videoInputDevice) { + state.videoInputDeviceId = videoInputDevice.deviceId; + } + } + }, updateCalling(state, { payload }: PayloadAction) { state.calling = payload; }, + updateSelectDeviceId(state, { payload }: PayloadAction<{ type: "video" | "audio", value: string }>) { + const { type, value } = payload; + if (type == "video") { + state.videoInputDeviceId = value; + } else { + state.audioInputDeviceId = value; + } + }, updateVoicingInfo(state, { payload }: PayloadAction) { if (payload) { state.voicing = { ...(state.voicing ?? {}), ...payload }; @@ -84,7 +104,7 @@ const voiceSlice = createSlice({ const loginUid = localStorage.getItem(KEY_UID) ?? 0; const idx = state.voicingMembers.ids.findIndex((uid) => uid == loginUid); if (idx > -1) { - state.voicingMembers.byId[+loginUid] = payload; + // state.voicingMembers.byId[+loginUid] = payload; Object.keys(payload).forEach((key) => { switch (key) { case "video": @@ -92,8 +112,17 @@ const voiceSlice = createSlice({ break; case "muted": state.voicingMembers.byId[+loginUid].muted = payload.muted; + if (!payload.muted) { + state.voicingMembers.byId[+loginUid].deafen = false; + if (state.voicing) { + state.voicing.deafen = false; + } + } break; case "deafen": + if (state.voicing) { + state.voicing.muted = payload.deafen; + } state.voicingMembers.byId[+loginUid].deafen = payload.deafen; state.voicingMembers.byId[+loginUid].muted = payload.deafen; break; @@ -199,5 +228,18 @@ const voiceSlice = createSlice({ } }); -export const { updateCalling, updateCallInfo, updatePin, updateConnectionState, addVoiceMember, removeVoiceMember, upsertVoiceList, updateVoicingInfo, updateVoicingNetworkQuality, updateVoicingMember } = voiceSlice.actions; +export const { + updateSelectDeviceId, + updateDevices, + updateCalling, + updateCallInfo, + updatePin, + updateConnectionState, + addVoiceMember, + removeVoiceMember, + upsertVoiceList, + updateVoicingInfo, + updateVoicingNetworkQuality, + updateVoicingMember +} = voiceSlice.actions; export default voiceSlice.reducer; diff --git a/src/components/Voice/Operations.tsx b/src/components/Voice/Operations.tsx new file mode 100644 index 00000000..e35a2db9 --- /dev/null +++ b/src/components/Voice/Operations.tsx @@ -0,0 +1,127 @@ +import { MouseEvent, SetStateAction, useState, Dispatch } from 'react'; +import { useTranslation } from 'react-i18next'; +import clsx from 'clsx'; +import { useDispatch } from 'react-redux'; +import Tippy from '@tippyjs/react'; + +import Tooltip from '../Tooltip'; +import IconMicOff from '@/assets/icons/mic.off.svg'; +import IconMic from '@/assets/icons/mic.on.svg'; +import IconCameraOff from '@/assets/icons/camera.off.svg'; +import IconArrow from '@/assets/icons/arrow.down.mini.svg'; +import IconCamera from '@/assets/icons/camera.svg'; +import IconFullscreen from '@/assets/icons/fullscreen.svg'; +import IconScreen from '@/assets/icons/share.screen.svg'; +import IconCheck from '@/assets/icons/check.sign.svg'; +import IconCallOff from '@/assets/icons/call.off.svg'; + +import { ChatContext } from '@/types/common'; +import useVoice from './useVoice'; +// import { updateChannelVisibleAside, updateDMVisibleAside } from '@/app/slices/footprint'; +import { DeviceInfo, updateSelectDeviceId } from '@/app/slices/voice'; + +type DeviceType = "audio" | "video"; +// https://docportal.shengwang.cn/cn/video-call-4.x/test_switch_device_web_ng?platform=Web +const DeviceList = ({ type, visible, setVisible, devices, selected }: { + type: DeviceType, + visible: VisibleType, + setVisible: Dispatch>, + devices: DeviceInfo[], + selected: string +}) => { + const dispatch = useDispatch(); + // const { t } = useTranslation("chat"); + const toggleVisible = (evt: MouseEvent) => { + evt.stopPropagation(); + setVisible((prev: VisibleType) => prev == type ? "" : type); + }; + const handleSelect = (evt: MouseEvent) => { + evt.stopPropagation(); + const { deviceId = "" } = evt.currentTarget.dataset; + if (selected == deviceId) return; + dispatch(updateSelectDeviceId({ type, value: deviceId })); + }; + return setVisible("")} + interactive + popperOptions={{ strategy: "fixed" }} + visible={visible == type} + placement="top-start" + content={
+
    + {devices.map(({ deviceId, kind, groupId, label }) => { + return ( +
  • + {label} + {selected == deviceId && } +
  • + ); + })} +
+
} + > +
+ +
+
; +}; +type VisibleType = "audio" | "video" | ""; +type Props = { + mode?: "channel" | "dm" | "fullscreen", + id: number, + context: ChatContext +} + +const Operations = ({ id, context, mode = "channel" }: Props) => { + const [panelVisible, setPanelVisible] = useState(""); + const { enterFullscreen, voicingInfo, leave, setMute, closeCamera, openCamera, startShareScreen, stopShareScreen, audioInputDevices, audioOutputDevices, videoInputDevices, videoInputDeviceId, audioInputDeviceId } = useVoice({ id, context }); + const { t } = useTranslation("chat"); + if (!voicingInfo) return null; + const { muted, video, shareScreen } = voicingInfo; + const baseButtonClass = clsx("flex-center py-2 px-3 rounded bg-gray-100 dark:bg-gray-900 relative"); + const baseIconClass = clsx("w-[25px] h-6 m-auto fill-gray-700 dark:fill-gray-300"); + return <> + + + + + + + + + + {mode !== "fullscreen" && + + } + {mode !== "dm" && + + } + ; +}; + +export default Operations; \ No newline at end of file diff --git a/src/components/Voice/index.tsx b/src/components/Voice/index.tsx index 6eccc686..21fdd15d 100644 --- a/src/components/Voice/index.tsx +++ b/src/components/Voice/index.tsx @@ -2,7 +2,7 @@ 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, updateCallInfo, updateConnectionState, updateVoicingMember, updateVoicingNetworkQuality } from '../../app/slices/voice'; +import { addVoiceMember, removeVoiceMember, updateCallInfo, updateConnectionState, updateDevices, updateVoicingMember, updateVoicingNetworkQuality } from '../../app/slices/voice'; import { useAppSelector } from '../../app/store'; import { playAgoraVideo } from '../../utils'; import useVoice from './useVoice'; @@ -40,12 +40,12 @@ const Voice = () => { // 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") { + if (mediaType == "audio" && user.hasAudio) { // 播放远端音频 user.audioTrack?.play(); window.VOICE_TRACK_MAP[+user.uid] = user.audioTrack; } - if (mediaType == "video") { + if (mediaType == "video" && user.hasVideo) { // const label = user.videoTrack?.getMediaStreamTrack()?.label; // console.log("labell", label); @@ -56,6 +56,7 @@ const Voice = () => { // // // 远端用户停止共享屏幕 // dispatch(updateVoicingMember({ uid: +user.uid, info: { shareScreen: true } })); // } + dispatch(updateVoicingMember({ uid: +user.uid, info: { video: true } })); window.VIDEO_TRACK_MAP[+user.uid] = user.videoTrack; playAgoraVideo(+user.uid); } @@ -84,10 +85,10 @@ const Voice = () => { // clear tracks window.VOICE_TRACK_MAP[+user.uid] = null; window.VIDEO_TRACK_MAP[+user.uid] = null; - if (voicingInfo && voicingInfo.context == "dm") { - // 有人离开,就断开 - dispatch(updateCallInfo({ from: 0 })); - } + // if (voicingInfo && voicingInfo.context == "dm") { + // // 有人离开,就断开 + // dispatch(updateCallInfo({ from: 0 })); + // } } break; default: @@ -139,9 +140,21 @@ const Voice = () => { }); // 有新用户加入 agoraEngine.on("user-joined", async (user) => { - console.log(user.uid, agoraEngine.channelName, " has joined the channel"); + console.log(user.uid, " has joined the channel", user); dispatch(addVoiceMember(+user.uid)); }); + AgoraRTC.onMicrophoneChanged = (info) => { + console.log("onMicrophoneChanged", info); + }; + AgoraRTC.getDevices().then((devices) => { + console.log("devices", devices); + dispatch(updateDevices(devices.map((d) => { + const { deviceId, groupId, label, kind } = d; + return { deviceId, groupId, label, kind }; + }))); + }).catch((err) => { + console.log("err", err); + }); window.VOICE_CLIENT = agoraEngine; }; const handlePageUnload = (evt: BeforeUnloadEvent) => { @@ -172,7 +185,7 @@ const Voice = () => { // 呼叫的人在频道里 getUsersByChannel(channelName).then(resp => { const [uid] = resp.data ?? []; - if (uid) { + if (uid && uid != loginUid) { dispatch(updateCallInfo({ from: uid, to: id })); } }); diff --git a/src/components/Voice/useVoice.ts b/src/components/Voice/useVoice.ts index fea7b519..bc209172 100644 --- a/src/components/Voice/useVoice.ts +++ b/src/components/Voice/useVoice.ts @@ -1,8 +1,9 @@ import AgoraRTC, { ICameraVideoTrack, IMicrophoneAudioTrack } from 'agora-rtc-sdk-ng'; +import { useDispatch } from 'react-redux'; import { ShareScreenTrack } from '../../types/global'; import AudioJoin from '@/assets/join.wav'; -import { useDispatch } from 'react-redux'; + import { useGenerateAgoraTokenMutation } from '../../app/services/server'; import { updateChannelVisibleAside, updateDMVisibleAside } from '../../app/slices/footprint'; @@ -19,10 +20,15 @@ type VoiceProps = { const audioJoin = new Audio(AudioJoin); const useVoice = ({ id, context = "channel" }: VoiceProps) => { const dispatch = useDispatch(); - const { voicingInfo, loginUid } = useAppSelector(store => { + const { voicingInfo, loginUid, audioInputDevices, audioOutputDevices, videoInputDevices, audioInputDeviceId, videoInputDeviceId } = useAppSelector(store => { return { loginUid: store.authData.user?.uid ?? 0, - voicingInfo: store.voice.voicing + voicingInfo: store.voice.voicing, + audioInputDevices: store.voice.devices.filter(d => d.kind == "audioinput") ?? [], + audioOutputDevices: store.voice.devices.filter(d => d.kind == "audiooutput") ?? [], + videoInputDevices: store.voice.devices.filter(d => d.kind == "videoinput") ?? [], + audioInputDeviceId: store.voice.audioInputDeviceId, + videoInputDeviceId: store.voice.videoInputDeviceId, }; }); const [generateToken] = useGenerateAgoraTokenMutation(); @@ -154,8 +160,10 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => { // 进入全屏并Pin自己 if (context == "channel") { dispatch(updateChannelVisibleAside({ id, aside: "voice_fullscreen" })); - dispatch(updatePin({ uid: loginUid, action: "pin" })); + } else { + dispatch(updateDMVisibleAside({ id, aside: "voice_fullscreen" })); } + dispatch(updatePin({ uid: loginUid, action: "pin" })); // 监听屏幕共享结束事件 if ("close" in localVideoTrack) { localVideoTrack.getMediaStreamTrack().onended = () => { @@ -232,6 +240,23 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => { } dispatch(updateVoicingInfo({ deafen, id, context })); }; + const enterFullscreen = (uid?: number) => { + if (context == "channel") { + dispatch(updateChannelVisibleAside({ id, aside: "voice_fullscreen" })); + } else { + dispatch(updateDMVisibleAside({ id, aside: "voice_fullscreen" })); + } + if (uid) { + dispatch(updatePin({ uid, action: "pin" })); + } + }; + const exitFullscreen = () => { + if (context == "channel") { + dispatch(updateChannelVisibleAside({ id, aside: "voice" })); + } else { + dispatch(updateDMVisibleAside({ id, aside: null })); + } + }; const joinedAtThisContext = voicingInfo ? (voicingInfo.id == id && voicingInfo.context == context) : false; return { setMute, @@ -247,6 +272,13 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => { closeCamera, startShareScreen, stopShareScreen, + enterFullscreen, + exitFullscreen, + audioInputDevices, + audioOutputDevices, + videoInputDevices, + audioInputDeviceId, + videoInputDeviceId }; }; diff --git a/src/routes/chat/Layout/DMVoicing.tsx b/src/routes/chat/Layout/DMVoicing.tsx index 02f7c146..5a67d519 100644 --- a/src/routes/chat/Layout/DMVoicing.tsx +++ b/src/routes/chat/Layout/DMVoicing.tsx @@ -1,6 +1,6 @@ // import React from 'react' import { useEffect } from "react"; -import { useTranslation } from "react-i18next"; +// import { useTranslation } from "react-i18next"; import { useDispatch } from "react-redux"; import clsx from "clsx"; @@ -9,33 +9,38 @@ import Tooltip from "@/components/Tooltip"; 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 { VoicingInfo, updateCallInfo } from "@/app/slices/voice"; +// 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 { updateCallInfo } from "@/app/slices/voice"; import { useVoice } from "@/components/Voice"; import { playAgoraVideo } from "@/utils"; import Avatar from "@/components/Avatar"; +import Operations from "@/components/Voice/Operations"; type VoicingMember = { id: number, + muted: boolean, video: boolean, speaking: boolean, name: string, avatar?: string } type BlockProps = { + onlyToSelf: boolean, sendByMe: boolean; connected: boolean; from: VoicingMember, to: VoicingMember } -const VoicingBlocks = ({ sendByMe, connected, from, to }: BlockProps) => { - const blocks = [from, to].map(({ id, speaking, name, avatar, video }, idx) => { - const showWaiting = idx == 1 && !connected && !sendByMe; +const VoicingBlocks = ({ onlyToSelf, sendByMe, connected, from, to }: BlockProps) => { + const blocks = [from, to].map(({ id, speaking, name, avatar, video, muted }, idx) => { + const showWaiting = idx == 1 && !connected && !sendByMe && !onlyToSelf; const showToWaiting = idx == 1 && !connected && sendByMe; - return
+ const isMyself = sendByMe ? idx == 0 : idx == 1; + const hiddenFrom = onlyToSelf && idx == 0; + return
{speaking &&
} {showToWaiting &&
} @@ -51,6 +56,12 @@ const VoicingBlocks = ({ sendByMe, connected, from, to }: BlockProps) => {
{/* camera video */}
+ {video && + {name} + } + {muted && !isMyself && + + }
; } ); @@ -61,9 +72,9 @@ type Props = { } const DMVoice = ({ uid }: Props) => { const dispatch = useDispatch(); - const { t } = useTranslation("chat"); + // const { t } = useTranslation("chat"); const { voice: { - callingFrom, callingTo, voicing: voicingInfo, voicingMembers + callingFrom, callingTo, voicingMembers }, userData, loginUser } = useAppSelector(store => { return { voice: store.voice, @@ -71,7 +82,7 @@ const DMVoice = ({ uid }: Props) => { loginUser: store.authData.user }; }); - const { leave, joinVoice, joining, setMute, closeCamera, openCamera, startShareScreen, stopShareScreen } = useVoice({ id: callingTo, context: "dm" }); + const { leave, joinVoice, joining } = useVoice({ id: callingTo, context: "dm" }); useEffect(() => { const ids = voicingMembers.ids; ids.forEach(id => { @@ -83,9 +94,10 @@ const DMVoice = ({ uid }: Props) => { const { name: fromUsername, avatar: fromAvatar } = userData[callingFrom]; const { name: toUsername, avatar: toAvatar, uid: toUid } = userData[callingTo]; const sendByMe = loginUser?.uid !== toUid; + const onlyToSelf = voicingMembers.ids.length == 1 && voicingMembers.ids[0] == callingTo; const handleCancel = () => { console.log('cancel'); - if (sendByMe || voicingMembers.ids.length == 2) { + if (sendByMe || voicingMembers.ids.length == 2 || onlyToSelf) { leave(); } dispatch(updateCallInfo({ from: 0, to: 0 })); @@ -94,21 +106,23 @@ const DMVoice = ({ uid }: Props) => { joinVoice(); }; const connected = voicingMembers.ids.length == 2; - const { muted, shareScreen, video } = voicingInfo ?? {} as VoicingInfo; - const { speakingVolume: toSpeakingVol = 0, video: toVideo = false, shareScreen: toScreen = false } = voicingMembers.byId[callingTo] ?? {}; + // const { muted, shareScreen, video } = voicingInfo ?? {} as VoicingInfo; + const { speakingVolume: toSpeakingVol = 0, muted: toMuted = false, video: toVideo = false, shareScreen: toScreen = false } = voicingMembers.byId[callingTo] ?? {}; const toSpeaking = toSpeakingVol > 50; - const { speakingVolume: fromSpeakingVol = 0, video: fromVideo = false, shareScreen: fromScreen = false } = voicingMembers.byId[callingFrom] ?? {}; + const { speakingVolume: fromSpeakingVol = 0, muted: fromMuted = false, video: fromVideo = false, shareScreen: fromScreen = false } = voicingMembers.byId[callingFrom] ?? {}; const fromSpeaking = fromSpeakingVol > 50; return (
-
- + { }} />
- {(sendByMe || connected) && - } - {!sendByMe && !connected && + {!sendByMe && !connected && !onlyToSelf && - } {connected && <> - - - - - - - - - + }
diff --git a/src/routes/chat/RTCWidget.tsx b/src/routes/chat/RTCWidget.tsx index ab699c65..7e16970e 100644 --- a/src/routes/chat/RTCWidget.tsx +++ b/src/routes/chat/RTCWidget.tsx @@ -8,7 +8,6 @@ import IconMicOff from '@/assets/icons/mic.off.svg'; import IconCallOff from '@/assets/icons/headphone.svg'; import IconSoundOn from '@/assets/icons/sound.on.svg'; import IconSoundOff from '@/assets/icons/sound.off.svg'; -// import IconCameraOff from '@/assets/icons/camera.off.svg'; import IconCamera from '@/assets/icons/camera.svg'; import IconScreen from '@/assets/icons/share.screen.svg'; import { useVoice } from '../../components/Voice'; @@ -24,8 +23,10 @@ type Props = { const RTCWidget = ({ id, context = "channel" }: Props) => { const { t } = useTranslation("chat"); const { leave, voicingInfo, setMute, setDeafen, joining = true, openCamera, closeCamera, startShareScreen, stopShareScreen } = useVoice({ context, id }); - const { loginUser, channelData, userData } = useAppSelector(store => { + const { callFrom, callTo, loginUser, channelData, userData } = useAppSelector(store => { return { + callFrom: store.voice.callingFrom, + callTo: store.voice.callingTo, userData: store.users.byId, channelData: store.channels.byId, loginUser: store.authData.user, @@ -35,6 +36,7 @@ const RTCWidget = ({ id, context = "channel" }: Props) => { // const name = voicingInfo.context == "channel" ? channelData[voicingInfo.id]?.name : userData[voicingInfo.id]?.name; // if (!name) return null; const isReConnecting = voicingInfo.connectionState == "RECONNECTING"; + const targetDMUsername = callFrom == loginUser.uid ? userData[callTo]?.name : userData[callFrom]?.name; return (
@@ -43,7 +45,9 @@ const RTCWidget = ({ id, context = "channel" }: Props) => {
{isReConnecting ? `Voice Reconnecting...` : `Voice Connected`} - {voicingInfo.context == "channel" ? "Channel" : "DM"} / {voicingInfo.context == "channel" ? channelData[voicingInfo.id]?.name : userData[voicingInfo.id]?.name} + + {voicingInfo.context == "channel" ? "Channel" : "DM"} / {voicingInfo.context == "channel" ? channelData[voicingInfo.id]?.name : targetDMUsername} +
diff --git a/src/routes/chat/VoiceChat/Dashboard.tsx b/src/routes/chat/VoiceChat/Dashboard.tsx index 1b5af52a..5effb496 100644 --- a/src/routes/chat/VoiceChat/Dashboard.tsx +++ b/src/routes/chat/VoiceChat/Dashboard.tsx @@ -11,9 +11,9 @@ type Props = { } const Dashboard = ({ context = "channel", id, visible }: Props) => { - const { voicingInfo, setMute, leave, closeCamera, openCamera, startShareScreen, stopShareScreen } = useVoice({ id, context }); + const { voicingInfo } = useVoice({ id, context }); return
- +
; }; diff --git a/src/routes/chat/VoiceChat/VoiceManagement.tsx b/src/routes/chat/VoiceChat/VoiceManagement.tsx index d8ec920e..f9c24403 100644 --- a/src/routes/chat/VoiceChat/VoiceManagement.tsx +++ b/src/routes/chat/VoiceChat/VoiceManagement.tsx @@ -1,41 +1,25 @@ import clsx from 'clsx'; import { useEffect } from 'react'; -import { useTranslation } from 'react-i18next'; import { VoicingInfo, updatePin } from '../../../app/slices/voice'; import { useAppSelector } from '../../../app/store'; import Avatar from '../../../components/Avatar'; -// import IconHeadphone from '@/assets/icons/sound.on.svg'; -// import IconHeadphoneOff from '@/assets/icons/sound.off.svg'; import IconMic from '@/assets/icons/mic.on.svg'; import IconMicOff from '@/assets/icons/mic.off.svg'; -import IconCameraOff from '@/assets/icons/camera.off.svg'; -import IconCamera from '@/assets/icons/camera.svg'; import IconFullscreen from '@/assets/icons/fullscreen.svg'; -import IconScreen from '@/assets/icons/share.screen.svg'; -import IconCallOff from '@/assets/icons/call.off.svg'; -import StyledButton from '../../../components/styled/Button'; -import Tooltip from '../../../components/Tooltip'; import { playAgoraVideo } from '@/utils'; import { ChatContext } from '../../../types/common'; -import { updateChannelVisibleAside } from '../../../app/slices/footprint'; +import Operations from '@/components/Voice/Operations'; +import { updateChannelVisibleAside } from '@/app/slices/footprint'; import { useDispatch } from 'react-redux'; -// import User from '../../../common/component/User'; type Props = { id: number, context: ChatContext, info: VoicingInfo | null, - setMute: (param: boolean) => void, - leave: () => void, - closeCamera: () => void, - openCamera: () => void, - startShareScreen: () => void, - stopShareScreen: () => void } -const VoiceManagement = ({ id, context, info, setMute, leave, closeCamera, openCamera, startShareScreen, stopShareScreen }: Props) => { - const { t } = useTranslation("chat"); +const VoiceManagement = ({ id, context, info, }: Props) => { const dispatch = useDispatch(); const { userData, voicingMembers } = useAppSelector(store => { return { @@ -58,7 +42,6 @@ const VoiceManagement = ({ id, context, info, setMute, leave, closeCamera, openC } }; if (!info) return null; - const { muted, video, shareScreen } = info; const nameClass = clsx(`text-sm text-gray-500 max-w-[120px] truncate font-semibold dark:text-white`); const members = voicingMembers.ids; const membersData = voicingMembers.byId; @@ -116,38 +99,10 @@ const VoiceManagement = ({ id, context, info, setMute, leave, closeCamera, openC
; })} - -
-
    - -
  • - {muted ? : } -
  • -
    - -
  • - {video ? : } -
  • -
    - -
  • - -
  • -
    - -
  • - -
  • -
    -
- - - - - +
+
-
); }; diff --git a/src/routes/chat/VoiceChat/index.tsx b/src/routes/chat/VoiceChat/index.tsx index 1349895a..230d21c2 100644 --- a/src/routes/chat/VoiceChat/index.tsx +++ b/src/routes/chat/VoiceChat/index.tsx @@ -50,7 +50,7 @@ const VoiceChat = ({ id, context = "channel" }: Props) => { dispatch(context == "channel" ? updateChannelVisibleAside(data) : updateDMVisibleAside(data)); // 实时显示calling box if (!joinedAtThisContext && context == "dm") { - dispatch(updateCallInfo({ from: loginUser?.uid ?? 0, to: id, calling: true })); + dispatch(updateCallInfo({ from: loginUser?.uid ?? 0, to: id, calling: false })); } }; if (!loginUser || !enabled) return null; diff --git a/src/routes/chat/VoiceFullscreen.tsx b/src/routes/chat/VoiceFullscreen.tsx index 5a00c399..b997863c 100644 --- a/src/routes/chat/VoiceFullscreen.tsx +++ b/src/routes/chat/VoiceFullscreen.tsx @@ -1,24 +1,18 @@ import { useEffect, useState, MouseEvent } from 'react'; import { useDispatch } from 'react-redux'; -import { useTranslation } from 'react-i18next'; import clsx from 'clsx'; import IconMic from '@/assets/icons/mic.on.svg'; import IconMicOff from '@/assets/icons/mic.off.svg'; -import IconCameraOff from '@/assets/icons/camera.off.svg'; import IconPin from '@/assets/icons/pin.svg'; -import IconCamera from '@/assets/icons/camera.svg'; -import IconScreen from '@/assets/icons/share.screen.svg'; import IconExitScreen from '@/assets/icons/fullscreen.exit.svg'; -import IconCallOff from '@/assets/icons/call.off.svg'; import { ChatContext } from '../../types/common'; import { useAppSelector } from '../../app/store'; import Avatar from '../../components/Avatar'; import { playAgoraVideo } from '../../utils'; -import { updateChannelVisibleAside } from '../../app/slices/footprint'; import Tooltip from '../../components/Tooltip'; import { useVoice } from '../../components/Voice'; -import StyledButton from '../../components/styled/Button'; import { updatePin } from '../../app/slices/voice'; +import Operations from '@/components/Voice/Operations'; type Props = { context: ChatContext, @@ -28,8 +22,7 @@ type Props = { const VoiceFullscreen = ({ id, context }: Props) => { const dispatch = useDispatch(); const [speakingUid, setSpeakingUid] = useState(0); - const { t } = useTranslation("chat"); - const { voicingInfo, setMute, leave, closeCamera, openCamera, startShareScreen, stopShareScreen } = useVoice({ id, context }); + const { voicingInfo, exitFullscreen } = useVoice({ id, context }); const { name, userData, voicingMembers } = useAppSelector(store => { return { userData: store.users.byId, @@ -49,11 +42,6 @@ const VoiceFullscreen = ({ id, context }: Props) => { }); }, [voicingMembers]); - const handleExitFullscreen = () => { - if (context == "channel") { - dispatch(updateChannelVisibleAside({ id, aside: "voice" })); - } - }; const handleDoubleClick = (evt: MouseEvent) => { const uid = evt.currentTarget.dataset.uid; @@ -77,14 +65,13 @@ const VoiceFullscreen = ({ id, context }: Props) => { const members = voicingMembers.ids; const membersData = voicingMembers.byId; const hasPin = typeof pinUid !== "undefined"; - const { muted, video, shareScreen } = voicingInfo; return (
{/* top */}
{_name}
- +
{/* middle */} @@ -129,28 +116,9 @@ const VoiceFullscreen = ({ id, context }: Props) => { {/* bottom */} -
    - -
  • - {muted ? : } -
  • -
    - -
  • - {video ? : } -
  • -
    - -
  • - -
  • -
    - - - - - -
+
+ +
); }; diff --git a/src/routes/chat/index.tsx b/src/routes/chat/index.tsx index a10aad42..8df88b1b 100644 --- a/src/routes/chat/index.tsx +++ b/src/routes/chat/index.tsx @@ -24,11 +24,12 @@ function ChatPage() { const [channelModalVisible, setChannelModalVisible] = useState(false); const [usersModalVisible, setUsersModalVisible] = useState(false); const { channel_id = 0, user_id = 0 } = useParams(); - const { sessionUids, isGuest, aside = "" } = useAppSelector((store) => { + const { sessionUids, isGuest, aside = "", callingTo } = useAppSelector((store) => { return { + callingTo: store.voice.callingTo, isGuest: store.authData.guest, sessionUids: store.userMessage.ids, - aside: channel_id ? store.footprint.channelAsides[+channel_id] : store.footprint.dmAsides[+user_id] + aside: channel_id ? store.footprint.channelAsides[+channel_id] : store.footprint.dmAsides[store.voice.callingTo] }; }); const toggleUsersModalVisible = () => { @@ -56,7 +57,7 @@ function ChatPage() { const dmChatVisible = user_id != 0 && aside !== "voice_fullscreen"; const isMainPath = isHomePath || isChatHomePath; const context = channel_id !== 0 ? "channel" : "dm"; - const contextId = (+channel_id || +user_id) ?? 0; + const contextId = (+channel_id || callingTo) ?? 0; console.log("fffff", channel_id, user_id, aside, channelChatVisible); return (