From f13616f176408bdbdeed91db536d880e2c1985e7 Mon Sep 17 00:00:00 2001 From: Tristan Yang Date: Fri, 28 Apr 2023 10:26:17 +0800 Subject: [PATCH] enhance: more like zoom UX --- src/app/slices/voice.ts | 19 +++++++- src/routes/chat/VoiceChat/Dashboard.tsx | 3 -- src/routes/chat/VoiceChat/VoiceManagement.tsx | 11 +++-- src/routes/chat/VoiceFullscreen.tsx | 47 ++++++++++++------- 4 files changed, 54 insertions(+), 26 deletions(-) diff --git a/src/app/slices/voice.ts b/src/app/slices/voice.ts index f5acd23c..5d2d0b6a 100644 --- a/src/app/slices/voice.ts +++ b/src/app/slices/voice.ts @@ -30,7 +30,8 @@ export type VoicingMembers = { ids: number[], byId: { [key: number]: VoicingMemberInfo - } + }, + pin?: number } export type VoiceInfo = { memberCount: number @@ -142,6 +143,12 @@ const voiceSlice = createSlice({ const { uid, info } = payload; state.voicingMembers.byId[uid] = { ...state.voicingMembers.byId[uid], ...info }; } + }, + updatePin(state, { payload }: PayloadAction<{ uid: number, action: "pin" | "unpin" }>) { + const idx = state.voicingMembers.ids.findIndex((uid) => uid == payload.uid); + if (idx > -1) { + state.voicingMembers.pin = payload.action == "pin" ? payload.uid : undefined; + } } }, extraReducers: (builder) => { @@ -149,6 +156,14 @@ const voiceSlice = createSlice({ // reset voicing info if (window.VOICE_CLIENT) { window.VOICE_CLIENT.leave(); + Object.entries(window.VOICE_TRACK_MAP).forEach(([uid, track]) => { + track?.close(); + delete window.VOICE_TRACK_MAP[+uid]; + }); + Object.entries(window.VIDEO_TRACK_MAP).forEach(([uid, track]) => { + track?.close(); + delete window.VOICE_TRACK_MAP[+uid]; + }); } state.voicing = null; state.voicingMembers = { @@ -159,5 +174,5 @@ const voiceSlice = createSlice({ } }); -export const { updateConnectionState, addVoiceMember, removeVoiceMember, upsertVoiceList, updateVoicingInfo, updateVoicingNetworkQuality, updateMuteStatus, updateVoicingMember, updateDeafenStatus } = voiceSlice.actions; +export const { updatePin, updateConnectionState, addVoiceMember, removeVoiceMember, upsertVoiceList, updateVoicingInfo, updateVoicingNetworkQuality, updateMuteStatus, updateVoicingMember, updateDeafenStatus } = voiceSlice.actions; export default voiceSlice.reducer; diff --git a/src/routes/chat/VoiceChat/Dashboard.tsx b/src/routes/chat/VoiceChat/Dashboard.tsx index cf7efaaa..2e45c0b8 100644 --- a/src/routes/chat/VoiceChat/Dashboard.tsx +++ b/src/routes/chat/VoiceChat/Dashboard.tsx @@ -12,9 +12,6 @@ type Props = { const Dashboard = ({ context = "channel", id, visible }: Props) => { const { voicingInfo, setMute, leave, closeCamera, openCamera, startShareScreen, stopShareScreen } = useVoice({ id, context }); - // const dispatch = useDispatch(); - - return
; diff --git a/src/routes/chat/VoiceChat/VoiceManagement.tsx b/src/routes/chat/VoiceChat/VoiceManagement.tsx index 2c0a1252..0f781d3e 100644 --- a/src/routes/chat/VoiceChat/VoiceManagement.tsx +++ b/src/routes/chat/VoiceChat/VoiceManagement.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx'; import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { VoicingInfo } from '../../../app/slices/voice'; +import { VoicingInfo, updatePin } from '../../../app/slices/voice'; import { useAppSelector } from '../../../app/store'; import Avatar from '../../../common/component/Avatar'; // import IconHeadphone from '../../../assets/icons/sound.on.svg'; @@ -49,9 +49,12 @@ const VoiceManagement = ({ id, context, info, setMute, leave, closeCamera, openC playAgoraVideo(id); }); }, [voicingMembers.ids]); - const handleFullscreen = () => { + const handleFullscreen = (uid?: number) => { if (context == "channel") { dispatch(updateChannelVisibleAside({ id, aside: "voice_fullscreen" })); + if (uid) { + dispatch(updatePin({ uid, action: "pin" })); + } } }; if (!info) return null; @@ -103,7 +106,7 @@ const VoiceManagement = ({ id, context, info, setMute, leave, closeCamera, openC {muted ? : } -
+
{/* camera placeholder */}
; @@ -128,7 +131,7 @@ const VoiceManagement = ({ id, context, info, setMute, leave, closeCamera, openC -
  • +
  • diff --git a/src/routes/chat/VoiceFullscreen.tsx b/src/routes/chat/VoiceFullscreen.tsx index 58d22437..ef49d80c 100644 --- a/src/routes/chat/VoiceFullscreen.tsx +++ b/src/routes/chat/VoiceFullscreen.tsx @@ -18,6 +18,7 @@ import { updateChannelVisibleAside } from '../../app/slices/footprint'; import Tooltip from '../../common/component/Tooltip'; import { useVoice } from '../../common/component/Voice'; import StyledButton from '../../common/component/styled/Button'; +import { updatePin } from '../../app/slices/voice'; type Props = { context: ChatContext, @@ -25,9 +26,8 @@ type Props = { } const VoiceFullscreen = ({ id, context }: Props) => { - const [speakingIndex, setSpeakingIndex] = useState(0); - const [pin, setPin] = useState(undefined); const dispatch = useDispatch(); + const [speakingUid, setSpeakingUid] = useState(0); const { t } = useTranslation("chat"); const { voicingInfo, setMute, leave, closeCamera, openCamera, startShareScreen, stopShareScreen } = useVoice({ id, context }); const { name, userData, voicingMembers } = useAppSelector(store => { @@ -39,12 +39,12 @@ const VoiceFullscreen = ({ id, context }: Props) => { }); useEffect(() => { const ids = voicingMembers.ids; - ids.forEach((id, idx) => { + ids.forEach((id) => { playAgoraVideo(id); const { speakingVolume = 0 } = voicingMembers.byId[id]; const speaking = speakingVolume > 50; if (speaking) { - setSpeakingIndex(idx); + setSpeakingUid(id); } }); @@ -54,19 +54,29 @@ const VoiceFullscreen = ({ id, context }: Props) => { dispatch(updateChannelVisibleAside({ id, aside: "voice" })); } }; + + const handleDoubleClick = (evt: MouseEvent) => { + const uid = evt.currentTarget.dataset.uid; + if (uid) { + dispatch(updatePin({ uid: +uid, action: "pin" })); + } + }; + const handlePin = (evt: MouseEvent) => { - const idx = evt.currentTarget.dataset.idx ?? ""; - if (idx == "undefined") { - setPin(undefined); + const uid = evt.currentTarget.dataset.uid ?? ""; + const action = evt.currentTarget.dataset.action ?? "pin"; + if (action == "unpin") { + dispatch(updatePin({ uid: +uid, action })); } else { - setPin(parseInt(idx)); + dispatch(updatePin({ uid: +uid, action: "pin" })); } }; if (!voicingInfo) return null; + const pinUid = voicingMembers.pin; const _name = context == "channel" ? `# ${name}` : `@ ${name}`; const members = voicingMembers.ids; const membersData = voicingMembers.byId; - const hasPin = typeof pin !== "undefined"; + const hasPin = typeof pinUid !== "undefined"; const { muted, video, shareScreen } = voicingInfo; return (
    @@ -74,19 +84,19 @@ const VoiceFullscreen = ({ id, context }: Props) => {
    {_name}
    - +
    {/* middle */}
      - {members.map((uid, idx) => { + {members.map((uid) => { const curr = userData[uid]; if (!curr) return null; const { muted, speakingVolume = 0, shareScreen } = membersData[uid]; const speaking = speakingVolume > 50; - const special = hasPin ? pin == idx : idx == speakingIndex; + const special = hasPin ? pinUid == uid : uid == speakingUid; const disablePin = special && !hasPin; - return
    • + return
    • {speaking &&
      } { />
      {shareScreen ?
      : null} - {!disablePin && } + {!disablePin && + + + } {curr?.name} @@ -129,7 +142,7 @@ const VoiceFullscreen = ({ id, context }: Props) => {
    • - +