refactor: DM voicing
This commit is contained in:
+29
-29
@@ -11,13 +11,9 @@ export type VoiceBasicInfo = {
|
||||
|
||||
export type VoicingInfo = {
|
||||
downlinkNetworkQuality?: number,
|
||||
muted?: boolean,
|
||||
deafen?: boolean,
|
||||
video?: boolean,
|
||||
shareScreen?: boolean,
|
||||
joining?: boolean,
|
||||
connectionState?: ConnectionState
|
||||
} & VoiceBasicInfo
|
||||
} & VoiceBasicInfo & VoicingMemberInfo
|
||||
|
||||
export type VoicingMemberInfo = {
|
||||
speakingVolume?: number,
|
||||
@@ -75,6 +71,33 @@ const voiceSlice = createSlice({
|
||||
updateVoicingInfo(state, { payload }: PayloadAction<VoicingInfo | null>) {
|
||||
if (payload) {
|
||||
state.voicing = { ...(state.voicing ?? {}), ...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] = payload;
|
||||
Object.keys(payload).forEach((key) => {
|
||||
switch (key) {
|
||||
case "video":
|
||||
state.voicingMembers.byId[+loginUid].video = payload.video;
|
||||
break;
|
||||
case "muted":
|
||||
state.voicingMembers.byId[+loginUid].muted = payload.muted;
|
||||
break;
|
||||
case "deafen":
|
||||
state.voicingMembers.byId[+loginUid].deafen = payload.deafen;
|
||||
state.voicingMembers.byId[+loginUid].muted = payload.deafen;
|
||||
break;
|
||||
case "shareScreen":
|
||||
state.voicingMembers.byId[+loginUid].shareScreen = payload.shareScreen;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} else {
|
||||
// reset
|
||||
state.voicing = payload;
|
||||
@@ -84,29 +107,6 @@ const voiceSlice = createSlice({
|
||||
};
|
||||
}
|
||||
},
|
||||
updateMuteStatus(state, { payload }: PayloadAction<boolean>) {
|
||||
if (state.voicing) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
},
|
||||
updateDeafenStatus(state, { payload }: PayloadAction<boolean>) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
},
|
||||
updateConnectionState(state, { payload }: PayloadAction<ConnectionState>) {
|
||||
if (state.voicing) {
|
||||
state.voicing.connectionState = payload;
|
||||
@@ -189,5 +189,5 @@ const voiceSlice = createSlice({
|
||||
}
|
||||
});
|
||||
|
||||
export const { updateCalling, updatePin, updateConnectionState, addVoiceMember, removeVoiceMember, upsertVoiceList, updateVoicingInfo, updateVoicingNetworkQuality, updateMuteStatus, updateVoicingMember, updateDeafenStatus } = voiceSlice.actions;
|
||||
export const { updateCalling, updatePin, updateConnectionState, addVoiceMember, removeVoiceMember, upsertVoiceList, updateVoicingInfo, updateVoicingNetworkQuality, updateVoicingMember } = voiceSlice.actions;
|
||||
export default voiceSlice.reducer;
|
||||
|
||||
@@ -18,7 +18,7 @@ const AudioMessage = ({ url, name, size, download }: Props) => {
|
||||
};
|
||||
const _size = formatBytes(size);
|
||||
return (
|
||||
<div className='w-96 flex flex-col gap-2 px-3 py-2 rounded-md border border-solid border-gray-300 dark:border-gray-500 overflow-hidden bg-stone-100 dark:bg-stone-900'>
|
||||
<div className='md:w-96 flex flex-col gap-2 px-3 py-2 rounded-md border border-solid border-gray-300 dark:border-gray-500 overflow-hidden bg-stone-100 dark:bg-stone-900'>
|
||||
<div className="flex justify-between z-30 overflow-hidden">
|
||||
<div className="flex gap-2 ">
|
||||
<IconAudio className="w-9 h-auto" />
|
||||
@@ -27,7 +27,7 @@ const AudioMessage = ({ url, name, size, download }: Props) => {
|
||||
<span>{_size}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a href={download} className="mt-2"><IconDownload className="fill-gray-500 dark:fill-gray-400" /></a>
|
||||
<a href={download} className="mt-2 hidden md:block"><IconDownload className="fill-gray-500 dark:fill-gray-400" /></a>
|
||||
</div>
|
||||
<audio src={url} onCanPlay={handleCanPlay} controls className={clsx("w-full object-cover z-10", canPlay ? "visible" : "invisible")} />
|
||||
</div>
|
||||
|
||||
@@ -100,7 +100,7 @@ const Reply: FC<ReplyProps> = ({ mid, interactive = true }) => {
|
||||
)}
|
||||
onClick={interactive ? handleClick : undefined}
|
||||
>
|
||||
<div className="flex shrink-0 w-6 h-6">
|
||||
<div className="md:flex shrink-0 w-6 h-6 hidden ">
|
||||
<Avatar
|
||||
width={24}
|
||||
height={24}
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
// import React from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
// 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 Tooltip from "../../../common/component/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 Avatar from '../Avatar';
|
||||
import { VoicingInfo, updateCalling } from '../../../app/slices/voice';
|
||||
import { updateCalling } from '../../../app/slices/voice';
|
||||
import useVoice from './useVoice';
|
||||
import { playAgoraVideo } from '../../utils';
|
||||
import Tooltip from '../Tooltip';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
type Props = {
|
||||
from: number
|
||||
@@ -25,10 +21,11 @@ type Props = {
|
||||
}
|
||||
|
||||
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 navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
const dispatch = useDispatch();
|
||||
const { leave, joinVoice, joining } = useVoice({ id: to, context: "dm" });
|
||||
const containerRef = useRef(null);
|
||||
const { voicingInfo, voicingMembers, fromUser, toUser, loginUser } = useAppSelector(store => {
|
||||
return {
|
||||
voicingInfo: store.voice.voicing ?? {},
|
||||
@@ -39,6 +36,13 @@ const DMCalling = ({ from, to = 0 }: Props) => {
|
||||
};
|
||||
});
|
||||
const sendByMe = loginUser?.uid !== toUser.uid;
|
||||
|
||||
useEffect(() => {
|
||||
const ids = voicingMembers.ids;
|
||||
ids.forEach(id => {
|
||||
playAgoraVideo(id);
|
||||
});
|
||||
}, [voicingMembers.ids]);
|
||||
const handleCancel = () => {
|
||||
console.log('cancel');
|
||||
if (sendByMe || voicingMembers.ids.length == 2) {
|
||||
@@ -48,45 +52,27 @@ const DMCalling = ({ from, to = 0 }: Props) => {
|
||||
};
|
||||
const handleAnswer = () => {
|
||||
joinVoice();
|
||||
navigate(`/chat/dm/${from}`);
|
||||
};
|
||||
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;
|
||||
const connected = voicingMembers.ids.length == 2;
|
||||
console.log("dm calling", !fromUser, !toUser, connected);
|
||||
const atChatPath = sendByMe ? pathname == `/chat/dm/${to}` : pathname == `/chat/dm/${from}`;
|
||||
if (!fromUser || !toUser || connected || atChatPath) return null;
|
||||
|
||||
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>
|
||||
w-64 h-80
|
||||
`)}>
|
||||
|
||||
<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' />
|
||||
@@ -102,41 +88,20 @@ const DMCalling = ({ from, to = 0 }: Props) => {
|
||||
/>
|
||||
<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 &&
|
||||
{!sendByMe &&
|
||||
<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>
|
||||
|
||||
@@ -13,8 +13,9 @@ 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 => {
|
||||
const { from, to, voiceList, loginUid, voicingInfo } = useAppSelector(store => {
|
||||
return {
|
||||
voicingInfo: store.voice.voicing,
|
||||
voiceList: store.voice.list,
|
||||
from: store.voice.callingFrom,
|
||||
to: store.voice.callingTo,
|
||||
@@ -83,6 +84,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(updateCalling({ from: 0 }));
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -119,6 +124,14 @@ const Voice = () => {
|
||||
// todo
|
||||
dispatch(updateVoicingMember({ uid: +uid, info: { muted: false } }));
|
||||
break;
|
||||
case "mute-video":
|
||||
// todo
|
||||
dispatch(updateVoicingMember({ uid: +uid, info: { video: false } }));
|
||||
break;
|
||||
case "unmute-video":
|
||||
// todo
|
||||
dispatch(updateVoicingMember({ uid: +uid, info: { video: true } }));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -147,7 +160,7 @@ const Voice = () => {
|
||||
window.removeEventListener("beforeunload", handlePageUnload, { capture: true });
|
||||
|
||||
};
|
||||
}, []);
|
||||
}, [voicingInfo]);
|
||||
useEffect(() => {
|
||||
// 有人呼叫我
|
||||
const callMeList = voiceList.filter(item => item.context == "dm" && item.id == loginUid);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useDispatch } from 'react-redux';
|
||||
import { useGenerateAgoraTokenMutation } from '../../../app/services/server';
|
||||
import { updateChannelVisibleAside, updateDMVisibleAside } from '../../../app/slices/footprint';
|
||||
|
||||
import { addVoiceMember, updateDeafenStatus, updateMuteStatus, updatePin, updateVoicingInfo, upsertVoiceList } from '../../../app/slices/voice';
|
||||
import { addVoiceMember, updatePin, updateVoicingInfo, upsertVoiceList } from '../../../app/slices/voice';
|
||||
import { useAppSelector } from '../../../app/store';
|
||||
import { playAgoraVideo } from '../../utils';
|
||||
|
||||
@@ -76,6 +76,7 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
|
||||
await window.VOICE_CLIENT?.publish(localVideoTrack);
|
||||
dispatch(updateVoicingInfo({
|
||||
video: true,
|
||||
shareScreen: false,
|
||||
id,
|
||||
context,
|
||||
}));
|
||||
@@ -94,6 +95,7 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
|
||||
playerEle.classList.remove("h-[120px]");
|
||||
dispatch(updateVoicingInfo({
|
||||
video: false,
|
||||
shareScreen: false,
|
||||
id,
|
||||
context,
|
||||
}));
|
||||
@@ -195,9 +197,17 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
|
||||
Object.entries(window.VOICE_TRACK_MAP).forEach(([, audioTrack]) => {
|
||||
audioTrack?.setVolume(100);
|
||||
});
|
||||
dispatch(updateDeafenStatus(false));
|
||||
dispatch(updateVoicingInfo({
|
||||
muted: false,
|
||||
id,
|
||||
context
|
||||
}));
|
||||
}
|
||||
dispatch(updateMuteStatus(mute));
|
||||
dispatch(updateVoicingInfo({
|
||||
muted: mute,
|
||||
id,
|
||||
context
|
||||
}));
|
||||
};
|
||||
const setDeafen = (deafen: boolean) => {
|
||||
const localAudioTrack = window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack;
|
||||
@@ -215,7 +225,7 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
|
||||
audioTrack?.setVolume(100);
|
||||
});
|
||||
}
|
||||
dispatch(updateDeafenStatus(deafen));
|
||||
dispatch(updateVoicingInfo({ deafen, id, context }));
|
||||
};
|
||||
const joinedAtThisContext = voicingInfo ? (voicingInfo.id == id && voicingInfo.context == context) : false;
|
||||
return {
|
||||
|
||||
@@ -84,10 +84,14 @@ const VoiceMessage = ({ file_path }: { file_path: string }) => {
|
||||
if (!current.isPlaying()) {
|
||||
// 先停掉其他的
|
||||
Object.keys(VoiceMap).forEach((key) => {
|
||||
try {
|
||||
const item = VoiceMap[key];
|
||||
if (item && item.isPlaying()) {
|
||||
if (item && item.backend && item.isPlaying()) {
|
||||
item.stop();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
// import React from 'react'
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch } from "react-redux";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { useAppSelector } from "../../../app/store";
|
||||
import Tooltip from "../../../common/component/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, updateCalling } from "../../../app/slices/voice";
|
||||
import { useVoice } from "../../../common/component/Voice";
|
||||
import { playAgoraVideo } from "../../../common/utils";
|
||||
import Avatar from "../../../common/component/Avatar";
|
||||
|
||||
type VoicingMember = {
|
||||
id: number,
|
||||
video: boolean,
|
||||
speaking: boolean,
|
||||
name: string,
|
||||
avatar?: string
|
||||
}
|
||||
type BlockProps = {
|
||||
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;
|
||||
return <div key={id} className={clsx("relative flex-center", video ? "w-80 h-60 overflow-hidden rounded" : "")}>
|
||||
<div className={clsx("w-20 h-20 flex shrink-0 relative transition-opacity", showWaiting && "animate-pulse")}>
|
||||
{speaking && <div className={clsx("z-10 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 opacity-0 rounded-full bg-green-300 animate-speaking", "w-[86px] h-[86px]")}></div>}
|
||||
{showWaiting && <div className={clsx("z-10 absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-gray-400", "w-[88px] h-[88px]")}></div>}
|
||||
<Avatar
|
||||
width={80}
|
||||
height={80}
|
||||
className={clsx("z-20 w-full h-full rounded-full object-cover")}
|
||||
src={avatar}
|
||||
name={name}
|
||||
alt="avatar"
|
||||
/>
|
||||
</div>
|
||||
<div className="z-30 absolute left-0 top-0 w-full h-full" id={`CAMERA_${id}`}>
|
||||
{/* camera video */}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
);
|
||||
return <>{blocks}</>;
|
||||
};
|
||||
type Props = {
|
||||
uid: number
|
||||
}
|
||||
const DMVoice = ({ uid }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation("chat");
|
||||
const { voice: {
|
||||
callingFrom, callingTo, voicing: voicingInfo, voicingMembers
|
||||
}, userData, loginUser } = useAppSelector(store => {
|
||||
return {
|
||||
voice: store.voice,
|
||||
userData: store.users.byId,
|
||||
loginUser: store.authData.user
|
||||
};
|
||||
});
|
||||
const { leave, joinVoice, joining, setMute, closeCamera, openCamera, startShareScreen, stopShareScreen } = useVoice({ id: callingTo, context: "dm" });
|
||||
useEffect(() => {
|
||||
const ids = voicingMembers.ids;
|
||||
ids.forEach(id => {
|
||||
playAgoraVideo(id);
|
||||
});
|
||||
}, [voicingMembers.ids]);
|
||||
if (![callingFrom, callingTo].includes(uid)) return null;
|
||||
|
||||
const { name: fromUsername, avatar: fromAvatar } = userData[callingFrom];
|
||||
const { name: toUsername, avatar: toAvatar, uid: toUid } = userData[callingTo];
|
||||
const sendByMe = loginUser?.uid !== toUid;
|
||||
const handleCancel = () => {
|
||||
console.log('cancel');
|
||||
if (sendByMe || voicingMembers.ids.length == 2) {
|
||||
leave();
|
||||
}
|
||||
dispatch(updateCalling({ from: 0, to: 0 }));
|
||||
};
|
||||
const handleAnswer = () => {
|
||||
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 toSpeaking = toSpeakingVol > 50;
|
||||
const { speakingVolume: fromSpeakingVol = 0, video: fromVideo = false, shareScreen: fromScreen = false } = voicingMembers.byId[callingFrom] ?? {};
|
||||
const fromSpeaking = fromSpeakingVol > 50;
|
||||
return (
|
||||
<div className="py-4 px-10 flex flex-col items-center gap-3 bg-slate-200 dark:bg-slate-800">
|
||||
<div className="flex gap-4">
|
||||
<VoicingBlocks sendByMe={sendByMe} connected={connected} from={{
|
||||
id: callingFrom,
|
||||
video: fromVideo || fromScreen,
|
||||
speaking: fromSpeaking,
|
||||
name: fromUsername,
|
||||
avatar: fromAvatar
|
||||
}} to={{
|
||||
id: callingTo,
|
||||
video: toVideo || toScreen,
|
||||
speaking: toSpeaking,
|
||||
name: toUsername,
|
||||
avatar: toAvatar
|
||||
}} />
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
export default DMVoice;
|
||||
@@ -17,6 +17,7 @@ import IconWarning from '../../../assets/icons/warning.svg';
|
||||
import ImagePreview from "../../../common/component/ImagePreview";
|
||||
|
||||
import VirtualMessageFeed from "./VirtualMessageFeed";
|
||||
import DMVoice from "./DMVoicing";
|
||||
|
||||
interface Props {
|
||||
readonly?: boolean;
|
||||
@@ -98,6 +99,7 @@ const Layout: FC<Props> = ({
|
||||
{header}
|
||||
<div className="w-full h-full flex items-start justify-between relative" >
|
||||
<div className="rounded-br-2xl flex flex-col absolute bottom-0 w-full h-full" ref={messagesContainer}>
|
||||
{context == "user" && <DMVoice uid={to} />}
|
||||
{/* 消息流 */}
|
||||
<VirtualMessageFeed key={`${context}_${to}`} context={context} id={to} />
|
||||
{/* 发送框 */}
|
||||
@@ -120,7 +122,7 @@ const Layout: FC<Props> = ({
|
||||
{aside}
|
||||
</div>}
|
||||
{users && <div className="hidden md:block">{users}</div>}
|
||||
{voice && <div className="max-md:absolute max-md:right-11 max-md:top-14 max-md:h-fit max-md:overflow-y-scroll bg-white dark:!bg-gray-700 md:block">{voice}</div>}
|
||||
{voice && <div className="z-10 max-md:absolute max-md:right-11 max-md:top-14 max-md:overflow-y-scroll bg-white dark:!bg-gray-700 md:block">{voice}</div>}
|
||||
{!readonly && inputMode == "text" && isActive && (
|
||||
<DnDTip context={context} name={name} />
|
||||
)}
|
||||
|
||||
@@ -63,9 +63,11 @@ const Session: FC<IProps> = ({
|
||||
mid: number;
|
||||
is_public: boolean;
|
||||
}>();
|
||||
const { messageData, userData, channelData, readIndex, loginUid, mids, muted, voiceList } = useAppSelector(
|
||||
const { callingFrom, callingTo, messageData, userData, channelData, readIndex, loginUid, mids, muted, voiceList } = useAppSelector(
|
||||
(store) => {
|
||||
return {
|
||||
callingFrom: store.voice.callingFrom,
|
||||
callingTo: store.voice.callingTo,
|
||||
voiceList: store.voice.list,
|
||||
mids: type == "dm" ? store.userMessage.byId[id] : store.channelMessage[id],
|
||||
loginUid: store.authData.user?.uid || 0,
|
||||
@@ -101,9 +103,9 @@ const Session: FC<IProps> = ({
|
||||
messageData,
|
||||
loginUid
|
||||
});
|
||||
const isVoicing = voiceList.some((item) => {
|
||||
const isVoicing = type == "channel" ? voiceList.some((item) => {
|
||||
return item.context == type && item.id === id;
|
||||
});
|
||||
}) : (id == callingFrom || id == callingTo);
|
||||
return (
|
||||
<li className="session">
|
||||
<ContextMenu
|
||||
|
||||
Reference in New Issue
Block a user