refactor: voice data structure

This commit is contained in:
Tristan Yang
2023-03-26 22:04:28 +08:00
parent 137d045c85
commit b201d5d06d
10 changed files with 83 additions and 68 deletions
+11 -8
View File
@@ -22,7 +22,7 @@ import {
} from "../../types/server"; } from "../../types/server";
import { Channel } from "../../types/channel"; import { Channel } from "../../types/channel";
import { ContentTypeKey } from "../../types/message"; import { ContentTypeKey } from "../../types/message";
import { updateVoiceInfo } from "../slices/voice"; import { updateVoiceList } from "../slices/voice";
const defaultExpireDuration = 2 * 24 * 60 * 60; const defaultExpireDuration = 2 * 24 * 60 * 60;
@@ -128,14 +128,17 @@ export const serverApi = createApi({
try { try {
const { data: resp } = await queryFulfilled; const { data: resp } = await queryFulfilled;
const { success } = resp; const { success } = resp;
if (success && resp.data.channels.length > 0) { if (success) {
const [channel] = resp.data.channels; const arr = resp.data.channels.map(data => {
const [id] = channel.channel_name.split(":").slice(-1); const [id] = data.channel_name.split(":").slice(-1);
// todo const count = data.user_count;
dispatch(updateVoiceInfo({ return {
id: +id, id: +id,
context: "channel" context: "channel" as const,
})); memberCount: count
};
});
dispatch(updateVoiceList(arr));
} }
} catch { } catch {
console.error("get voice list error"); console.error("get voice list error");
+22 -27
View File
@@ -1,22 +1,27 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export type VoiceInfo = { export type VoiceBasicInfo = {
context: "channel" | "dm" context: "channel" | "dm"
id: number | null, id: number,
}
export type VoicingInfo = {
members: number[] members: number[]
} } & VoiceBasicInfo
export type VoiceInfo = {
memberCount: number
} & VoiceBasicInfo
interface State { interface State {
joined: boolean, voicing: VoicingInfo | null,
voicing: VoiceInfo list: VoiceInfo[]
} }
const initialInfo = { // const initialInfo = {
context: "channel" as const, // context: "channel" as const,
id: null, // id: 0,
members: [] // members: []
}; // };
const initialState: State = { const initialState: State = {
joined: false, voicing: null,
voicing: initialInfo list: []
}; };
@@ -25,21 +30,11 @@ const voiceSlice = createSlice({
name: "voice", name: "voice",
initialState, initialState,
reducers: { reducers: {
updateJoinStatus(state, { payload }: PayloadAction<boolean>) { updateVoicingInfo(state, { payload }: PayloadAction<VoicingInfo | null>) {
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; state.voicing = payload;
} },
updateVoiceList(state, { payload }: PayloadAction<VoiceInfo[]>) {
} state.list = payload;
}, },
addVoiceMember(state, { payload }: PayloadAction<number>) { addVoiceMember(state, { payload }: PayloadAction<number>) {
if (state.voicing) { if (state.voicing) {
@@ -59,5 +54,5 @@ const voiceSlice = createSlice({
}, },
}); });
export const { updateJoinStatus, updateVoiceInfo, addVoiceMember, removeVoiceMember } = voiceSlice.actions; export const { addVoiceMember, removeVoiceMember, updateVoiceList, updateVoicingInfo } = voiceSlice.actions;
export default voiceSlice.reducer; export default voiceSlice.reducer;
+14 -10
View File
@@ -1,8 +1,8 @@
import AgoraRTC from 'agora-rtc-sdk-ng'; import AgoraRTC, { IMicrophoneAudioTrack } from 'agora-rtc-sdk-ng';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { useGetAgoraConfigQuery, useGetAgoraVoicingListQuery, useLazyGetAgoraTokenQuery } from '../../app/services/server'; import { useGetAgoraConfigQuery, useGetAgoraVoicingListQuery, useLazyGetAgoraTokenQuery } from '../../app/services/server';
import { addVoiceMember, removeVoiceMember, updateJoinStatus, updateVoiceInfo } from '../../app/slices/voice'; import { addVoiceMember, removeVoiceMember, updateVoicingInfo } from '../../app/slices/voice';
import { useAppSelector } from '../../app/store'; import { useAppSelector } from '../../app/store';
// type Props = {} // type Props = {}
@@ -34,6 +34,7 @@ const Voice = () => {
dispatch(addVoiceMember(+user.uid)); dispatch(addVoiceMember(+user.uid));
// console.log("subscribe success"); // console.log("subscribe success");
// Subscribe and play the remote audio track. // Subscribe and play the remote audio track.
console.log(user.uid + "has joined the channel");
if (mediaType == "audio") { if (mediaType == "audio") {
// Play the remote audio track. // Play the remote audio track.
user.audioTrack?.play(); user.audioTrack?.play();
@@ -71,10 +72,11 @@ type VoiceProps = {
} }
const useVoice = ({ id, context = "channel" }: VoiceProps) => { const useVoice = ({ id, context = "channel" }: VoiceProps) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const { voiceInfo, joined } = useAppSelector(store => { const { voiceInfo, joined, loginUid = 0 } = useAppSelector(store => {
return { return {
loginUid: store.authData.user?.uid,
voiceInfo: store.voice.voicing, voiceInfo: store.voice.voicing,
joined: store.voice.joined joined: !!store.voice.voicing
}; };
}); });
const [generateToken] = useLazyGetAgoraTokenQuery(); const [generateToken] = useLazyGetAgoraTokenQuery();
@@ -88,25 +90,27 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
await window.VOICE_CLIENT.join(app_id, channel_name, agora_token, uid); await window.VOICE_CLIENT.join(app_id, channel_name, agora_token, uid);
console.table(data); console.table(data);
// Create a local audio track from the microphone audio. // Create a local audio track from the microphone audio.
const localAudioTrack = await AgoraRTC.createMicrophoneAudioTrack();
window.VOICE_TRACK_MAP[loginUid] = await AgoraRTC.createMicrophoneAudioTrack();
// Publish the local audio track in the channel. // Publish the local audio track in the channel.
await window.VOICE_CLIENT.publish(localAudioTrack); await window.VOICE_CLIENT.publish(window.VOICE_TRACK_MAP[loginUid]);
console.log("Publish success!"); console.log("Publish success!");
dispatch(updateVoiceInfo({ dispatch(updateVoicingInfo({
id, id,
context, context,
members: [uid] members: [uid]
})); }));
dispatch(updateJoinStatus(true));
} }
} }
setJoining(false); setJoining(false);
}; };
const leave = async () => { const leave = async () => {
if (window.VOICE_CLIENT) { if (window.VOICE_CLIENT) {
(window.VOICE_TRACK_MAP[loginUid] as IMicrophoneAudioTrack).close();
await window.VOICE_CLIENT.leave(); await window.VOICE_CLIENT.leave();
dispatch(updateJoinStatus(false)); dispatch(updateVoicingInfo(null));
// window.VOICE_TRACK_MAP // window.VOICE_TRACK_MAP[loginUid]?.stop();
// window.VOICE_TRACK_MAP[loginUid] = null;
} }
}; };
return { return {
+1
View File
@@ -20,6 +20,7 @@ interface Props
| "pattern" | "pattern"
| "disabled" | "disabled"
| "minLength" | "minLength"
| "spellCheck"
>, >,
HTMLInputElement HTMLInputElement
> { > {
+10 -3
View File
@@ -14,17 +14,24 @@ type Props = {
const RTCWidget = ({ id, context = "channel" }: Props) => { const RTCWidget = ({ id, context = "channel" }: Props) => {
const { leave } = useVoice({ context, id }); const { leave } = useVoice({ context, id });
const { loginUser, joined } = useAppSelector(store => { return { loginUser: store.authData.user, joined: store.voice.joined }; }); const { loginUser, voicingInfo, channelData, userData } = useAppSelector(store => {
return {
userData: store.users.byId,
channelData: store.channels.byId,
loginUser: store.authData.user,
voicingInfo: store.voice.voicing
};
});
if (!loginUser) return null; if (!loginUser) return null;
return ( return (
<div className='bg-gray-100 dark:bg-gray-900 flex flex-col p-2 rounded-3xl m-3 text-sm'> <div className='bg-gray-100 dark:bg-gray-900 flex flex-col p-2 rounded-3xl m-3 text-sm'>
{joined && {voicingInfo &&
<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 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"> <div className="flex items-center gap-1">
<IconSignal /> <IconSignal />
<div className="flex flex-col"> <div className="flex flex-col">
<span className='text-green-800'>Voice Connected</span> <span className='text-green-800'>Voice Connected</span>
<span className='text-gray-600 dark:text-gray-400 text-xs'>Channel / name</span> <span className='text-gray-600 dark:text-gray-400 text-xs'>{voicingInfo.context == "channel" ? "Channel" : "DM"} / {voicingInfo.context == "channel" ? channelData[voicingInfo.id].name : userData[voicingInfo.id].name}</span>
</div> </div>
</div> </div>
<IconHeadphone onClick={leave} role="button" className="fill-red-600" /> <IconHeadphone onClick={leave} role="button" className="fill-red-600" />
+13 -12
View File
@@ -16,20 +16,20 @@ import { useAppSelector } from "../../../app/store";
import { fromNowTime } from "../../../common/utils"; import { fromNowTime } from "../../../common/utils";
interface IProps { interface IProps {
type?: "user" | "channel"; type?: "dm" | "channel";
id: number; id: number;
mid: number; mid: number;
setDeleteChannelId: (param: number) => void; setDeleteChannelId: (param: number) => void;
setInviteChannelId: (param: number) => void; setInviteChannelId: (param: number) => void;
} }
const Session: FC<IProps> = ({ const Session: FC<IProps> = ({
type = "user", type = "dm",
id, id,
mid, mid,
setDeleteChannelId, setDeleteChannelId,
setInviteChannelId setInviteChannelId
}) => { }) => {
const navPath = type == "user" ? `/chat/dm/${id}` : `/chat/channel/${id}`; const navPath = type == "dm" ? `/chat/dm/${id}` : `/chat/channel/${id}`;
// const { pathname } = useLocation(); // const { pathname } = useLocation();
const isCurrentPath = useMatch(navPath); const isCurrentPath = useMatch(navPath);
const navigate = useNavigate(); const navigate = useNavigate();
@@ -46,7 +46,7 @@ const Session: FC<IProps> = ({
return { size, type, name, url }; return { size, type, name, url };
}); });
addStageFile(filesData); addStageFile(filesData);
navigate(type == "user" ? `/chat/dm/${id}` : `/chat/channel/${id}`); navigate(type == "dm" ? `/chat/dm/${id}` : `/chat/channel/${id}`);
} }
}, },
collect: (monitor) => ({ collect: (monitor) => ({
@@ -62,24 +62,24 @@ const Session: FC<IProps> = ({
mid: number; mid: number;
is_public: boolean; is_public: boolean;
}>(); }>();
const { messageData, userData, channelData, readIndex, loginUid, mids, muted, voiceInfo } = useAppSelector( const { messageData, userData, channelData, readIndex, loginUid, mids, muted, voiceList } = useAppSelector(
(store) => { (store) => {
return { return {
voiceInfo: store.voice.voicing, voiceList: store.voice.list,
mids: type == "user" ? store.userMessage.byId[id] : store.channelMessage[id], mids: type == "dm" ? store.userMessage.byId[id] : store.channelMessage[id],
loginUid: store.authData.user?.uid || 0, loginUid: store.authData.user?.uid || 0,
readIndex: readIndex:
type == "user" ? store.footprint.readUsers[id] : store.footprint.readChannels[id], type == "dm" ? store.footprint.readUsers[id] : store.footprint.readChannels[id],
messageData: store.message, messageData: store.message,
userData: store.users.byId, userData: store.users.byId,
channelData: store.channels.byId, channelData: store.channels.byId,
muted: type == "user" ? store.footprint.muteUsers[id] : store.footprint.muteChannels[id] muted: type == "dm" ? store.footprint.muteUsers[id] : store.footprint.muteChannels[id]
}; };
} }
); );
useEffect(() => { useEffect(() => {
const tmp = type == "user" ? userData[id] : channelData[id]; const tmp = type == "dm" ? userData[id] : channelData[id];
if (!tmp) return; if (!tmp) return;
if ("avatar" in tmp) { if ("avatar" in tmp) {
// user // user
@@ -100,6 +100,7 @@ const Session: FC<IProps> = ({
messageData, messageData,
loginUid loginUid
}); });
const isVoicing = type == "channel" && voiceList.findIndex(item => item.id == id) > -1;
return ( return (
<li className="session"> <li className="session">
<ContextMenu <ContextMenu
@@ -118,7 +119,7 @@ const Session: FC<IProps> = ({
onContextMenu={handleContextMenuEvent} onContextMenu={handleContextMenuEvent}
> >
<div className="flex shrink-0 relative"> <div className="flex shrink-0 relative">
{type == "user" ? ( {type == "dm" ? (
<User avatarSize={40} compact interactive={false} uid={id} /> <User avatarSize={40} compact interactive={false} uid={id} />
) : ( ) : (
<Avatar <Avatar
@@ -130,7 +131,7 @@ const Session: FC<IProps> = ({
src={icon} src={icon}
/> />
)} )}
{type == "channel" && voiceInfo.id == id && <IconVoicing className="-top-0.5 -right-0.5 absolute w-[18px] h-[18px]" />} {isVoicing && <IconVoicing className="-top-0.5 -right-0.5 absolute w-[18px] h-[18px]" />}
</div> </div>
<div className="w-full flex flex-col justify-between overflow-hidden"> <div className="w-full flex flex-col justify-between overflow-hidden">
<div className="flex items-center justify-between "> <div className="flex items-center justify-between ">
+3 -3
View File
@@ -6,7 +6,7 @@ import DeleteChannelConfirmModal from "../../settingChannel/DeleteConfirmModal";
import InviteModal from "../../../common/component/InviteModal"; import InviteModal from "../../../common/component/InviteModal";
import { useAppSelector } from "../../../app/store"; import { useAppSelector } from "../../../app/store";
export interface ChatSession { export interface ChatSession {
type: "user" | "channel"; type: "dm" | "channel";
id: number; id: number;
mid: number; mid: number;
unread: number; unread: number;
@@ -46,11 +46,11 @@ const SessionList: FC<Props> = ({ tempSession }) => {
// console.log("adddd", id); // console.log("adddd", id);
const mids = userMessage[id]; const mids = userMessage[id];
if (!mids || mids.length == 0) { if (!mids || mids.length == 0) {
return { unreads: 0, id, type: "user" }; return { unreads: 0, id, type: "dm" };
} }
// 先转换成数字,再排序 // 先转换成数字,再排序
const mid = [...mids].sort((a, b) => +a - +b).pop(); const mid = [...mids].sort((a, b) => +a - +b).pop();
return { type: "user", id, mid }; return { type: "dm", id, mid };
}); });
const temps = [...(cSessions as ChatSession[]), ...(uSessions as ChatSession[])].sort((a, b) => { const temps = [...(cSessions as ChatSession[]), ...(uSessions as ChatSession[])].sort((a, b) => {
const { mid: aMid = 0 } = a; const { mid: aMid = 0 } = a;
+1 -1
View File
@@ -44,7 +44,7 @@ function ChatPage() {
mid: 0, mid: 0,
unread: 0, unread: 0,
id: +user_id, id: +user_id,
type: "user" as const type: "dm" as const
} }
: undefined; : undefined;
// console.log("temp uid", tmpUid); // console.log("temp uid", tmpUid);
+5 -1
View File
@@ -54,8 +54,8 @@ export default function ConfigAgora() {
<div className="input"> <div className="input">
<Label htmlFor="project_id">Project ID</Label> <Label htmlFor="project_id">Project ID</Label>
<Input <Input
spellCheck={false}
disabled={!enabled} disabled={!enabled}
// type={"number"}
data-type="project_id" data-type="project_id"
onChange={handleChange} onChange={handleChange}
value={project_id} value={project_id}
@@ -66,6 +66,7 @@ export default function ConfigAgora() {
<div className="input"> <div className="input">
<Label htmlFor="app_id">App ID</Label> <Label htmlFor="app_id">App ID</Label>
<Input <Input
spellCheck={false}
disabled={!enabled} disabled={!enabled}
data-type="app_id" data-type="app_id"
onChange={handleChange} onChange={handleChange}
@@ -77,6 +78,7 @@ export default function ConfigAgora() {
<div className="input"> <div className="input">
<Label htmlFor="app_certificate">APP Certificate</Label> <Label htmlFor="app_certificate">APP Certificate</Label>
<Input <Input
spellCheck={false}
disabled={!enabled} disabled={!enabled}
data-type="app_certificate" data-type="app_certificate"
onChange={handleChange} onChange={handleChange}
@@ -88,6 +90,7 @@ export default function ConfigAgora() {
<div className="input"> <div className="input">
<Label htmlFor="rtm_key">RTM Key</Label> <Label htmlFor="rtm_key">RTM Key</Label>
<Textarea <Textarea
spellCheck={false}
disabled={!enabled} disabled={!enabled}
data-type="rtm_key" data-type="rtm_key"
onChange={handleChange} onChange={handleChange}
@@ -99,6 +102,7 @@ export default function ConfigAgora() {
<div className="input"> <div className="input">
<Label htmlFor="rtm_secret">RTM Secret</Label> <Label htmlFor="rtm_secret">RTM Secret</Label>
<Textarea <Textarea
spellCheck={false}
disabled={!enabled} disabled={!enabled}
data-type="rtm_secret" data-type="rtm_secret"
onChange={handleChange} onChange={handleChange}
+2 -2
View File
@@ -1,4 +1,4 @@
import { IAgoraRTCClient, IRemoteAudioTrack } from "agora-rtc-sdk-ng"; import { IAgoraRTCClient, IMicrophoneAudioTrack, IRemoteAudioTrack } from "agora-rtc-sdk-ng";
interface BeforeInstallPromptEvent extends Event { interface BeforeInstallPromptEvent extends Event {
/** /**
@@ -33,7 +33,7 @@ export declare global {
CACHE: { [key: string]: typeof localforage | undefined }; CACHE: { [key: string]: typeof localforage | undefined };
VOICE_CLIENT?: IAgoraRTCClient; VOICE_CLIENT?: IAgoraRTCClient;
VOICE_TRACK_MAP: { VOICE_TRACK_MAP: {
[key: number]: IRemoteAudioTrack | undefined [key: number]: IRemoteAudioTrack | IMicrophoneAudioTrack | undefined | null
} }
} }
interface WindowEventMap { interface WindowEventMap {