feat: useVoice

This commit is contained in:
Tristan Yang
2023-03-24 23:07:56 +08:00
parent da00f3b797
commit 92411d0468
20 changed files with 331 additions and 132 deletions
+23 -10
View File
@@ -3,20 +3,33 @@ import { useAppSelector } from '../../app/store';
import User from '../../common/component/User';
import IconHeadphone from '../../assets/icons/headphone.svg';
import IconMic from '../../assets/icons/mic.on.svg';
import IconSoundOn from '../../assets/icons/sound.on.svg';
import IconSignal from '../../assets/icons/signal.svg';
import { useVoice } from '../../common/component/Voice';
type Props = {}
type Props = {
id: number,
context?: "channel" | "dm"
}
const RTCWidget = (props: Props) => {
const { loginUser, voicing } = useAppSelector(store => { return { loginUser: store.authData.user, voicing: store.authData.voice }; });
const RTCWidget = ({ id, context = "channel" }: Props) => {
const { leave } = useVoice({ context, id });
const { loginUser, joined } = useAppSelector(store => { return { loginUser: store.authData.user, joined: store.voice.joined }; });
if (!loginUser) return null;
return (
<div className='bg-gray-100 dark:bg-gray-900 flex flex-col p-2 rounded-full m-3 text-sm'>
{voicing && <div className="flex justify-between items-center">
<div className="flex items-center gap-3 text-green-800">
Voice Connected
<div className='bg-gray-100 dark:bg-gray-900 flex flex-col p-2 rounded-3xl m-3 text-sm'>
{joined &&
<div className="flex justify-between items-center border-b border-solid border-gray-100 dark:border-gray-800 pb-3 mb-2">
<div className="flex items-center gap-1">
<IconSignal />
<div className="flex flex-col">
<span className='text-green-800'>Voice Connected</span>
<span className='text-gray-600 dark:text-gray-400 text-xs'>Channel / name</span>
</div>
</div>
<IconHeadphone onClick={leave} role="button" className="fill-red-600" />
</div>
</div>}
}
<div className="flex justify-between items-center">
<div className="flex items-center gap-3">
<User uid={loginUser.uid} compact />
@@ -26,7 +39,7 @@ const RTCWidget = (props: Props) => {
</div>
</div>
<div className="flex gap-2">
<IconHeadphone role="button" />
<IconSoundOn role="button" />
<IconMic role="button" />
</div>
</div>
+5 -2
View File
@@ -9,6 +9,7 @@ import getUnreadCount, { renderPreviewMessage } from "../utils";
import User from "../../../common/component/User";
import Avatar from "../../../common/component/Avatar";
import IconLock from "../../../assets/icons/lock.svg";
import IconVoicing from "../../../assets/icons/voicing.svg";
import useContextMenu from "../../../common/hook/useContextMenu";
import useUploadFile from "../../../common/hook/useUploadFile";
import { useAppSelector } from "../../../app/store";
@@ -61,9 +62,10 @@ const Session: FC<IProps> = ({
mid: number;
is_public: boolean;
}>();
const { messageData, userData, channelData, readIndex, loginUid, mids, muted } = useAppSelector(
const { messageData, userData, channelData, readIndex, loginUid, mids, muted, voiceInfo } = useAppSelector(
(store) => {
return {
voiceInfo: store.voice.voicing,
mids: type == "user" ? store.userMessage.byId[id] : store.channelMessage[id],
loginUid: store.authData.user?.uid || 0,
readIndex:
@@ -115,7 +117,7 @@ const Session: FC<IProps> = ({
to={navPath}
onContextMenu={handleContextMenuEvent}
>
<div className="flex shrink-0">
<div className="flex shrink-0 relative">
{type == "user" ? (
<User avatarSize={40} compact interactive={false} uid={id} />
) : (
@@ -128,6 +130,7 @@ const Session: FC<IProps> = ({
src={icon}
/>
)}
{type == "channel" && voiceInfo.id == id && <IconVoicing className="-top-0.5 -right-0.5 absolute w-[18px] h-[18px]" />}
</div>
<div className="w-full flex flex-col justify-between overflow-hidden">
<div className="flex items-center justify-between ">
+8 -64
View File
@@ -1,76 +1,20 @@
import { useEffect } from 'react';
import AgoraRTC from "agora-rtc-sdk-ng";
import useConfig from '../../../common/hook/useConfig';
import { useDispatch } from 'react-redux';
import { updateVoiceStatus } from '../../../app/slices/auth.data';
import { useLazyGetAgoraTokenQuery } from '../../../app/services/server';
// import { useEffect } from 'react';
// import { useDispatch } from 'react-redux';
import VoiceManagement from './VoiceManagement';
import JoinVoice from './JoinVoice';
import { useVoice } from '../../../common/component/Voice';
type Props = {
channel: string,
context?: "channel" | "dm",
id: number,
uid: number,
voicing?: boolean;
}
const Dashboard = ({ id, voicing, uid, channel }: Props) => {
const dispatch = useDispatch();
const [generateToken] = useLazyGetAgoraTokenQuery();
const Dashboard = ({ context = "channel", id }: Props) => {
const { joinVoice, joined, joining, voiceInfo } = useVoice({ id, context });
// const dispatch = useDispatch();
const { agoraConfig } = useConfig("agora");
useEffect(() => {
console.log("agora config", agoraConfig);
const startConnect = async () => {
// Create an instance of the Agora Engine
const agoraEngine = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });
// Listen for the "user-published" event to retrieve an AgoraRTCRemoteUser object.
agoraEngine.on("user-published", async (user, mediaType) => {
// Subscribe to the remote user when the SDK triggers the "user-published" event.
await agoraEngine.subscribe(user, mediaType);
console.log("subscribe success");
// Subscribe and play the remote audio track.
if (mediaType == "audio") {
// channelParameters.remoteUid=user.uid;
// // Get the RemoteAudioTrack object from the AgoraRTCRemoteUser object.
// channelParameters.remoteAudioTrack = user.audioTrack;
// Play the remote audio track.
user.audioTrack?.play();
console.log("Remote user connected: " + user.uid);
}
// Listen for the "user-unpublished" event.
agoraEngine.on("user-unpublished", user => {
console.log(user.uid + "has left the channel");
console.log("Remote user has left the channel");
});
});
// Join a channel.
console.log("join data ", agoraConfig);
const { isError, data } = await generateToken(id);
if (!isError && data) {
const { channel_name, app_id, agora_token, uid } = data;
await agoraEngine.join(app_id, channel_name, agora_token, uid);
console.table(data);
// Create a local audio track from the microphone audio.
const localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack();
// Publish the local audio track in the channel.
await agoraEngine.publish(localAudioTrack);
console.log("Publish success!");
dispatch(updateVoiceStatus(true));
}
};
if (!voicing && agoraConfig) {
startConnect();
}
}, [voicing, agoraConfig, uid, channel]);
return voicing ? <VoiceManagement /> : <JoinVoice />;
return <div className='absolute -left-full -translate-x-full -top-1 z-50 shadow rounded p-2 bg-white dark:bg-black'>{joined ? <VoiceManagement info={voiceInfo} /> : <JoinVoice join={joinVoice} joining={joining} />}</div>;
};
export default Dashboard;
+12 -3
View File
@@ -1,10 +1,19 @@
import React from 'react';
import StyledButton from '../../../common/component/styled/Button';
type Props = {}
type Props = {
join: () => void,
joining: boolean
}
const JoinVoice = (props: Props) => {
const JoinVoice = ({ joining, join }: Props) => {
if (joining) return <div>
Joining
</div>;
return (
<div>JoinVoice</div>
<div>
<StyledButton className='mini' onClick={join}>Join</StyledButton>
</div>
);
};
+21 -3
View File
@@ -1,10 +1,28 @@
import React from 'react';
import { VoiceInfo } from '../../../app/slices/voice';
import { useAppSelector } from '../../../app/store';
import User from '../../../common/component/User';
type Props = {}
type Props = {
info: VoiceInfo | null
}
const VoiceManagement = (props: Props) => {
const VoiceManagement = ({ info }: Props) => {
const userData = useAppSelector(store => store.users.byId);
if (!info) return null;
const { context, id, members } = info;
return (
<div>VoiceManagement</div>
<div>
<ul>
{members.map((uid) => {
return <li key={uid}>
<User uid={uid} interactive={false} />
{/* {userData[uid]?.name} */}
</li>;
})}
</ul>
</div>
);
};
+14 -18
View File
@@ -1,5 +1,6 @@
// import React from 'react';
import Tippy from '@tippyjs/react';
// import Tippy from '@tippyjs/react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useAppSelector } from '../../../app/store';
import IconHeadphone from '../../../assets/icons/headphone.svg';
@@ -7,29 +8,24 @@ import Tooltip from '../../../common/component/Tooltip';
import Dashboard from './Dashboard';
type Props = {
context?: "channel" | "dm"
id: number,
channel: string
}
const VoiceChat = ({ channel, id }: Props) => {
const { loginUser, voice } = useAppSelector(store => { return { loginUser: store.authData.user, voice: store.authData.voice }; });
const VoiceChat = ({ id, context = "channel" }: Props) => {
const [dashboardVisible, setDashboardVisible] = useState(false);
const { loginUser } = useAppSelector(store => { return { loginUser: store.authData.user }; });
const { t } = useTranslation("chat");
const toolClass = `relative cursor-pointer`;
const toggleDashboard = () => {
setDashboardVisible(prev => !prev);
};
if (!loginUser) return null;
return (
<Tooltip tip={t("voice")} placement="left">
<Tippy
placement="left-start"
popperOptions={{ strategy: "fixed" }}
offset={[0, 164]}
interactive
trigger="click"
content={<Dashboard id={id} uid={loginUser.uid} channel={channel} voicing={voice} />}
>
<li className={`${toolClass}`}>
<IconHeadphone className="fill-gray-500" />
</li>
</Tippy>
<Tooltip disabled={dashboardVisible} tip={t("voice")} placement="left">
<li className={`relative`} >
<IconHeadphone className="fill-gray-500" role="button" onClick={toggleDashboard} />
{dashboardVisible && <Dashboard id={id} context={context} />}
</li>
</Tooltip>
);
};
-23
View File
@@ -1,23 +0,0 @@
import { useState } from "react";
import { useAppSelector } from "../../../app/store";
const useVoice = (uid: number) => {
const [engine, setEngine] = useState(window.VoiceEngine);
const { voicing } = useAppSelector(store => {
return {
voicing: store.users.byId[uid].voice ?? false
};
});
return {
voicing,
};
};
export default useVoice;
+3 -1
View File
@@ -50,6 +50,8 @@ function ChatPage() {
// console.log("temp uid", tmpUid);
const placeholderVisible = channel_id == 0 && user_id == 0;
const isMainPath = isHomePath || isChatHomePath;
const context = channel_id !== 0 ? "channel" : "dm";
const contextId = (channel_id || user_id) ?? 0;
return (
<ErrorCatcher>
{channelModalVisible && (
@@ -66,7 +68,7 @@ function ChatPage() {
{isGuest ? <footer className="hidden md:block py-1 text-xs text-gray-300 dark:text-gray-700 text-center">
Host your own <a href="https://voce.chat" target="_blank" rel="noopener noreferrer" className="text-gray-400 dark:text-gray-600">voce.chat</a>
</footer> : <RTCWidget />}
</footer> : <RTCWidget id={+contextId} context={context} />}
</div>
<div className={clsx(`right-container md:rounded-r-2xl w-full bg-white dark:!bg-gray-700`, placeholderVisible && "h-full flex-center", isMainPath && "hidden md:flex")}>
{placeholderVisible && (isGuest ? <GuestBlankPlaceholder /> : <BlankPlaceholder />)}
+2
View File
@@ -19,6 +19,7 @@ import MobileNavs from "./MobileNavs";
import { updateRememberedNavs } from "../../app/slices/ui";
import UnreadTabTip from "../../common/component/UnreadTabTip";
import ReLoginModal from "../../common/component/ReLoginModal";
import Voice from "../../common/component/Voice";
function HomePage() {
@@ -67,6 +68,7 @@ function HomePage() {
<>
{roleChanged && <ReLoginModal />}
{!guest && <UnreadTabTip />}
{!guest && <Voice />}
<Manifest />
{!guest && <Notification />}
<div className={`vocechat-container flex w-screen h-screen bg-gray-200 dark:bg-gray-900`}>