refactor: voice operations
This commit is contained in:
@@ -24,7 +24,7 @@ import {
|
||||
} from "@/types/server";
|
||||
import { Channel } from "@/types/channel";
|
||||
import { ContentTypeKey } from "@/types/message";
|
||||
import { upsertVoiceList } from "../slices/voice";
|
||||
import { updateCallInfo, upsertVoiceList } from "../slices/voice";
|
||||
|
||||
const defaultExpireDuration = 2 * 24 * 60 * 60;
|
||||
|
||||
@@ -119,8 +119,9 @@ export const serverApi = createApi({
|
||||
}),
|
||||
getAgoraChannels: builder.query<AgoraVoicingListResponse, { page_no: number, page_size: number }>({
|
||||
query: (param = { page_no: 0, page_size: 100 }) => ({ url: `/admin/agora/channel/${param.page_no}/${param.page_size}` }),
|
||||
async onQueryStarted(data, { dispatch, queryFulfilled }) {
|
||||
async onQueryStarted(data, { dispatch, queryFulfilled, getState }) {
|
||||
try {
|
||||
const { voice: { callingFrom }, authData } = getState() as RootState;
|
||||
const { data: resp } = await queryFulfilled;
|
||||
const { success } = resp;
|
||||
if (success) {
|
||||
@@ -136,6 +137,12 @@ export const serverApi = createApi({
|
||||
};
|
||||
});
|
||||
dispatch(upsertVoiceList(arr));
|
||||
const hasMyself = arr.some(data => data.context === "dm" && data.id == authData?.user?.uid);
|
||||
const sendByMe = callingFrom && callingFrom === authData?.user?.uid;
|
||||
// reset dm call setting
|
||||
if (callingFrom && !sendByMe && !hasMyself) {
|
||||
dispatch(updateCallInfo({ from: 0, to: 0, calling: false }));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
console.error("get voice list error");
|
||||
|
||||
@@ -6,7 +6,7 @@ import { resetAuthData } from "./auth.data";
|
||||
import { ChatContext } from "@/types/common";
|
||||
|
||||
type ChannelAside = "members" | "voice" | "voice_fullscreen" | null;
|
||||
type DMAside = "voice" | null;
|
||||
type DMAside = "voice" | "voice_fullscreen" | null;
|
||||
export interface State {
|
||||
og: { [url: string]: OG }
|
||||
usersVersion: number;
|
||||
|
||||
+54
-12
@@ -35,19 +35,18 @@ export type VoiceInfo = {
|
||||
channelName: string,
|
||||
memberCount: number
|
||||
} & VoiceBasicInfo
|
||||
export type DeviceInfo = Pick<MediaDeviceInfo, "deviceId" | "groupId" | "kind" | "label">;
|
||||
interface State {
|
||||
callingFrom: number,
|
||||
callingTo: number,
|
||||
calling: boolean,
|
||||
voicing: VoicingInfo | null,
|
||||
voicingMembers: VoicingMembers,
|
||||
list: VoiceInfo[]
|
||||
list: VoiceInfo[],
|
||||
devices: DeviceInfo[],
|
||||
audioInputDeviceId: string,
|
||||
videoInputDeviceId: string,
|
||||
}
|
||||
// const initialInfo = {
|
||||
// context: "channel" as const,
|
||||
// id: 0,
|
||||
// members: []
|
||||
// };
|
||||
const initialState: State = {
|
||||
calling: false,
|
||||
callingFrom: 0,
|
||||
@@ -57,11 +56,11 @@ const initialState: State = {
|
||||
ids: [],
|
||||
byId: {}
|
||||
},
|
||||
list: []
|
||||
list: [],
|
||||
devices: [],
|
||||
audioInputDeviceId: "",
|
||||
videoInputDeviceId: ""
|
||||
};
|
||||
|
||||
|
||||
|
||||
const voiceSlice = createSlice({
|
||||
name: "voice",
|
||||
initialState,
|
||||
@@ -74,9 +73,30 @@ const voiceSlice = createSlice({
|
||||
state.calling = calling;
|
||||
}
|
||||
},
|
||||
updateDevices(state, { payload }: PayloadAction<DeviceInfo[]>) {
|
||||
state.devices = payload;
|
||||
if (payload.length > 0) {
|
||||
const audioInputDevice = payload.find(v => v.kind == "audioinput" && v.deviceId == "default");
|
||||
const videoInputDevice = payload.find(v => v.kind == "videoinput");
|
||||
if (audioInputDevice) {
|
||||
state.audioInputDeviceId = audioInputDevice.deviceId;
|
||||
}
|
||||
if (videoInputDevice) {
|
||||
state.videoInputDeviceId = videoInputDevice.deviceId;
|
||||
}
|
||||
}
|
||||
},
|
||||
updateCalling(state, { payload }: PayloadAction<boolean>) {
|
||||
state.calling = payload;
|
||||
},
|
||||
updateSelectDeviceId(state, { payload }: PayloadAction<{ type: "video" | "audio", value: string }>) {
|
||||
const { type, value } = payload;
|
||||
if (type == "video") {
|
||||
state.videoInputDeviceId = value;
|
||||
} else {
|
||||
state.audioInputDeviceId = value;
|
||||
}
|
||||
},
|
||||
updateVoicingInfo(state, { payload }: PayloadAction<VoicingInfo | null>) {
|
||||
if (payload) {
|
||||
state.voicing = { ...(state.voicing ?? {}), ...payload };
|
||||
@@ -84,7 +104,7 @@ const voiceSlice = createSlice({
|
||||
const loginUid = localStorage.getItem(KEY_UID) ?? 0;
|
||||
const idx = state.voicingMembers.ids.findIndex((uid) => uid == loginUid);
|
||||
if (idx > -1) {
|
||||
state.voicingMembers.byId[+loginUid] = payload;
|
||||
// state.voicingMembers.byId[+loginUid] = payload;
|
||||
Object.keys(payload).forEach((key) => {
|
||||
switch (key) {
|
||||
case "video":
|
||||
@@ -92,8 +112,17 @@ const voiceSlice = createSlice({
|
||||
break;
|
||||
case "muted":
|
||||
state.voicingMembers.byId[+loginUid].muted = payload.muted;
|
||||
if (!payload.muted) {
|
||||
state.voicingMembers.byId[+loginUid].deafen = false;
|
||||
if (state.voicing) {
|
||||
state.voicing.deafen = false;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "deafen":
|
||||
if (state.voicing) {
|
||||
state.voicing.muted = payload.deafen;
|
||||
}
|
||||
state.voicingMembers.byId[+loginUid].deafen = payload.deafen;
|
||||
state.voicingMembers.byId[+loginUid].muted = payload.deafen;
|
||||
break;
|
||||
@@ -199,5 +228,18 @@ const voiceSlice = createSlice({
|
||||
}
|
||||
});
|
||||
|
||||
export const { updateCalling, updateCallInfo, updatePin, updateConnectionState, addVoiceMember, removeVoiceMember, upsertVoiceList, updateVoicingInfo, updateVoicingNetworkQuality, updateVoicingMember } = voiceSlice.actions;
|
||||
export const {
|
||||
updateSelectDeviceId,
|
||||
updateDevices,
|
||||
updateCalling,
|
||||
updateCallInfo,
|
||||
updatePin,
|
||||
updateConnectionState,
|
||||
addVoiceMember,
|
||||
removeVoiceMember,
|
||||
upsertVoiceList,
|
||||
updateVoicingInfo,
|
||||
updateVoicingNetworkQuality,
|
||||
updateVoicingMember
|
||||
} = voiceSlice.actions;
|
||||
export default voiceSlice.reducer;
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { MouseEvent, SetStateAction, useState, Dispatch } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Tippy from '@tippyjs/react';
|
||||
|
||||
import Tooltip from '../Tooltip';
|
||||
import IconMicOff from '@/assets/icons/mic.off.svg';
|
||||
import IconMic from '@/assets/icons/mic.on.svg';
|
||||
import IconCameraOff from '@/assets/icons/camera.off.svg';
|
||||
import IconArrow from '@/assets/icons/arrow.down.mini.svg';
|
||||
import IconCamera from '@/assets/icons/camera.svg';
|
||||
import IconFullscreen from '@/assets/icons/fullscreen.svg';
|
||||
import IconScreen from '@/assets/icons/share.screen.svg';
|
||||
import IconCheck from '@/assets/icons/check.sign.svg';
|
||||
import IconCallOff from '@/assets/icons/call.off.svg';
|
||||
|
||||
import { ChatContext } from '@/types/common';
|
||||
import useVoice from './useVoice';
|
||||
// import { updateChannelVisibleAside, updateDMVisibleAside } from '@/app/slices/footprint';
|
||||
import { DeviceInfo, updateSelectDeviceId } from '@/app/slices/voice';
|
||||
|
||||
type DeviceType = "audio" | "video";
|
||||
// https://docportal.shengwang.cn/cn/video-call-4.x/test_switch_device_web_ng?platform=Web
|
||||
const DeviceList = ({ type, visible, setVisible, devices, selected }: {
|
||||
type: DeviceType,
|
||||
visible: VisibleType,
|
||||
setVisible: Dispatch<SetStateAction<VisibleType>>,
|
||||
devices: DeviceInfo[],
|
||||
selected: string
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
// const { t } = useTranslation("chat");
|
||||
const toggleVisible = (evt: MouseEvent<HTMLDivElement>) => {
|
||||
evt.stopPropagation();
|
||||
setVisible((prev: VisibleType) => prev == type ? "" : type);
|
||||
};
|
||||
const handleSelect = (evt: MouseEvent<HTMLLIElement>) => {
|
||||
evt.stopPropagation();
|
||||
const { deviceId = "" } = evt.currentTarget.dataset;
|
||||
if (selected == deviceId) return;
|
||||
dispatch(updateSelectDeviceId({ type, value: deviceId }));
|
||||
};
|
||||
return <Tippy
|
||||
onClickOutside={() => setVisible("")}
|
||||
interactive
|
||||
popperOptions={{ strategy: "fixed" }}
|
||||
visible={visible == type}
|
||||
placement="top-start"
|
||||
content={<div className="p-3 bg-white dark:bg-gray-800 overflow-auto rounded-lg flex flex-col items-start relative drop-shadow">
|
||||
<ul className="w-full flex flex-col gap-4">
|
||||
{devices.map(({ deviceId, kind, groupId, label }) => {
|
||||
return (
|
||||
<li data-device-id={deviceId} key={label} className="relative rounded-sm cursor-pointer flex items-center justify-between gap-4 text-gray-500 hover:text-gray-900 dark:text-gray-300 hover:dark:text-gray-100 font-semibold text-sm whitespace-nowrap"
|
||||
onClick={handleSelect}>
|
||||
{label}
|
||||
{selected == deviceId && <IconCheck className="shrink-0" />}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>}
|
||||
>
|
||||
<div onClick={toggleVisible} className="p-1 absolute rounded-sm top-0.5 right-0.5 hover:dark:bg-gray-500/50" role='button' >
|
||||
<IconArrow className={clsx("w-2 fill-gray-600 dark:fill-white transition-all", visible == type && "rotate-180")} />
|
||||
</div>
|
||||
</Tippy>;
|
||||
};
|
||||
type VisibleType = "audio" | "video" | "";
|
||||
type Props = {
|
||||
mode?: "channel" | "dm" | "fullscreen",
|
||||
id: number,
|
||||
context: ChatContext
|
||||
}
|
||||
|
||||
const Operations = ({ id, context, mode = "channel" }: Props) => {
|
||||
const [panelVisible, setPanelVisible] = useState<VisibleType>("");
|
||||
const { enterFullscreen, voicingInfo, leave, setMute, closeCamera, openCamera, startShareScreen, stopShareScreen, audioInputDevices, audioOutputDevices, videoInputDevices, videoInputDeviceId, audioInputDeviceId } = useVoice({ id, context });
|
||||
const { t } = useTranslation("chat");
|
||||
if (!voicingInfo) return null;
|
||||
const { muted, video, shareScreen } = voicingInfo;
|
||||
const baseButtonClass = clsx("flex-center py-2 px-3 rounded bg-gray-100 dark:bg-gray-900 relative");
|
||||
const baseIconClass = clsx("w-[25px] h-6 m-auto fill-gray-700 dark:fill-gray-300");
|
||||
return <>
|
||||
<Tooltip disabled={panelVisible == "audio"} tip={muted ? t("unmute") : t("mute")} placement="top">
|
||||
<button onClick={setMute.bind(null, !muted)} className={baseButtonClass}>
|
||||
{muted ? <IconMicOff className={baseIconClass} /> : <IconMic className={baseIconClass} />}
|
||||
<DeviceList
|
||||
visible={panelVisible}
|
||||
setVisible={setPanelVisible}
|
||||
type='audio'
|
||||
devices={audioInputDevices}
|
||||
selected={audioInputDeviceId}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip disabled={panelVisible == "video"} tip={video ? t("camera_off") : t("camera_on")} placement="top">
|
||||
<button onClick={video ? closeCamera : openCamera} className={baseButtonClass}>
|
||||
{video ? <IconCamera className={baseIconClass} /> : <IconCameraOff className={baseIconClass} />}
|
||||
<DeviceList
|
||||
visible={panelVisible}
|
||||
setVisible={setPanelVisible}
|
||||
type='video'
|
||||
devices={videoInputDevices}
|
||||
selected={videoInputDeviceId}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip tip={"Share Screen"} placement="top">
|
||||
<button onClick={shareScreen ? stopShareScreen : startShareScreen} className={clsx("py-2 px-3 rounded", shareScreen ? "bg-green-700" : "bg-gray-100 dark:bg-gray-900")}>
|
||||
<IconScreen className={clsx("w-6 h-6 dark:fill-gray-300", shareScreen ? "fill-gray-200" : "fill-gray-800")} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
{mode !== "fullscreen" && <Tooltip tip={"Fullscreen"} placement="top">
|
||||
<button onClick={enterFullscreen.bind(null, undefined)} className={baseButtonClass}>
|
||||
<IconFullscreen className={baseIconClass} />
|
||||
</button>
|
||||
</Tooltip>}
|
||||
{mode !== "dm" && <Tooltip tip={t("leave_voice")} placement="top" >
|
||||
<button onClick={leave} className={clsx('py-2 px-3 rounded bg-red-600 hover:bg-red-700', mode !== "fullscreen" && "col-span-4")}>
|
||||
<IconCallOff className="m-auto w-[25px] h-6" />
|
||||
</button>
|
||||
</Tooltip>}
|
||||
</>;
|
||||
};
|
||||
|
||||
export default Operations;
|
||||
@@ -2,7 +2,7 @@ import AgoraRTC from 'agora-rtc-sdk-ng';
|
||||
import { memo, useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useGetAgoraChannelsQuery, useGetAgoraStatusQuery, useLazyGetAgoraUsersByChannelQuery } from '../../app/services/server';
|
||||
import { addVoiceMember, removeVoiceMember, updateCallInfo, updateConnectionState, updateVoicingMember, updateVoicingNetworkQuality } from '../../app/slices/voice';
|
||||
import { addVoiceMember, removeVoiceMember, updateCallInfo, updateConnectionState, updateDevices, updateVoicingMember, updateVoicingNetworkQuality } from '../../app/slices/voice';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import { playAgoraVideo } from '../../utils';
|
||||
import useVoice from './useVoice';
|
||||
@@ -40,12 +40,12 @@ const Voice = () => {
|
||||
// Subscribe to the remote user when the SDK triggers the "user-published" event.
|
||||
await agoraEngine.subscribe(user, mediaType);
|
||||
console.log(user, " has published at the channel");
|
||||
if (mediaType == "audio") {
|
||||
if (mediaType == "audio" && user.hasAudio) {
|
||||
// 播放远端音频
|
||||
user.audioTrack?.play();
|
||||
window.VOICE_TRACK_MAP[+user.uid] = user.audioTrack;
|
||||
}
|
||||
if (mediaType == "video") {
|
||||
if (mediaType == "video" && user.hasVideo) {
|
||||
// const label = user.videoTrack?.getMediaStreamTrack()?.label;
|
||||
// console.log("labell", label);
|
||||
|
||||
@@ -56,6 +56,7 @@ const Voice = () => {
|
||||
// // // 远端用户停止共享屏幕
|
||||
// dispatch(updateVoicingMember({ uid: +user.uid, info: { shareScreen: true } }));
|
||||
// }
|
||||
dispatch(updateVoicingMember({ uid: +user.uid, info: { video: true } }));
|
||||
window.VIDEO_TRACK_MAP[+user.uid] = user.videoTrack;
|
||||
playAgoraVideo(+user.uid);
|
||||
}
|
||||
@@ -84,10 +85,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(updateCallInfo({ from: 0 }));
|
||||
}
|
||||
// if (voicingInfo && voicingInfo.context == "dm") {
|
||||
// // 有人离开,就断开
|
||||
// dispatch(updateCallInfo({ from: 0 }));
|
||||
// }
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -139,9 +140,21 @@ const Voice = () => {
|
||||
});
|
||||
// 有新用户加入
|
||||
agoraEngine.on("user-joined", async (user) => {
|
||||
console.log(user.uid, agoraEngine.channelName, " has joined the channel");
|
||||
console.log(user.uid, " has joined the channel", user);
|
||||
dispatch(addVoiceMember(+user.uid));
|
||||
});
|
||||
AgoraRTC.onMicrophoneChanged = (info) => {
|
||||
console.log("onMicrophoneChanged", info);
|
||||
};
|
||||
AgoraRTC.getDevices().then((devices) => {
|
||||
console.log("devices", devices);
|
||||
dispatch(updateDevices(devices.map((d) => {
|
||||
const { deviceId, groupId, label, kind } = d;
|
||||
return { deviceId, groupId, label, kind };
|
||||
})));
|
||||
}).catch((err) => {
|
||||
console.log("err", err);
|
||||
});
|
||||
window.VOICE_CLIENT = agoraEngine;
|
||||
};
|
||||
const handlePageUnload = (evt: BeforeUnloadEvent) => {
|
||||
@@ -172,7 +185,7 @@ const Voice = () => {
|
||||
// 呼叫的人在频道里
|
||||
getUsersByChannel(channelName).then(resp => {
|
||||
const [uid] = resp.data ?? [];
|
||||
if (uid) {
|
||||
if (uid && uid != loginUid) {
|
||||
dispatch(updateCallInfo({ from: uid, to: id }));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import AgoraRTC, { ICameraVideoTrack, IMicrophoneAudioTrack } from 'agora-rtc-sdk-ng';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { ShareScreenTrack } from '../../types/global';
|
||||
import AudioJoin from '@/assets/join.wav';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { useGenerateAgoraTokenMutation } from '../../app/services/server';
|
||||
import { updateChannelVisibleAside, updateDMVisibleAside } from '../../app/slices/footprint';
|
||||
|
||||
@@ -19,10 +20,15 @@ type VoiceProps = {
|
||||
const audioJoin = new Audio(AudioJoin);
|
||||
const useVoice = ({ id, context = "channel" }: VoiceProps) => {
|
||||
const dispatch = useDispatch();
|
||||
const { voicingInfo, loginUid } = useAppSelector(store => {
|
||||
const { voicingInfo, loginUid, audioInputDevices, audioOutputDevices, videoInputDevices, audioInputDeviceId, videoInputDeviceId } = useAppSelector(store => {
|
||||
return {
|
||||
loginUid: store.authData.user?.uid ?? 0,
|
||||
voicingInfo: store.voice.voicing
|
||||
voicingInfo: store.voice.voicing,
|
||||
audioInputDevices: store.voice.devices.filter(d => d.kind == "audioinput") ?? [],
|
||||
audioOutputDevices: store.voice.devices.filter(d => d.kind == "audiooutput") ?? [],
|
||||
videoInputDevices: store.voice.devices.filter(d => d.kind == "videoinput") ?? [],
|
||||
audioInputDeviceId: store.voice.audioInputDeviceId,
|
||||
videoInputDeviceId: store.voice.videoInputDeviceId,
|
||||
};
|
||||
});
|
||||
const [generateToken] = useGenerateAgoraTokenMutation();
|
||||
@@ -154,8 +160,10 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
|
||||
// 进入全屏并Pin自己
|
||||
if (context == "channel") {
|
||||
dispatch(updateChannelVisibleAside({ id, aside: "voice_fullscreen" }));
|
||||
dispatch(updatePin({ uid: loginUid, action: "pin" }));
|
||||
} else {
|
||||
dispatch(updateDMVisibleAside({ id, aside: "voice_fullscreen" }));
|
||||
}
|
||||
dispatch(updatePin({ uid: loginUid, action: "pin" }));
|
||||
// 监听屏幕共享结束事件
|
||||
if ("close" in localVideoTrack) {
|
||||
localVideoTrack.getMediaStreamTrack().onended = () => {
|
||||
@@ -232,6 +240,23 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
|
||||
}
|
||||
dispatch(updateVoicingInfo({ deafen, id, context }));
|
||||
};
|
||||
const enterFullscreen = (uid?: number) => {
|
||||
if (context == "channel") {
|
||||
dispatch(updateChannelVisibleAside({ id, aside: "voice_fullscreen" }));
|
||||
} else {
|
||||
dispatch(updateDMVisibleAside({ id, aside: "voice_fullscreen" }));
|
||||
}
|
||||
if (uid) {
|
||||
dispatch(updatePin({ uid, action: "pin" }));
|
||||
}
|
||||
};
|
||||
const exitFullscreen = () => {
|
||||
if (context == "channel") {
|
||||
dispatch(updateChannelVisibleAside({ id, aside: "voice" }));
|
||||
} else {
|
||||
dispatch(updateDMVisibleAside({ id, aside: null }));
|
||||
}
|
||||
};
|
||||
const joinedAtThisContext = voicingInfo ? (voicingInfo.id == id && voicingInfo.context == context) : false;
|
||||
return {
|
||||
setMute,
|
||||
@@ -247,6 +272,13 @@ const useVoice = ({ id, context = "channel" }: VoiceProps) => {
|
||||
closeCamera,
|
||||
startShareScreen,
|
||||
stopShareScreen,
|
||||
enterFullscreen,
|
||||
exitFullscreen,
|
||||
audioInputDevices,
|
||||
audioOutputDevices,
|
||||
videoInputDevices,
|
||||
audioInputDeviceId,
|
||||
videoInputDeviceId
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// import React from 'react'
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
// import { useTranslation } from "react-i18next";
|
||||
import { useDispatch } from "react-redux";
|
||||
import clsx from "clsx";
|
||||
|
||||
@@ -9,33 +9,38 @@ import Tooltip from "@/components/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, updateCallInfo } from "@/app/slices/voice";
|
||||
// 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 { updateCallInfo } from "@/app/slices/voice";
|
||||
import { useVoice } from "@/components/Voice";
|
||||
import { playAgoraVideo } from "@/utils";
|
||||
import Avatar from "@/components/Avatar";
|
||||
import Operations from "@/components/Voice/Operations";
|
||||
|
||||
type VoicingMember = {
|
||||
id: number,
|
||||
muted: boolean,
|
||||
video: boolean,
|
||||
speaking: boolean,
|
||||
name: string,
|
||||
avatar?: string
|
||||
}
|
||||
type BlockProps = {
|
||||
onlyToSelf: boolean,
|
||||
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;
|
||||
const VoicingBlocks = ({ onlyToSelf, sendByMe, connected, from, to }: BlockProps) => {
|
||||
const blocks = [from, to].map(({ id, speaking, name, avatar, video, muted }, idx) => {
|
||||
const showWaiting = idx == 1 && !connected && !sendByMe && !onlyToSelf;
|
||||
const showToWaiting = idx == 1 && !connected && sendByMe;
|
||||
return <div key={id} className={clsx("relative flex-center", video ? "w-80 h-60 overflow-hidden rounded" : "")}>
|
||||
const isMyself = sendByMe ? idx == 0 : idx == 1;
|
||||
const hiddenFrom = onlyToSelf && idx == 0;
|
||||
return <div key={id} className={clsx("group relative flex-center", video ? "w-[500px] h-[280px] overflow-hidden rounded" : "", hiddenFrom && "hidden")}>
|
||||
<div className={clsx("w-20 h-20 flex shrink-0 relative transition-opacity", showToWaiting && "animate-pulse", showWaiting && "opacity-40")}>
|
||||
{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-500 animate-speaking", "w-[86px] h-[86px]")}></div>}
|
||||
{showToWaiting && <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>}
|
||||
@@ -51,6 +56,12 @@ const VoicingBlocks = ({ sendByMe, connected, from, to }: BlockProps) => {
|
||||
<div className="z-30 absolute left-0 top-0 w-full h-full" id={`CAMERA_${id}`}>
|
||||
{/* camera video */}
|
||||
</div>
|
||||
{video && <span className={clsx("text-gray-300 bg-black/50 rounded absolute z-40", "left-1 bottom-1 py-1 px-2 text-xs")} title={name}>
|
||||
{name}
|
||||
</span>}
|
||||
{muted && !isMyself && <span className={clsx("bg-black/50 rounded absolute z-40 right-1 bottom-1 p-1", video && "invisible group-hover:visible")} title={name}>
|
||||
<IconMicOff className="w-4 h-4 fill-gray-300" />
|
||||
</span>}
|
||||
</div>;
|
||||
}
|
||||
);
|
||||
@@ -61,9 +72,9 @@ type Props = {
|
||||
}
|
||||
const DMVoice = ({ uid }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation("chat");
|
||||
// const { t } = useTranslation("chat");
|
||||
const { voice: {
|
||||
callingFrom, callingTo, voicing: voicingInfo, voicingMembers
|
||||
callingFrom, callingTo, voicingMembers
|
||||
}, userData, loginUser } = useAppSelector(store => {
|
||||
return {
|
||||
voice: store.voice,
|
||||
@@ -71,7 +82,7 @@ const DMVoice = ({ uid }: Props) => {
|
||||
loginUser: store.authData.user
|
||||
};
|
||||
});
|
||||
const { leave, joinVoice, joining, setMute, closeCamera, openCamera, startShareScreen, stopShareScreen } = useVoice({ id: callingTo, context: "dm" });
|
||||
const { leave, joinVoice, joining } = useVoice({ id: callingTo, context: "dm" });
|
||||
useEffect(() => {
|
||||
const ids = voicingMembers.ids;
|
||||
ids.forEach(id => {
|
||||
@@ -83,9 +94,10 @@ const DMVoice = ({ uid }: Props) => {
|
||||
const { name: fromUsername, avatar: fromAvatar } = userData[callingFrom];
|
||||
const { name: toUsername, avatar: toAvatar, uid: toUid } = userData[callingTo];
|
||||
const sendByMe = loginUser?.uid !== toUid;
|
||||
const onlyToSelf = voicingMembers.ids.length == 1 && voicingMembers.ids[0] == callingTo;
|
||||
const handleCancel = () => {
|
||||
console.log('cancel');
|
||||
if (sendByMe || voicingMembers.ids.length == 2) {
|
||||
if (sendByMe || voicingMembers.ids.length == 2 || onlyToSelf) {
|
||||
leave();
|
||||
}
|
||||
dispatch(updateCallInfo({ from: 0, to: 0 }));
|
||||
@@ -94,21 +106,23 @@ const DMVoice = ({ uid }: Props) => {
|
||||
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 { muted, shareScreen, video } = voicingInfo ?? {} as VoicingInfo;
|
||||
const { speakingVolume: toSpeakingVol = 0, muted: toMuted = false, 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 { speakingVolume: fromSpeakingVol = 0, muted: fromMuted = false, 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={{
|
||||
<div className="flex items-center gap-4">
|
||||
<VoicingBlocks onlyToSelf={onlyToSelf} sendByMe={sendByMe} connected={connected} from={{
|
||||
id: callingFrom,
|
||||
muted: fromMuted,
|
||||
video: fromVideo || fromScreen,
|
||||
speaking: fromSpeaking,
|
||||
name: fromUsername,
|
||||
avatar: fromAvatar
|
||||
}} to={{
|
||||
muted: toMuted,
|
||||
id: callingTo,
|
||||
video: toVideo || toScreen,
|
||||
speaking: toSpeaking,
|
||||
@@ -117,34 +131,20 @@ const DMVoice = ({ uid }: Props) => {
|
||||
}} />
|
||||
</div>
|
||||
<div className={clsx("flex gap-3", connected ? "h-full items-end" : "")}>
|
||||
{(sendByMe || connected) && <Tooltip tip={"Disconnect"} placement="top">
|
||||
<button onClick={handleCancel} className='flex-center bg-red-600 hover:bg-red-700 py-2 px-3 rounded-lg'>
|
||||
{(sendByMe || connected || onlyToSelf) && <Tooltip tip={"Leave"} placement="top">
|
||||
<button onClick={handleCancel} className='flex-center bg-red-600 hover:bg-red-700 py-2 px-3 rounded'>
|
||||
<IconCallOff className="w-6 h-6" />
|
||||
</button>
|
||||
</Tooltip>}
|
||||
{!sendByMe && !connected &&
|
||||
{!sendByMe && !connected && !onlyToSelf &&
|
||||
<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'>
|
||||
<button disabled={joining} onClick={handleAnswer} className='flex-center bg-green-600 hover:bg-green-700 py-2 px-3 rounded'>
|
||||
<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>
|
||||
<Operations id={callingTo} context="dm" mode="dm" />
|
||||
|
||||
</>}
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,6 @@ import IconMicOff from '@/assets/icons/mic.off.svg';
|
||||
import IconCallOff from '@/assets/icons/headphone.svg';
|
||||
import IconSoundOn from '@/assets/icons/sound.on.svg';
|
||||
import IconSoundOff from '@/assets/icons/sound.off.svg';
|
||||
// import IconCameraOff from '@/assets/icons/camera.off.svg';
|
||||
import IconCamera from '@/assets/icons/camera.svg';
|
||||
import IconScreen from '@/assets/icons/share.screen.svg';
|
||||
import { useVoice } from '../../components/Voice';
|
||||
@@ -24,8 +23,10 @@ type Props = {
|
||||
const RTCWidget = ({ id, context = "channel" }: Props) => {
|
||||
const { t } = useTranslation("chat");
|
||||
const { leave, voicingInfo, setMute, setDeafen, joining = true, openCamera, closeCamera, startShareScreen, stopShareScreen } = useVoice({ context, id });
|
||||
const { loginUser, channelData, userData } = useAppSelector(store => {
|
||||
const { callFrom, callTo, loginUser, channelData, userData } = useAppSelector(store => {
|
||||
return {
|
||||
callFrom: store.voice.callingFrom,
|
||||
callTo: store.voice.callingTo,
|
||||
userData: store.users.byId,
|
||||
channelData: store.channels.byId,
|
||||
loginUser: store.authData.user,
|
||||
@@ -35,6 +36,7 @@ const RTCWidget = ({ id, context = "channel" }: Props) => {
|
||||
// const name = voicingInfo.context == "channel" ? channelData[voicingInfo.id]?.name : userData[voicingInfo.id]?.name;
|
||||
// if (!name) return null;
|
||||
const isReConnecting = voicingInfo.connectionState == "RECONNECTING";
|
||||
const targetDMUsername = callFrom == loginUser.uid ? userData[callTo]?.name : userData[callFrom]?.name;
|
||||
return (
|
||||
<div className='bg-gray-100 dark:bg-gray-900 flex flex-col p-2 rounded-3xl m-3 mb-4 text-sm'>
|
||||
<div className="border-b border-b-gray-200 dark:border-b-gray-800 pb-2">
|
||||
@@ -43,7 +45,9 @@ const RTCWidget = ({ id, context = "channel" }: Props) => {
|
||||
<Signal strength={voicingInfo.downlinkNetworkQuality} />
|
||||
<div className="flex flex-col">
|
||||
<span className={clsx('text-green-800 font-bold', isReConnecting && `text-red-500`)}>{isReConnecting ? `Voice Reconnecting...` : `Voice Connected`}</span>
|
||||
<span className='text-gray-600 dark:text-gray-400 text-xs truncate max-w-[170px]' >{voicingInfo.context == "channel" ? "Channel" : "DM"} / {voicingInfo.context == "channel" ? channelData[voicingInfo.id]?.name : userData[voicingInfo.id]?.name}</span>
|
||||
<span className='text-gray-600 dark:text-gray-400 text-xs truncate max-w-[170px]' >
|
||||
{voicingInfo.context == "channel" ? "Channel" : "DM"} / {voicingInfo.context == "channel" ? channelData[voicingInfo.id]?.name : targetDMUsername}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip tip={t("leave_voice")} placement="top">
|
||||
|
||||
@@ -11,9 +11,9 @@ type Props = {
|
||||
}
|
||||
|
||||
const Dashboard = ({ context = "channel", id, visible }: Props) => {
|
||||
const { voicingInfo, setMute, leave, closeCamera, openCamera, startShareScreen, stopShareScreen } = useVoice({ id, context });
|
||||
const { voicingInfo } = useVoice({ id, context });
|
||||
return <div className={`h-full flex-col gap-1 w-[226px] overflow-y-scroll overflow-x-hidden p-2 border border-black/10 md:border-none md:shadow-[inset_1px_0px_0px_rgba(0,_0,_0,_0.1)] ${visible ? "flex" : "hidden"}`}>
|
||||
<VoiceManagement id={id} context={context} info={voicingInfo} setMute={setMute} leave={leave} closeCamera={closeCamera} openCamera={openCamera} startShareScreen={startShareScreen} stopShareScreen={stopShareScreen} />
|
||||
<VoiceManagement id={id} context={context} info={voicingInfo} />
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,41 +1,25 @@
|
||||
import clsx from 'clsx';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { VoicingInfo, updatePin } from '../../../app/slices/voice';
|
||||
import { useAppSelector } from '../../../app/store';
|
||||
import Avatar from '../../../components/Avatar';
|
||||
// import IconHeadphone from '@/assets/icons/sound.on.svg';
|
||||
// import IconHeadphoneOff from '@/assets/icons/sound.off.svg';
|
||||
import IconMic from '@/assets/icons/mic.on.svg';
|
||||
import IconMicOff from '@/assets/icons/mic.off.svg';
|
||||
import IconCameraOff from '@/assets/icons/camera.off.svg';
|
||||
import IconCamera from '@/assets/icons/camera.svg';
|
||||
import IconFullscreen from '@/assets/icons/fullscreen.svg';
|
||||
import IconScreen from '@/assets/icons/share.screen.svg';
|
||||
import IconCallOff from '@/assets/icons/call.off.svg';
|
||||
import StyledButton from '../../../components/styled/Button';
|
||||
import Tooltip from '../../../components/Tooltip';
|
||||
import { playAgoraVideo } from '@/utils';
|
||||
import { ChatContext } from '../../../types/common';
|
||||
import { updateChannelVisibleAside } from '../../../app/slices/footprint';
|
||||
import Operations from '@/components/Voice/Operations';
|
||||
import { updateChannelVisibleAside } from '@/app/slices/footprint';
|
||||
import { useDispatch } from 'react-redux';
|
||||
// import User from '../../../common/component/User';
|
||||
|
||||
type Props = {
|
||||
id: number,
|
||||
context: ChatContext,
|
||||
info: VoicingInfo | null,
|
||||
setMute: (param: boolean) => void,
|
||||
leave: () => void,
|
||||
closeCamera: () => void,
|
||||
openCamera: () => void,
|
||||
startShareScreen: () => void,
|
||||
stopShareScreen: () => void
|
||||
}
|
||||
|
||||
const VoiceManagement = ({ id, context, info, setMute, leave, closeCamera, openCamera, startShareScreen, stopShareScreen }: Props) => {
|
||||
const { t } = useTranslation("chat");
|
||||
const VoiceManagement = ({ id, context, info, }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const { userData, voicingMembers } = useAppSelector(store => {
|
||||
return {
|
||||
@@ -58,7 +42,6 @@ const VoiceManagement = ({ id, context, info, setMute, leave, closeCamera, openC
|
||||
}
|
||||
};
|
||||
if (!info) return null;
|
||||
const { muted, video, shareScreen } = info;
|
||||
const nameClass = clsx(`text-sm text-gray-500 max-w-[120px] truncate font-semibold dark:text-white`);
|
||||
const members = voicingMembers.ids;
|
||||
const membersData = voicingMembers.byId;
|
||||
@@ -116,38 +99,10 @@ const VoiceManagement = ({ id, context, info, setMute, leave, closeCamera, openC
|
||||
</div>
|
||||
</li>;
|
||||
})}
|
||||
|
||||
</ul>
|
||||
<div className="flex flex-col gap-2">
|
||||
<ul className='flex justify-between'>
|
||||
<Tooltip tip={muted ? t("unmute") : t("mute")} placement="top">
|
||||
<li role={"button"} onClick={setMute.bind(null, !muted)} className="py-2 px-3 rounded bg-gray-100 dark:bg-gray-900">
|
||||
{muted ? <IconMicOff className="fill-gray-700 dark:fill-gray-300" /> : <IconMic className="fill-gray-700 dark:fill-gray-300" />}
|
||||
</li>
|
||||
</Tooltip>
|
||||
<Tooltip tip={video ? t("camera_off") : t("camera_on")} placement="top">
|
||||
<li role={"button"} onClick={video ? closeCamera : openCamera} className="py-2 px-3 rounded bg-gray-100 dark:bg-gray-900">
|
||||
{video ? <IconCamera className="fill-gray-700 dark:fill-gray-300" /> : <IconCameraOff className="fill-gray-700 dark:fill-gray-300" />}
|
||||
</li>
|
||||
</Tooltip>
|
||||
<Tooltip tip={"Share Screen"} placement="top">
|
||||
<li role={"button"} onClick={shareScreen ? stopShareScreen : startShareScreen} className={clsx("py-2 px-3 rounded ", shareScreen ? "bg-green-700" : "bg-gray-100 dark:bg-gray-900")}>
|
||||
<IconScreen className={clsx("dark:fill-gray-300", shareScreen ? "fill-gray-200" : "fill-gray-800")} />
|
||||
</li>
|
||||
</Tooltip>
|
||||
<Tooltip tip={"Fullscreen"} placement="top">
|
||||
<li role={"button"} onClick={handleFullscreen.bind(null, undefined)} className="py-2 px-3 rounded bg-gray-100 dark:bg-gray-900">
|
||||
<IconFullscreen className="fill-gray-700 dark:fill-gray-300" />
|
||||
</li>
|
||||
</Tooltip>
|
||||
</ul>
|
||||
<StyledButton onClick={leave} className='bg-red-600 hover:!bg-red-700 text-center'>
|
||||
<Tooltip tip={t("leave_voice")} placement="top" offset={[0, 24]} >
|
||||
<IconCallOff className="m-auto" />
|
||||
</Tooltip>
|
||||
</StyledButton>
|
||||
<div className="grid grid-rows-2 grid-cols-4 gap-2">
|
||||
<Operations id={id} context={context} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -50,7 +50,7 @@ const VoiceChat = ({ id, context = "channel" }: Props) => {
|
||||
dispatch(context == "channel" ? updateChannelVisibleAside(data) : updateDMVisibleAside(data));
|
||||
// 实时显示calling box
|
||||
if (!joinedAtThisContext && context == "dm") {
|
||||
dispatch(updateCallInfo({ from: loginUser?.uid ?? 0, to: id, calling: true }));
|
||||
dispatch(updateCallInfo({ from: loginUser?.uid ?? 0, to: id, calling: false }));
|
||||
}
|
||||
};
|
||||
if (!loginUser || !enabled) return null;
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
import { useEffect, useState, MouseEvent } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx';
|
||||
import IconMic from '@/assets/icons/mic.on.svg';
|
||||
import IconMicOff from '@/assets/icons/mic.off.svg';
|
||||
import IconCameraOff from '@/assets/icons/camera.off.svg';
|
||||
import IconPin from '@/assets/icons/pin.svg';
|
||||
import IconCamera from '@/assets/icons/camera.svg';
|
||||
import IconScreen from '@/assets/icons/share.screen.svg';
|
||||
import IconExitScreen from '@/assets/icons/fullscreen.exit.svg';
|
||||
import IconCallOff from '@/assets/icons/call.off.svg';
|
||||
import { ChatContext } from '../../types/common';
|
||||
import { useAppSelector } from '../../app/store';
|
||||
import Avatar from '../../components/Avatar';
|
||||
import { playAgoraVideo } from '../../utils';
|
||||
import { updateChannelVisibleAside } from '../../app/slices/footprint';
|
||||
import Tooltip from '../../components/Tooltip';
|
||||
import { useVoice } from '../../components/Voice';
|
||||
import StyledButton from '../../components/styled/Button';
|
||||
import { updatePin } from '../../app/slices/voice';
|
||||
import Operations from '@/components/Voice/Operations';
|
||||
|
||||
type Props = {
|
||||
context: ChatContext,
|
||||
@@ -28,8 +22,7 @@ type Props = {
|
||||
const VoiceFullscreen = ({ id, context }: Props) => {
|
||||
const dispatch = useDispatch();
|
||||
const [speakingUid, setSpeakingUid] = useState(0);
|
||||
const { t } = useTranslation("chat");
|
||||
const { voicingInfo, setMute, leave, closeCamera, openCamera, startShareScreen, stopShareScreen } = useVoice({ id, context });
|
||||
const { voicingInfo, exitFullscreen } = useVoice({ id, context });
|
||||
const { name, userData, voicingMembers } = useAppSelector(store => {
|
||||
return {
|
||||
userData: store.users.byId,
|
||||
@@ -49,11 +42,6 @@ const VoiceFullscreen = ({ id, context }: Props) => {
|
||||
});
|
||||
|
||||
}, [voicingMembers]);
|
||||
const handleExitFullscreen = () => {
|
||||
if (context == "channel") {
|
||||
dispatch(updateChannelVisibleAside({ id, aside: "voice" }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleDoubleClick = (evt: MouseEvent<HTMLLIElement>) => {
|
||||
const uid = evt.currentTarget.dataset.uid;
|
||||
@@ -77,14 +65,13 @@ const VoiceFullscreen = ({ id, context }: Props) => {
|
||||
const members = voicingMembers.ids;
|
||||
const membersData = voicingMembers.byId;
|
||||
const hasPin = typeof pinUid !== "undefined";
|
||||
const { muted, video, shareScreen } = voicingInfo;
|
||||
return (
|
||||
<div className='h-full bg-black text-gray-300 flex flex-col justify-between rounded-r-2xl'>
|
||||
{/* top */}
|
||||
<div className="px-7 py-6 flex justify-between">
|
||||
<span className='text-sm font-semibold'>{_name}</span>
|
||||
<div className="flex gap-4">
|
||||
<IconExitScreen role="button" onClick={handleExitFullscreen} className="fill-gray-200" />
|
||||
<IconExitScreen role="button" onClick={exitFullscreen} className="fill-gray-200" />
|
||||
</div>
|
||||
</div>
|
||||
{/* middle */}
|
||||
@@ -129,28 +116,9 @@ const VoiceFullscreen = ({ id, context }: Props) => {
|
||||
|
||||
</ul>
|
||||
{/* bottom */}
|
||||
<ul className='py-4 flex justify-center gap-2'>
|
||||
<Tooltip tip={muted ? t("unmute") : t("mute")} placement="top">
|
||||
<li role={"button"} onClick={setMute.bind(null, !muted)} className="flex-center py-1 px-3 rounded 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" />}
|
||||
</li>
|
||||
</Tooltip>
|
||||
<Tooltip tip={video ? t("camera_off") : t("camera_on")} placement="top">
|
||||
<li role={"button"} onClick={video ? closeCamera : openCamera} className="flex-center py-1 px-3 rounded 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" />}
|
||||
</li>
|
||||
</Tooltip>
|
||||
<Tooltip tip={"Share Screen"} placement="top">
|
||||
<li role={"button"} onClick={shareScreen ? stopShareScreen : startShareScreen} className={clsx("flex-center py-1 px-3 rounded ", 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")} />
|
||||
</li>
|
||||
</Tooltip>
|
||||
<StyledButton onClick={leave} className='bg-red-600 hover:!bg-red-700 text-center'>
|
||||
<Tooltip tip={t("leave_voice")} placement="top" offset={[0, 24]} >
|
||||
<IconCallOff className="m-auto" />
|
||||
</Tooltip>
|
||||
</StyledButton>
|
||||
</ul>
|
||||
<div className='py-4 flex justify-center gap-2'>
|
||||
<Operations id={id} context={context} mode="fullscreen" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,11 +24,12 @@ function ChatPage() {
|
||||
const [channelModalVisible, setChannelModalVisible] = useState(false);
|
||||
const [usersModalVisible, setUsersModalVisible] = useState(false);
|
||||
const { channel_id = 0, user_id = 0 } = useParams();
|
||||
const { sessionUids, isGuest, aside = "" } = useAppSelector((store) => {
|
||||
const { sessionUids, isGuest, aside = "", callingTo } = useAppSelector((store) => {
|
||||
return {
|
||||
callingTo: store.voice.callingTo,
|
||||
isGuest: store.authData.guest,
|
||||
sessionUids: store.userMessage.ids,
|
||||
aside: channel_id ? store.footprint.channelAsides[+channel_id] : store.footprint.dmAsides[+user_id]
|
||||
aside: channel_id ? store.footprint.channelAsides[+channel_id] : store.footprint.dmAsides[store.voice.callingTo]
|
||||
};
|
||||
});
|
||||
const toggleUsersModalVisible = () => {
|
||||
@@ -56,7 +57,7 @@ function ChatPage() {
|
||||
const dmChatVisible = user_id != 0 && aside !== "voice_fullscreen";
|
||||
const isMainPath = isHomePath || isChatHomePath;
|
||||
const context = channel_id !== 0 ? "channel" : "dm";
|
||||
const contextId = (+channel_id || +user_id) ?? 0;
|
||||
const contextId = (+channel_id || callingTo) ?? 0;
|
||||
console.log("fffff", channel_id, user_id, aside, channelChatVisible);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user