feat: useVoice
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vocechat-web",
|
||||
"version": "0.3.55",
|
||||
"version": "0.3.56",
|
||||
"private": true,
|
||||
"homepage": "https://voce.chat",
|
||||
"dependencies": {
|
||||
|
||||
@@ -27,7 +27,8 @@ const whiteList = [
|
||||
"createAdmin",
|
||||
"getBotRelatedChannels",
|
||||
"sendMessageByBot",
|
||||
"replyWithChatGPT"
|
||||
"replyWithChatGPT",
|
||||
"getAgoraVoicingList"
|
||||
];
|
||||
|
||||
const baseQuery = fetchBaseQuery({
|
||||
|
||||
@@ -17,10 +17,12 @@ import {
|
||||
LicenseResponse,
|
||||
RenewLicense,
|
||||
RenewLicenseResponse,
|
||||
AgoraTokenResponse
|
||||
AgoraTokenResponse,
|
||||
AgoraVoicingListResponse
|
||||
} from "../../types/server";
|
||||
import { Channel } from "../../types/channel";
|
||||
import { ContentTypeKey } from "../../types/message";
|
||||
import { updateVoiceInfo } from "../slices/voice";
|
||||
|
||||
const defaultExpireDuration = 2 * 24 * 60 * 60;
|
||||
|
||||
@@ -115,6 +117,31 @@ export const serverApi = createApi({
|
||||
url: `group/${id}/agora_token`,
|
||||
})
|
||||
}),
|
||||
getAgoraVoicingList: builder.query<AgoraVoicingListResponse, { appid: string, key: string, secret: string }>({
|
||||
query: ({ appid, key, secret }) => ({
|
||||
headers: {
|
||||
Authorization: `Basic ${btoa(`${key}:${secret}`)}`
|
||||
},
|
||||
url: `https://api.agora.io/dev/v1/channel/${appid}`,
|
||||
}),
|
||||
async onQueryStarted(data, { dispatch, queryFulfilled }) {
|
||||
try {
|
||||
const { data: resp } = await queryFulfilled;
|
||||
const { success } = resp;
|
||||
if (success && resp.data.channels.length > 0) {
|
||||
const [channel] = resp.data.channels;
|
||||
const [id] = channel.channel_name.split(":").slice(-1);
|
||||
// todo
|
||||
dispatch(updateVoiceInfo({
|
||||
id: +id,
|
||||
context: "channel"
|
||||
}));
|
||||
}
|
||||
} catch {
|
||||
console.error("get voice list error");
|
||||
}
|
||||
}
|
||||
}),
|
||||
getSMTPConfig: builder.query<SMTPConfig, void>({
|
||||
query: () => ({ url: `admin/smtp/config` })
|
||||
}),
|
||||
@@ -327,5 +354,7 @@ export const {
|
||||
useSendMessageByBotMutation,
|
||||
useUpdateFrontendUrlMutation,
|
||||
useGetFrontendUrlQuery,
|
||||
useLazyGetAgoraTokenQuery
|
||||
useLazyGetAgoraTokenQuery,
|
||||
useGetAgoraConfigQuery,
|
||||
useGetAgoraVoicingListQuery
|
||||
} = serverApi;
|
||||
|
||||
@@ -85,9 +85,6 @@ const authDataSlice = createSlice({
|
||||
updateRoleChanged(state, action: PayloadAction<boolean>) {
|
||||
state.roleChanged = action.payload;
|
||||
},
|
||||
updateVoiceStatus(state, action: PayloadAction<boolean>) {
|
||||
state.voice = action.payload;
|
||||
},
|
||||
resetAuthData() {
|
||||
// remove local data
|
||||
localStorage.removeItem(KEY_EXPIRE);
|
||||
@@ -134,5 +131,5 @@ const authDataSlice = createSlice({
|
||||
// }
|
||||
});
|
||||
|
||||
export const { updateInitialized, updateLoginUser, setAuthData, resetAuthData, updateToken, updateRoleChanged, updateVoiceStatus } = authDataSlice.actions;
|
||||
export const { updateInitialized, updateLoginUser, setAuthData, resetAuthData, updateToken, updateRoleChanged } = authDataSlice.actions;
|
||||
export default authDataSlice.reducer;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
export type VoiceInfo = {
|
||||
context: "channel" | "dm"
|
||||
id: number | null,
|
||||
members: number[]
|
||||
}
|
||||
interface State {
|
||||
joined: boolean,
|
||||
voicing: VoiceInfo
|
||||
}
|
||||
const initialInfo = {
|
||||
context: "channel" as const,
|
||||
id: null,
|
||||
members: []
|
||||
};
|
||||
const initialState: State = {
|
||||
joined: false,
|
||||
voicing: initialInfo
|
||||
};
|
||||
|
||||
|
||||
|
||||
const voiceSlice = createSlice({
|
||||
name: "voice",
|
||||
initialState,
|
||||
reducers: {
|
||||
updateJoinStatus(state, { payload }: PayloadAction<boolean>) {
|
||||
state.joined = payload;
|
||||
},
|
||||
updateVoiceInfo(state, { payload }: PayloadAction<VoiceInfo | null>) {
|
||||
if (!payload) {
|
||||
// reset
|
||||
state.voicing = initialInfo;
|
||||
} else {
|
||||
if (state.voicing) {
|
||||
state.voicing = { ...state.voicing, ...payload };
|
||||
} else {
|
||||
state.voicing = payload;
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
addVoiceMember(state, { payload }: PayloadAction<number>) {
|
||||
if (state.voicing) {
|
||||
if (!state.voicing.members.includes(payload)) {
|
||||
state.voicing.members.push(payload);
|
||||
}
|
||||
}
|
||||
},
|
||||
removeVoiceMember(state, { payload }: PayloadAction<number>) {
|
||||
if (state.voicing) {
|
||||
const idx = state.voicing.members.findIndex((uid) => uid == payload);
|
||||
if (idx > -1) {
|
||||
state.voicing.members.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const { updateJoinStatus, updateVoiceInfo, addVoiceMember, removeVoiceMember } = voiceSlice.actions;
|
||||
export default voiceSlice.reducer;
|
||||
@@ -3,6 +3,7 @@ import { setupListeners } from "@reduxjs/toolkit/query";
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
|
||||
import listenerMiddleware from "./listener.middleware";
|
||||
import authDataReducer from "./slices/auth.data";
|
||||
import voiceReducer from "./slices/voice";
|
||||
import footprintReducer from "./slices/footprint";
|
||||
import serverReducer from "./slices/server";
|
||||
import uiReducer from "./slices/ui";
|
||||
@@ -23,6 +24,7 @@ import { serverApi } from "./services/server";
|
||||
|
||||
const reducer = combineReducers({
|
||||
authData: authDataReducer,
|
||||
voice: voiceReducer,
|
||||
ui: uiReducer,
|
||||
footprint: footprintReducer,
|
||||
server: serverReducer,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="#039855" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 5C20.5523 5 21 5.44735 21 5.99917V19.0008C21 19.5527 20.5523 20 20 20C19.4477 20 19 19.5527 19 19.0008V5.99917C19 5.44735 19.4477 5 20 5ZM16 8C16.5523 8 17 8.44567 17 8.99543V19.0046C17 19.5543 16.5523 20 16 20C15.4477 20 15 19.5543 15 19.0046V8.99543C15 8.44567 15.4477 8 16 8ZM12 11C12.5523 11 13 11.4477 13 12V19C13 19.5523 12.5523 20 12 20C11.4477 20 11 19.5523 11 19V12C11 11.4477 11.4477 11 12 11ZM8 14C8.55228 14 9 14.4451 9 14.9942V19.0058C9 19.5549 8.55228 20 8 20C7.44772 20 7 19.5549 7 19.0058V14.9942C7 14.4451 7.44772 14 8 14ZM4 17C4.55228 17 5 17.4403 5 17.9836V19.0164C5 19.5597 4.55228 20 4 20C3.44772 20 3 19.5597 3 19.0164V17.9836C3 17.4403 3.44772 17 4 17Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 799 B |
@@ -0,0 +1,5 @@
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="2" y="2" width="24" height="24" rx="12" fill="#12B76A"/>
|
||||
<path d="M14.9995 8.50001C14.9995 8.29898 14.8791 8.11749 14.6939 8.03934C14.5086 7.96119 14.2946 8.00157 14.1506 8.14185L11.2239 10.9927H9.49951C8.67108 10.9927 7.99951 11.6643 7.99951 12.4927V15.4817C7.99951 16.3101 8.67108 16.9817 9.49951 16.9817H11.2227L14.1491 19.8567C14.2928 19.9978 14.5071 20.039 14.6929 19.9611C14.8786 19.8832 14.9995 19.7014 14.9995 19.5V8.50001ZM16.1108 11.1887C16.2827 10.9726 16.5972 10.9368 16.8133 11.1087L16.8142 11.1094L16.8151 11.1102L16.8173 11.1119L16.8226 11.1163L16.8376 11.1288C16.8494 11.1388 16.8647 11.1522 16.883 11.169C16.9196 11.2026 16.9684 11.2499 17.0252 11.3112C17.1387 11.4336 17.2852 11.613 17.4302 11.8514C17.7217 12.331 18.0038 13.0444 18.0038 13.9986C18.0038 14.9527 17.7217 15.6669 17.4304 16.1471C17.2855 16.3859 17.1392 16.5657 17.0258 16.6884C16.9691 16.7498 16.9203 16.7973 16.8837 16.8309C16.8641 16.849 16.8439 16.8666 16.8235 16.8838L16.815 16.8907L16.8142 16.8914C16.8142 16.8914 16.3679 17.1337 16.1115 16.8129C15.9399 16.5983 15.9738 16.2858 16.1866 16.1128L16.1882 16.1115L16.1874 16.1122L16.1891 16.1108L16.1882 16.1115C16.1911 16.109 16.1974 16.1036 16.2065 16.0952C16.2247 16.0784 16.2542 16.05 16.2913 16.0098C16.3658 15.9292 16.4699 15.8025 16.5754 15.6285C16.785 15.283 17.0038 14.7461 17.0038 13.9986C17.0038 13.2511 16.785 12.7152 16.5757 12.3709C16.4702 12.1974 16.3662 12.0712 16.2919 11.9911C16.2548 11.951 16.2254 11.9228 16.2072 11.9061C16.1981 11.8978 16.1919 11.8924 16.189 11.89L16.19 11.8907C15.9743 11.7187 15.939 11.4047 16.1108 11.1887ZM17.8126 9.10886C17.5965 8.93686 17.282 8.97255 17.11 9.18858C16.938 9.40451 16.9742 9.71932 17.1899 9.89138L17.201 9.9006C17.2118 9.90975 17.2293 9.92484 17.2526 9.94582C17.2993 9.98781 17.3688 10.0532 17.4537 10.1413C17.6238 10.3179 17.8536 10.5841 18.084 10.9351C18.5445 11.6364 19.0028 12.6685 19.0028 14.004C19.0028 15.3395 18.5445 16.3694 18.0843 17.0685C17.854 17.4184 17.6243 17.6835 17.4544 17.8593C17.3696 17.947 17.3001 18.0121 17.2535 18.0538C17.2302 18.0747 17.2127 18.0897 17.2019 18.0988L17.1903 18.1083L17.1894 18.1091C16.9741 18.2808 16.9381 18.5945 17.1092 18.8105C17.2807 19.0269 17.5959 19.0628 17.8124 18.8913L17.8453 18.8642C17.8639 18.8487 17.8893 18.8268 17.9208 18.7986C17.9837 18.7423 18.0705 18.6607 18.1734 18.5543C18.3788 18.3418 18.6495 18.0286 18.9196 17.6183C19.4603 16.797 20.0028 15.5788 20.0028 14.004C20.0028 12.4293 19.4603 11.2094 18.9199 10.3863C18.6499 9.97503 18.3793 9.66089 18.1741 9.4477C18.0713 9.34097 17.9844 9.25908 17.9216 9.20255C17.8902 9.17426 17.8647 9.15228 17.8462 9.13665L17.8238 9.11798L17.8167 9.11222L17.8143 9.11024L17.8126 9.10886ZM16.189 11.89L16.1876 11.8887L16.189 11.89Z" fill="white"/>
|
||||
<rect x="2" y="2" width="24" height="24" rx="12" stroke="white" stroke-width="4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
@@ -0,0 +1,122 @@
|
||||
import AgoraRTC 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, updateJoinStatus, updateVoiceInfo } from '../../app/slices/voice';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
|
||||
// type Props = {}
|
||||
window.VOICE_TRACK_MAP = window.VOICE_TRACK_MAP ?? {};
|
||||
const Voice = () => {
|
||||
const isAdmin = useAppSelector(store => store.authData.user?.is_admin);
|
||||
const { data: agoraConfig } = useGetAgoraConfigQuery(undefined, {
|
||||
skip: !isAdmin
|
||||
});
|
||||
useGetAgoraVoicingListQuery({
|
||||
appid: agoraConfig?.app_id ?? "",
|
||||
key: agoraConfig?.rtm_key ?? "",
|
||||
secret: agoraConfig?.rtm_secret ?? "",
|
||||
|
||||
}, {
|
||||
skip: !isAdmin || !agoraConfig,
|
||||
pollingInterval: 5000
|
||||
});
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
const initializeAgoraClient = 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);
|
||||
dispatch(addVoiceMember(+user.uid));
|
||||
// console.log("subscribe success");
|
||||
// Subscribe and play the remote audio track.
|
||||
if (mediaType == "audio") {
|
||||
// Play the remote audio track.
|
||||
user.audioTrack?.play();
|
||||
window.VOICE_TRACK_MAP[+user.uid] = user.audioTrack;
|
||||
// console.log("Remote user connected: " + user.uid);
|
||||
}
|
||||
// Listen for the "user-unpublished" event.
|
||||
agoraEngine.on("user-unpublished", user => {
|
||||
dispatch(removeVoiceMember(+user.uid as number));
|
||||
console.log(user.uid + "has left the channel");
|
||||
// console.log("Remote user has left the channel");
|
||||
});
|
||||
});
|
||||
window.VOICE_CLIENT = agoraEngine;
|
||||
};
|
||||
if (!window.VOICE_CLIENT) {
|
||||
initializeAgoraClient();
|
||||
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (window.VOICE_CLIENT) {
|
||||
window.VOICE_CLIENT.leave();
|
||||
}
|
||||
// window.VOICE_CLIENT=null
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
return null;
|
||||
};
|
||||
type VoiceProps = {
|
||||
id: number,
|
||||
context?: "channel" | "dm"
|
||||
}
|
||||
const useVoice = ({ id, context = "channel" }: VoiceProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const { voiceInfo, joined } = useAppSelector(store => {
|
||||
return {
|
||||
voiceInfo: store.voice.voicing,
|
||||
joined: store.voice.joined
|
||||
};
|
||||
});
|
||||
const [generateToken] = useLazyGetAgoraTokenQuery();
|
||||
const [joining, setJoining] = useState(false);
|
||||
const joinVoice = async () => {
|
||||
setJoining(true);
|
||||
const { isError, data } = await generateToken(id);
|
||||
if (!isError && data) {
|
||||
const { channel_name, app_id, agora_token, uid } = data;
|
||||
if (window.VOICE_CLIENT) {
|
||||
await window.VOICE_CLIENT.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 window.VOICE_CLIENT.publish(localAudioTrack);
|
||||
console.log("Publish success!");
|
||||
dispatch(updateVoiceInfo({
|
||||
id,
|
||||
context,
|
||||
members: [uid]
|
||||
}));
|
||||
dispatch(updateJoinStatus(true));
|
||||
}
|
||||
}
|
||||
setJoining(false);
|
||||
};
|
||||
const leave = async () => {
|
||||
if (window.VOICE_CLIENT) {
|
||||
await window.VOICE_CLIENT.leave();
|
||||
dispatch(updateJoinStatus(false));
|
||||
// window.VOICE_TRACK_MAP
|
||||
}
|
||||
};
|
||||
return {
|
||||
leave,
|
||||
// canVoice,
|
||||
voiceInfo,
|
||||
joining,
|
||||
joined,
|
||||
joinVoice
|
||||
};
|
||||
};
|
||||
export { useVoice };
|
||||
export default Voice;
|
||||
@@ -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>}
|
||||
</div>
|
||||
<IconHeadphone onClick={leave} role="button" className="fill-red-600" />
|
||||
</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>
|
||||
|
||||
@@ -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 ">
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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" />
|
||||
<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>
|
||||
</Tippy>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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 />)}
|
||||
|
||||
@@ -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`}>
|
||||
|
||||
Vendored
+6
@@ -1,3 +1,5 @@
|
||||
import { IAgoraRTCClient, IRemoteAudioTrack } from "agora-rtc-sdk-ng";
|
||||
|
||||
interface BeforeInstallPromptEvent extends Event {
|
||||
/**
|
||||
* Returns an array of DOMString items containing the platforms on which the event was dispatched.
|
||||
@@ -29,6 +31,10 @@ export declare global {
|
||||
__WB_MANIFEST: Array<PrecacheEntry | string>;
|
||||
skipWaiting: () => void;
|
||||
CACHE: { [key: string]: typeof localforage | undefined };
|
||||
VOICE_CLIENT?: IAgoraRTCClient;
|
||||
VOICE_TRACK_MAP: {
|
||||
[key: number]: IRemoteAudioTrack | undefined
|
||||
}
|
||||
}
|
||||
interface WindowEventMap {
|
||||
beforeinstallprompt: BeforeInstallPromptEvent;
|
||||
|
||||
@@ -27,6 +27,13 @@ export interface AgoraConfig {
|
||||
rtm_key: string;
|
||||
rtm_secret: string;
|
||||
}
|
||||
export interface AgoraVoicingListResponse {
|
||||
success: boolean,
|
||||
data: {
|
||||
channels: { channel_name: string, user_count: number }[],
|
||||
total_size: number
|
||||
}
|
||||
}
|
||||
export interface AgoraTokenResponse {
|
||||
agora_token: string,
|
||||
uid: number,
|
||||
|
||||
Reference in New Issue
Block a user