refactor: DM voicing

This commit is contained in:
Tristan Yang
2023-05-11 11:56:22 +08:00
parent 52ff1eb7dc
commit 1bead4e244
10 changed files with 270 additions and 119 deletions
+155
View File
@@ -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;
+3 -1
View File
@@ -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} />
)}
+5 -3
View File
@@ -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