diff --git a/src/app/slices/channels.ts b/src/app/slices/channels.ts index b712aefb..bd781730 100644 --- a/src/app/slices/channels.ts +++ b/src/app/slices/channels.ts @@ -3,8 +3,10 @@ import { isNull, omitBy } from "lodash"; import BASE_URL from "../config"; import { Channel, UpdateChannelDTO, UpdatePinnedMessageDTO } from "../../types/channel"; +type ChannelAside = "members" | "voice" | null; interface StoredChannel extends Channel { icon?: string; + visibleAside: ChannelAside } interface State { @@ -33,7 +35,8 @@ const channelsSlice = createSlice({ icon: c.avatar_updated_at == 0 ? "" - : `${BASE_URL}/resource/group_avatar?gid=${c.gid}&t=${c.avatar_updated_at}` + : `${BASE_URL}/resource/group_avatar?gid=${c.gid}&t=${c.avatar_updated_at}`, + visibleAside: "members" }; }); }, @@ -48,7 +51,8 @@ const channelsSlice = createSlice({ icon: avatar_updated_at == 0 ? "" - : `${BASE_URL}/resource/group_avatar?gid=${gid}&t=${avatar_updated_at}` + : `${BASE_URL}/resource/group_avatar?gid=${gid}&t=${avatar_updated_at}`, + visibleAside: "members" }; }, updateChannel(state, action: PayloadAction) { @@ -99,6 +103,12 @@ const channelsSlice = createSlice({ } } }, + updateChannelVisibleAside(state, action: PayloadAction<{ id: number, aside: ChannelAside }>) { + const { id, aside } = action.payload; + if (state.byId[id]) { + state.byId[id].visibleAside = aside; + } + }, removeChannel(state, action: PayloadAction) { const gid = action.payload; const idx = state.ids.findIndex((i) => i == gid); @@ -116,7 +126,8 @@ export const { fillChannels, addChannel, updateChannel, - removeChannel + removeChannel, + updateChannelVisibleAside } = channelsSlice.actions; export default channelsSlice.reducer; diff --git a/src/app/slices/users.ts b/src/app/slices/users.ts index 18664e25..5a787ad0 100644 --- a/src/app/slices/users.ts +++ b/src/app/slices/users.ts @@ -4,10 +4,12 @@ import BASE_URL from "../config"; import { User } from "../../types/user"; import { UserLog, UserState } from "../../types/sse"; +type DMAside = "voice" | null; export interface StoredUser extends User { online?: boolean; voice?: boolean; avatar?: string; + visibleAside?: DMAside } export interface State { diff --git a/src/app/slices/voice.ts b/src/app/slices/voice.ts index a226bec6..762ebab1 100644 --- a/src/app/slices/voice.ts +++ b/src/app/slices/voice.ts @@ -9,11 +9,13 @@ export type VoiceBasicInfo = { export type VoicingInfo = { downlinkNetworkQuality?: number, muted: boolean, + deafen: boolean } & VoiceBasicInfo export type VoicingMemberInfo = { speakingVolume?: number, - muted?: boolean + muted?: boolean, + deafen?: boolean } export type VoicingMembers = { @@ -73,6 +75,18 @@ const voiceSlice = createSlice({ } } }, + updateDeafenStatus(state, { payload }: PayloadAction) { + if (state.voicing) { + state.voicing.deafen = payload; + state.voicing.muted = payload; + // 更新登录用户在member list的状态 + const loginUid = localStorage.getItem(KEY_UID) ?? 0; + const idx = state.voicingMembers.ids.findIndex((uid) => uid == loginUid); + if (idx > -1) { + state.voicingMembers.byId[+loginUid].muted = payload; + } + } + }, updateVoicingNetworkQuality(state, { payload }: PayloadAction) { if (state.voicing) { state.voicing.downlinkNetworkQuality = payload; @@ -109,5 +123,5 @@ const voiceSlice = createSlice({ }, }); -export const { addVoiceMember, removeVoiceMember, updateVoiceList, updateVoicingInfo, updateVoicingNetworkQuality, updateMuteStatus, updateVoicingMember } = voiceSlice.actions; +export const { addVoiceMember, removeVoiceMember, updateVoiceList, updateVoicingInfo, updateVoicingNetworkQuality, updateMuteStatus, updateVoicingMember, updateDeafenStatus } = voiceSlice.actions; export default voiceSlice.reducer; diff --git a/src/common/component/Voice.tsx b/src/common/component/Voice.tsx index 2fb0c944..800f87e8 100644 --- a/src/common/component/Voice.tsx +++ b/src/common/component/Voice.tsx @@ -2,7 +2,7 @@ import AgoraRTC, { IMicrophoneAudioTrack } 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, updateMuteStatus, updateVoicingInfo, updateVoicingMember, updateVoicingNetworkQuality } from '../../app/slices/voice'; +import { addVoiceMember, removeVoiceMember, updateDeafenStatus, updateMuteStatus, updateVoicingInfo, updateVoicingMember, updateVoicingNetworkQuality } from '../../app/slices/voice'; import { useAppSelector } from '../../app/store'; // type Props = {} @@ -150,6 +150,7 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => { await window.VOICE_CLIENT.publish(localTrack); console.log("Publish success!,joined the channel"); dispatch(updateVoicingInfo({ + deafen: false, muted: false, id, context, @@ -174,8 +175,27 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => { dispatch(updateMuteStatus(mute)); } }; + const setDeafen = (deafen: boolean) => { + if (localTrack) { + if (deafen) { + localTrack.setMuted(true); + // 远端音频,全部静音 + Object.entries(window.VOICE_TRACK_MAP).forEach(([, audioTrack]) => { + audioTrack?.setVolume(0); + }); + } else { + localTrack.setMuted(false); + // 远端音频,恢复原音 + Object.entries(window.VOICE_TRACK_MAP).forEach(([, audioTrack]) => { + audioTrack?.setVolume(100); + }); + } + dispatch(updateDeafenStatus(deafen)); + } + }; return { setMute, + setDeafen, leave, // canVoice, voicingInfo, diff --git a/src/routes/chat/ChannelChat/index.tsx b/src/routes/chat/ChannelChat/index.tsx index 4e8dacd8..49d50af6 100644 --- a/src/routes/chat/ChannelChat/index.tsx +++ b/src/routes/chat/ChannelChat/index.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, memo } from "react"; +import { useEffect, memo } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import Tippy from "@tippyjs/react"; import { useTranslation } from "react-i18next"; @@ -17,6 +17,8 @@ import { useAppSelector } from "../../../app/store"; import GoBackNav from "../../../common/component/GoBackNav"; import Members from "./Members"; import VoiceChat from "../VoiceChat"; +import { updateChannelVisibleAside } from "../../../app/slices/channels"; +import Dashboard from "../VoiceChat/Dashboard"; type Props = { cid?: number; dropFiles?: File[]; @@ -26,7 +28,6 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) { const { pathname } = useLocation(); const navigate = useNavigate(); const dispatch = useDispatch(); - const [membersVisible, setMembersVisible] = useState(true); const { userIds, @@ -53,7 +54,10 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) { }, [pathname]); const toggleMembersVisible = () => { - setMembersVisible((prev) => !prev); + dispatch(updateChannelVisibleAside({ + id: cid, + aside: data.visibleAside !== "members" ? "members" : null + })); }; @@ -84,7 +88,8 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) { - + {/* 音频 */} + - + @@ -120,7 +125,10 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) { } users={ - + + } + voice={ + } />; } diff --git a/src/routes/chat/Layout/index.tsx b/src/routes/chat/Layout/index.tsx index a5d20aad..96f518b6 100644 --- a/src/routes/chat/Layout/index.tsx +++ b/src/routes/chat/Layout/index.tsx @@ -23,6 +23,7 @@ interface Props { header: ReactElement; aside?: ReactElement | null; users?: ReactElement | null; + voice?: ReactElement | null; dropFiles?: File[]; context: "channel" | "user"; to: number; @@ -33,6 +34,7 @@ const Layout: FC = ({ header, aside = null, users = null, + voice = null, dropFiles = [], context = "channel", to @@ -116,6 +118,7 @@ const Layout: FC = ({ {aside && } {users &&
{users}
} + {voice &&
{voice}
} {!readonly && inputMode == "text" && isActive && ( )} diff --git a/src/routes/chat/RTCWidget.tsx b/src/routes/chat/RTCWidget.tsx index 3f08b2df..46a5e686 100644 --- a/src/routes/chat/RTCWidget.tsx +++ b/src/routes/chat/RTCWidget.tsx @@ -16,7 +16,7 @@ type Props = { } const RTCWidget = ({ id, context = "channel" }: Props) => { - const { leave, voicingInfo, setMute } = useVoice({ context, id }); + const { leave, voicingInfo, setMute, setDeafen } = useVoice({ context, id }); const { loginUser, channelData, userData } = useAppSelector(store => { return { userData: store.users.byId, @@ -51,7 +51,7 @@ const RTCWidget = ({ id, context = "channel" }: Props) => { {/* {voicingInfo && */}
- + {voicingInfo.deafen ? : } {voicingInfo.muted ? : diff --git a/src/routes/chat/VoiceChat/Dashboard.tsx b/src/routes/chat/VoiceChat/Dashboard.tsx index 41c20cd3..0c101a6a 100644 --- a/src/routes/chat/VoiceChat/Dashboard.tsx +++ b/src/routes/chat/VoiceChat/Dashboard.tsx @@ -5,17 +5,18 @@ import JoinVoice from './JoinVoice'; import { useVoice } from '../../../common/component/Voice'; type Props = { + visible: boolean, context?: "channel" | "dm", id: number, } -const Dashboard = ({ context = "channel", id }: Props) => { - const { joinVoice, joined, joining, voicingInfo } = useVoice({ id, context }); +const Dashboard = ({ context = "channel", id, visible }: Props) => { + const { joinVoice, joined, joining, voicingInfo, setMute, setDeafen, leave } = useVoice({ id, context }); // const dispatch = useDispatch(); - return
- {joined ? : } + return
+ {joined ? : }
; }; diff --git a/src/routes/chat/VoiceChat/JoinVoice.tsx b/src/routes/chat/VoiceChat/JoinVoice.tsx index d1c54f3c..61e056e3 100644 --- a/src/routes/chat/VoiceChat/JoinVoice.tsx +++ b/src/routes/chat/VoiceChat/JoinVoice.tsx @@ -7,12 +7,12 @@ type Props = { } const JoinVoice = ({ joining, join }: Props) => { - if (joining) return
- Joining + if (joining) return
+ Connecting to voice channel...
; return (
- Join + Start Audio Chat
); }; diff --git a/src/routes/chat/VoiceChat/VoiceManagement.tsx b/src/routes/chat/VoiceChat/VoiceManagement.tsx index 055bc765..44402666 100644 --- a/src/routes/chat/VoiceChat/VoiceManagement.tsx +++ b/src/routes/chat/VoiceChat/VoiceManagement.tsx @@ -3,15 +3,22 @@ import React from 'react'; import { VoicingInfo } from '../../../app/slices/voice'; import { useAppSelector } from '../../../app/store'; import Avatar from '../../../common/component/Avatar'; +import IconHeadphone from '../../../assets/icons/headphone.svg'; +import IconHeadphoneOff from '../../../assets/icons/headphone.off.svg'; import IconMic from '../../../assets/icons/mic.on.svg'; import IconMicOff from '../../../assets/icons/mic.off.svg'; +import StyledButton from '../../../common/component/styled/Button'; +import IconCallOff from '../../../assets/icons/call.off.svg'; // import User from '../../../common/component/User'; type Props = { - info: VoicingInfo | null + info: VoicingInfo | null, + setMute: (param: boolean) => void, + setDeafen: (param: boolean) => void, + leave: () => void } -const VoiceManagement = ({ info }: Props) => { +const VoiceManagement = ({ info, setMute, setDeafen, leave }: Props) => { const { userData, voicingMembers } = useAppSelector(store => { return { userData: store.users.byId, @@ -19,39 +26,37 @@ const VoiceManagement = ({ info }: Props) => { }; }); if (!info) return null; - const { context, id } = info; + const { deafen, muted } = info; const nameClass = clsx(`text-sm text-gray-500 max-w-[190px] truncate font-semibold dark:text-white`); const members = voicingMembers.ids; const membersData = voicingMembers.byId; return ( -
-
    +
    +
      {members.map((uid) => { const curr = userData[uid]; if (!curr) return null; - const { muted, speakingVolume = 0 } = membersData[uid]; + const { muted, deafen, speakingVolume = 0 } = membersData[uid]; const speaking = speakingVolume > 50; - return
    • -
      -
      -
      - -
      - - {curr?.name} - -
      -
      - {/* {muted ? : } */} - {muted ? : } + return
    • +
      +
      +
      + + {curr?.name} + +
      +
      + {deafen ? : } + {muted ? : }
      {/* */} {/* {userData[uid]?.name} */} @@ -59,6 +64,20 @@ const VoiceManagement = ({ info }: Props) => { })}
    +
    +
      +
    • + {deafen ? : } +
    • +
    • + {muted ? : } + +
    • +
    + + + +
    ); }; diff --git a/src/routes/chat/VoiceChat/index.tsx b/src/routes/chat/VoiceChat/index.tsx index 22167cb1..229b0b53 100644 --- a/src/routes/chat/VoiceChat/index.tsx +++ b/src/routes/chat/VoiceChat/index.tsx @@ -1,11 +1,13 @@ // import React from 'react'; // import Tippy from '@tippyjs/react'; -import { useState } from 'react'; +// import { useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; +import { updateChannelVisibleAside } from '../../../app/slices/channels'; import { useAppSelector } from '../../../app/store'; import IconHeadphone from '../../../assets/icons/headphone.svg'; import Tooltip from '../../../common/component/Tooltip'; -import Dashboard from './Dashboard'; +// import Dashboard from './Dashboard'; type Props = { context?: "channel" | "dm" @@ -13,18 +15,24 @@ type Props = { } const VoiceChat = ({ id, context = "channel" }: Props) => { - const [dashboardVisible, setDashboardVisible] = useState(false); - const { loginUser } = useAppSelector(store => { return { loginUser: store.authData.user }; }); + const dispatch = useDispatch(); + const { loginUser, contextData } = useAppSelector(store => { return { loginUser: store.authData.user, contextData: context == "channel" ? store.channels.byId[id] : store.users.byId[id] }; }); const { t } = useTranslation("chat"); const toggleDashboard = () => { - setDashboardVisible(prev => !prev); + if (context == "channel") { + dispatch(updateChannelVisibleAside({ + id, + aside: contextData.visibleAside == "voice" ? null : "voice" + })); + } + // todo DM }; if (!loginUser) return null; + const visible = contextData.visibleAside == "voice"; return ( - +
  • - - {dashboardVisible && } +
  • );