refactor: voice operations

This commit is contained in:
Tristan Yang
2023-05-17 17:32:33 +08:00
parent d931bf8daa
commit c0c1dc8bf5
13 changed files with 312 additions and 163 deletions
+38 -38
View File
@@ -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>
+7 -3
View File
@@ -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">
+2 -2
View File
@@ -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>;
};
+5 -50
View File
@@ -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>
);
};
+1 -1
View File
@@ -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;
+6 -38
View File
@@ -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>
);
};
+4 -3
View File
@@ -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 (