Files
DiscordClone/packages/shared/src/pages/Chat.jsx
Bryan1029384756 bebf0bf989
All checks were successful
Build and Release / build-and-release (push) Successful in 11m38s
Better Chat
2026-02-17 10:12:38 -06:00

725 lines
32 KiB
JavaScript

import React, { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { useQuery, useConvex } from 'convex/react';
import { api } from '../../../../convex/_generated/api';
import Sidebar from '../components/Sidebar';
import ChatArea from '../components/ChatArea';
import VoiceStage from '../components/VoiceStage';
import FloatingStreamPiP from '../components/FloatingStreamPiP';
import { useVoice } from '../contexts/VoiceContext';
import FriendsView from '../components/FriendsView';
import MembersList from '../components/MembersList';
import ChatHeader from '../components/ChatHeader';
import SearchPanel from '../components/SearchPanel';
import SearchDropdown from '../components/SearchDropdown';
import { useToasts } from '../components/Toast';
import { PresenceProvider } from '../contexts/PresenceContext';
import { getUserPref, setUserPref } from '../utils/userPreferences';
import { usePlatform } from '../platform';
import { useIsMobile } from '../hooks/useIsMobile';
import IncomingCallUI from '../components/IncomingCallUI';
import Avatar from '../components/Avatar';
import callRingSound from '../assets/sounds/default_call_sound.mp3';
const MAX_SEARCH_HISTORY = 10;
const Chat = () => {
const { crypto, settings } = usePlatform();
const isMobile = useIsMobile();
const [userId, setUserId] = useState(() => localStorage.getItem('userId'));
const [username, setUsername] = useState(() => localStorage.getItem('username') || '');
const [view, setView] = useState(() => {
const id = localStorage.getItem('userId');
return id ? getUserPref(id, 'lastView', 'server') : 'server';
});
const [activeChannel, setActiveChannel] = useState(null);
const [channelKeys, setChannelKeys] = useState({});
const [activeDMChannel, setActiveDMChannel] = useState(null);
const [showMembers, setShowMembers] = useState(true);
const [showPinned, setShowPinned] = useState(false);
const [mobileView, setMobileView] = useState('sidebar');
// Jump-to-message state (for search result clicks)
const [jumpToMessageId, setJumpToMessageId] = useState(null);
const clearJumpToMessage = useCallback(() => setJumpToMessageId(null), []);
// Search state
const [searchQuery, setSearchQuery] = useState('');
const [showSearchDropdown, setShowSearchDropdown] = useState(false);
const [showSearchResults, setShowSearchResults] = useState(false);
const [searchSortOrder, setSearchSortOrder] = useState('newest');
const [searchHistory, setSearchHistory] = useState(() => {
const id = localStorage.getItem('userId');
return id ? getUserPref(id, 'searchHistory', []) : [];
});
const searchInputRef = useRef(null);
const convex = useConvex();
const { toasts, addToast, removeToast, ToastContainer } = useToasts();
const prevDmChannelsRef = useRef(null);
const { toggleMute, connectToVoice, disconnectVoice, activeChannelId: voiceActiveChannelId, voiceStates, room } = useVoice();
// Keyboard shortcuts
useEffect(() => {
const handler = (e) => {
if (e.ctrlKey && e.key === 'k') {
e.preventDefault();
// Focus the search input
const input = searchInputRef.current?.querySelector('input');
if (input) {
input.focus();
}
}
if (e.ctrlKey && e.shiftKey && e.key === 'M') {
e.preventDefault();
toggleMute();
}
};
window.addEventListener('keydown', handler);
return () => window.removeEventListener('keydown', handler);
}, [toggleMute]);
const channels = useQuery(api.channels.list) || [];
const categories = useQuery(api.categories.list) || [];
const serverSettings = useQuery(api.serverSettings.get);
const serverName = serverSettings?.serverName || 'Secure Chat';
const serverIconUrl = serverSettings?.iconUrl || null;
const allMembers = useQuery(api.members.listAll) || [];
const rawChannelKeys = useQuery(
api.channelKeys.getKeysForUser,
userId ? { userId } : "skip"
);
const dmChannels = useQuery(
api.dms.listDMs,
userId ? { userId } : "skip"
) || [];
useEffect(() => {
if (!rawChannelKeys || rawChannelKeys.length === 0) return;
const privateKey = sessionStorage.getItem('privateKey');
if (!privateKey) return;
async function decryptKeys() {
const keys = {};
for (const item of rawChannelKeys) {
try {
const bundleJson = await crypto.privateDecrypt(privateKey, item.encrypted_key_bundle);
Object.assign(keys, JSON.parse(bundleJson));
} catch (e) {
console.error(`Failed to decrypt keys for channel ${item.channel_id}`, e);
}
}
setChannelKeys(keys);
}
decryptKeys();
}, [rawChannelKeys]);
useEffect(() => {
if (activeChannel || channels.length === 0) return;
// Try to restore last active channel
if (userId) {
const savedChannel = getUserPref(userId, 'lastActiveChannel', null);
if (savedChannel && channels.some(c => c._id === savedChannel)) {
setActiveChannel(savedChannel);
return;
}
}
const firstTextChannel = channels.find(c => c.type === 'text');
if (firstTextChannel) {
setActiveChannel(firstTextChannel._id);
}
}, [channels, activeChannel, userId]);
// Persist active channel
useEffect(() => {
if (activeChannel && userId) {
setUserPref(userId, 'lastActiveChannel', activeChannel, settings);
}
}, [activeChannel, userId]);
// Persist view mode
useEffect(() => {
if (userId) {
setUserPref(userId, 'lastView', view, settings);
}
}, [view, userId]);
const openDM = useCallback(async (targetUserId, targetUsername) => {
const uid = localStorage.getItem('userId');
if (!uid) return;
try {
const { channelId, created } = await convex.mutation(api.dms.openDM, {
userId: uid,
targetUserId
});
if (created) {
const keyHex = await crypto.randomBytes(32);
const allUsers = await convex.query(api.auth.getPublicKeys, {});
const participants = allUsers.filter(u => u.id === uid || u.id === targetUserId);
const batchKeys = [];
for (const u of participants) {
if (!u.public_identity_key) continue;
try {
const payload = JSON.stringify({ [channelId]: keyHex });
const encryptedKeyHex = await crypto.publicEncrypt(u.public_identity_key, payload);
batchKeys.push({
channelId,
userId: u.id,
encryptedKeyBundle: encryptedKeyHex,
keyVersion: 1
});
} catch (e) {
console.error('Failed to encrypt DM key for user', u.id, e);
}
}
if (batchKeys.length > 0) {
await convex.mutation(api.channelKeys.uploadKeys, { keys: batchKeys });
}
}
setActiveDMChannel({ channel_id: channelId, other_username: targetUsername });
setView('me');
if (isMobile) setMobileView('chat');
} catch (err) {
console.error('Error opening DM:', err);
}
}, [convex, isMobile]);
const handleSelectChannel = useCallback((channelId) => {
setActiveChannel(channelId);
setShowPinned(false);
if (isMobile) setMobileView('chat');
}, [isMobile]);
const handleMobileBack = useCallback(() => {
setMobileView('sidebar');
}, []);
const activeChannelObj = channels.find(c => c._id === activeChannel);
const { watchingStreamOf } = useVoice();
const isDMView = view === 'me' && activeDMChannel;
const isServerTextChannel = view === 'server' && activeChannel && activeChannelObj?.type !== 'voice';
const currentChannelId = isDMView ? activeDMChannel.channel_id : activeChannel;
const effectiveShowMembers = isMobile ? false : showMembers;
// DM call state
const dmCallActive = isDMView && activeDMChannel
? (voiceStates[activeDMChannel.channel_id]?.length > 0) : false;
const isInDMCall = isDMView && activeDMChannel
? voiceActiveChannelId === activeDMChannel.channel_id : false;
const [dmCallExpanded, setDmCallExpanded] = useState(false);
const [dmCallStageHeight, setDmCallStageHeight] = useState(45);
const [rejectedCallChannelId, setRejectedCallChannelId] = useState(null);
const ringAudioRef = useRef(null);
const ringTimeoutRef = useRef(null);
useEffect(() => {
if (!isInDMCall) setDmCallExpanded(false);
}, [isInDMCall]);
// Global incoming DM call detection — not gated by current view
const incomingDMCall = useMemo(() => {
if (!userId) return null;
for (const dm of dmChannels) {
const users = voiceStates[dm.channel_id] || [];
if (users.length > 0 && voiceActiveChannelId !== dm.channel_id) {
const caller = users.find(u => u.userId !== userId);
if (caller) {
return {
channelId: dm.channel_id,
otherUsername: dm.other_username,
callerUsername: caller.username,
callerAvatarUrl: caller.avatarUrl || null,
};
}
}
}
return null;
}, [dmChannels, voiceStates, voiceActiveChannelId, userId]);
// Play ringing sound for incoming DM calls (global — works from any view)
useEffect(() => {
if (incomingDMCall && incomingDMCall.channelId !== rejectedCallChannelId) {
const audio = new Audio(callRingSound);
audio.loop = true;
audio.volume = 0.5;
audio.play().catch(() => {});
ringAudioRef.current = audio;
ringTimeoutRef.current = setTimeout(() => {
audio.pause();
audio.currentTime = 0;
setRejectedCallChannelId(incomingDMCall.channelId);
}, 30000);
return () => {
audio.pause();
audio.currentTime = 0;
ringAudioRef.current = null;
clearTimeout(ringTimeoutRef.current);
ringTimeoutRef.current = null;
};
}
}, [incomingDMCall?.channelId, rejectedCallChannelId]);
// Clear rejected state when the rejected call's channel becomes empty
useEffect(() => {
if (rejectedCallChannelId && !(voiceStates[rejectedCallChannelId]?.length > 0)) {
setRejectedCallChannelId(null);
}
}, [rejectedCallChannelId, voiceStates]);
const handleDmCallResizeStart = useCallback((e) => {
e.preventDefault();
const chatContent = e.target.closest('.chat-content');
if (!chatContent) return;
const startY = e.clientY;
const contentRect = chatContent.getBoundingClientRect();
const startHeight = dmCallStageHeight;
const onMouseMove = (moveE) => {
const deltaY = moveE.clientY - startY;
const deltaPercent = (deltaY / contentRect.height) * 100;
const newHeight = Math.min(80, Math.max(20, startHeight + deltaPercent));
setDmCallStageHeight(newHeight);
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
}, [dmCallStageHeight]);
const handleStartDMCall = useCallback(async () => {
if (!activeDMChannel) return;
const dmChannelId = activeDMChannel.channel_id;
const otherUsername = activeDMChannel.other_username;
// Already in this DM call
if (voiceActiveChannelId === dmChannelId) return;
// Suppress ringing when we leave later — just show the banner
setRejectedCallChannelId(dmChannelId);
// If in another voice channel, disconnect first
if (voiceActiveChannelId) {
disconnectVoice();
// Brief delay for cleanup
await new Promise(r => setTimeout(r, 300));
}
// Check if this is a new call (no one currently in it)
const isNewCall = !voiceStates[dmChannelId]?.length;
connectToVoice(dmChannelId, otherUsername, userId, true);
// Send system message for new calls
if (isNewCall && channelKeys[dmChannelId]) {
try {
const systemContent = JSON.stringify({ type: 'system', text: `${username} started a call` });
const { content: encryptedContent, iv, tag } = await crypto.encryptData(systemContent, channelKeys[dmChannelId]);
const ciphertext = encryptedContent + tag;
const signingKey = sessionStorage.getItem('signingKey');
if (signingKey) {
await convex.mutation(api.messages.send, {
channelId: dmChannelId,
senderId: userId,
ciphertext,
nonce: iv,
signature: await crypto.signMessage(signingKey, ciphertext),
keyVersion: 1
});
}
} catch (e) {
console.error('Failed to send call system message:', e);
}
}
}, [activeDMChannel, voiceActiveChannelId, disconnectVoice, connectToVoice, userId, username, channelKeys, crypto, convex, voiceStates]);
// PiP: show when watching a stream and NOT currently viewing the voice channel in VoiceStage
const isViewingDMCallStage = isDMView && isInDMCall;
const isViewingVoiceStage = (view === 'server' && activeChannelObj?.type === 'voice' && activeChannel === voiceActiveChannelId) || isViewingDMCallStage;
const showPiP = watchingStreamOf !== null && !isViewingVoiceStage;
const handleGoBackToStream = useCallback(() => {
if (voiceActiveChannelId) {
setActiveChannel(voiceActiveChannelId);
setView('server');
}
}, [voiceActiveChannelId]);
// Search handlers
const handleSearchQueryChange = useCallback((val) => {
setSearchQuery(val);
if (val === '') {
setShowSearchResults(false);
}
if (!showSearchDropdown && val !== undefined) {
setShowSearchDropdown(true);
}
}, [showSearchDropdown]);
const handleSearchFocus = useCallback(() => {
setShowSearchDropdown(true);
}, []);
const handleSearchBlur = useCallback(() => {
// Dropdown close is handled by click-outside in SearchDropdown
}, []);
const handleSearchSubmit = useCallback(() => {
if (!searchQuery.trim()) return;
setShowSearchDropdown(false);
setShowSearchResults(true);
// Save to history
setSearchHistory(prev => {
const filtered = prev.filter(h => h !== searchQuery.trim());
const updated = [searchQuery.trim(), ...filtered].slice(0, MAX_SEARCH_HISTORY);
if (userId) {
setUserPref(userId, 'searchHistory', updated, settings);
}
return updated;
});
}, [searchQuery, userId, settings]);
const handleSelectFilter = useCallback((prefix, value) => {
if (value !== null) {
// Replace the current active prefix with the completed token
const beforePrefix = searchQuery.replace(/\b(from|in|has|mentions):\S*$/i, '').trimEnd();
const newQuery = beforePrefix + (beforePrefix ? ' ' : '') + prefix + ':' + value + ' ';
setSearchQuery(newQuery);
} else {
// Just insert the prefix (e.g., clicking "from:" suggestion)
const newQuery = searchQuery + (searchQuery && !searchQuery.endsWith(' ') ? ' ' : '') + prefix + ':';
setSearchQuery(newQuery);
}
// Re-focus input
setTimeout(() => {
const input = searchInputRef.current?.querySelector('input');
if (input) input.focus();
}, 0);
}, [searchQuery]);
const handleSelectHistoryItem = useCallback((item) => {
setSearchQuery(item);
setShowSearchDropdown(false);
setShowSearchResults(true);
}, []);
const handleClearHistory = useCallback(() => {
setSearchHistory([]);
if (userId) {
setUserPref(userId, 'searchHistory', [], settings);
}
}, [userId, settings]);
const handleClearHistoryItem = useCallback((index) => {
setSearchHistory(prev => {
const updated = prev.filter((_, i) => i !== index);
if (userId) {
setUserPref(userId, 'searchHistory', updated, settings);
}
return updated;
});
}, [userId, settings]);
const handleCloseSearchDropdown = useCallback(() => {
setShowSearchDropdown(false);
}, []);
const handleCloseSearchResults = useCallback(() => {
setShowSearchResults(false);
setSearchQuery('');
}, []);
const handleJumpToMessage = useCallback((channelId, messageId) => {
// Switch to the correct channel if needed
const isDM = dmChannels.some(dm => dm.channel_id === channelId);
if (isDM) {
const dm = dmChannels.find(d => d.channel_id === channelId);
if (dm) {
setActiveDMChannel(dm);
setView('me');
}
} else {
setActiveChannel(channelId);
setView('server');
}
setShowSearchResults(false);
setSearchQuery('');
setJumpToMessageId(messageId);
}, [dmChannels]);
// Shared search props for ChatHeader
const searchProps = {
searchQuery,
onSearchQueryChange: handleSearchQueryChange,
onSearchSubmit: handleSearchSubmit,
onSearchFocus: handleSearchFocus,
onSearchBlur: handleSearchBlur,
searchInputRef,
searchActive: showSearchDropdown || showSearchResults,
};
function renderMainContent() {
if (view === 'me') {
if (activeDMChannel) {
const showIncomingUI = dmCallActive && !isInDMCall && incomingDMCall?.channelId === activeDMChannel?.channel_id && rejectedCallChannelId !== activeDMChannel?.channel_id;
return (
<div className="chat-container">
<ChatHeader
channelName={activeDMChannel.other_username}
channelType="dm"
onToggleMembers={() => {}}
showMembers={false}
onTogglePinned={() => setShowPinned(p => !p)}
isMobile={isMobile}
onMobileBack={handleMobileBack}
onStartCall={handleStartDMCall}
isDMCallActive={dmCallActive}
{...searchProps}
/>
<div className="chat-content" style={dmCallActive || isInDMCall ? { flexDirection: 'column' } : undefined}>
{showIncomingUI && (
<IncomingCallUI
callerUsername={incomingDMCall.callerUsername}
callerAvatarUrl={incomingDMCall.callerAvatarUrl}
onJoin={handleStartDMCall}
onReject={() => setRejectedCallChannelId(incomingDMCall.channelId)}
/>
)}
{dmCallActive && !isInDMCall && !showIncomingUI && (
<div className="dm-call-idle-stage">
<Avatar username={incomingDMCall?.callerUsername || activeDMChannel.other_username} avatarUrl={incomingDMCall?.callerAvatarUrl || null} size={80} />
<div className="dm-call-idle-username">{activeDMChannel.other_username}</div>
<div className="dm-call-idle-status">In a call</div>
<button className="dm-call-join-btn" onClick={handleStartDMCall}>Join Call</button>
</div>
)}
{isInDMCall && (
<div className="dm-call-stage" style={dmCallExpanded ? { height: '100%', maxHeight: '100%' } : { height: `${dmCallStageHeight}%` }}>
<button
className="dm-call-expand-btn"
onClick={() => setDmCallExpanded(prev => !prev)}
title={dmCallExpanded ? "Show Chat" : "Hide Chat"}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"
style={{ transform: dmCallExpanded ? 'rotate(180deg)' : undefined, transition: 'transform 0.2s' }}>
<path d="M5.3 9.3a1 1 0 0 1 1.4 0l5.3 5.29 5.3-5.3a1 1 0 1 1 1.4 1.42l-6 6a1 1 0 0 1-1.4 0l-6-6a1 1 0 0 1 0-1.42Z"/>
</svg>
</button>
<VoiceStage room={room} channelId={activeDMChannel.channel_id} voiceStates={voiceStates} channelName={activeDMChannel.other_username} />
{!dmCallExpanded && <div className="dm-call-resize-handle" onMouseDown={handleDmCallResizeStart} />}
</div>
)}
{(!isInDMCall || !dmCallExpanded) && (
<div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
<ChatArea
channelId={activeDMChannel.channel_id}
channelName={activeDMChannel.other_username}
channelType="dm"
channelKey={channelKeys[activeDMChannel.channel_id]}
username={username}
userId={userId}
showMembers={false}
onToggleMembers={() => {}}
onOpenDM={openDM}
showPinned={showPinned}
onTogglePinned={() => setShowPinned(false)}
jumpToMessageId={jumpToMessageId}
onClearJumpToMessage={clearJumpToMessage}
/>
<SearchPanel
visible={showSearchResults}
onClose={handleCloseSearchResults}
channels={channels}
isDM={true}
dmChannelId={activeDMChannel.channel_id}
onJumpToMessage={handleJumpToMessage}
query={searchQuery}
sortOrder={searchSortOrder}
onSortChange={setSearchSortOrder}
/>
</div>
)}
</div>
</div>
);
}
return (
<>
{isMobile && (
<div className="chat-header" style={{ position: 'sticky', top: 0, zIndex: 10 }}>
<div className="chat-header-left">
<button className="mobile-back-btn" onClick={handleMobileBack}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<span className="chat-header-name">Friends</span>
</div>
</div>
)}
<FriendsView onOpenDM={openDM} />
</>
);
}
if (activeChannel) {
if (activeChannelObj?.type === 'voice') {
return (
<div className="chat-container">
{isMobile && (
<div className="chat-header">
<div className="chat-header-left">
<button className="mobile-back-btn" onClick={handleMobileBack}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"/></svg>
</button>
<svg width="20" height="20" viewBox="0 0 24 24" style={{ color: 'var(--text-muted)', marginRight: 4 }}>
<path fill="currentColor" d="M11.383 3.07904C11.009 2.92504 10.579 3.01004 10.293 3.29604L6.586 7.00304H3C2.45 7.00304 2 7.45304 2 8.00304V16.003C2 16.553 2.45 17.003 3 17.003H6.586L10.293 20.71C10.579 20.996 11.009 21.082 11.383 20.927C11.757 20.772 12 20.407 12 20.003V4.00304C12 3.59904 11.757 3.23404 11.383 3.07904Z" />
</svg>
<span className="chat-header-name">{activeChannelObj?.name}</span>
</div>
</div>
)}
<VoiceStage room={room} channelId={activeChannel} voiceStates={voiceStates} channelName={activeChannelObj?.name} />
</div>
);
}
return (
<div className="chat-container">
<ChatHeader
channelName={activeChannelObj?.name || activeChannel}
channelType="text"
channelTopic={activeChannelObj?.topic}
onToggleMembers={() => setShowMembers(!showMembers)}
showMembers={effectiveShowMembers}
onTogglePinned={() => setShowPinned(p => !p)}
serverName={serverName}
isMobile={isMobile}
onMobileBack={handleMobileBack}
{...searchProps}
/>
<div className="chat-content">
<ChatArea
channelId={activeChannel}
channelName={activeChannelObj?.name || activeChannel}
channelType="text"
channelKey={channelKeys[activeChannel]}
username={username}
userId={userId}
showMembers={effectiveShowMembers}
onToggleMembers={() => setShowMembers(!showMembers)}
onOpenDM={openDM}
showPinned={showPinned}
onTogglePinned={() => setShowPinned(false)}
jumpToMessageId={jumpToMessageId}
onClearJumpToMessage={clearJumpToMessage}
/>
<MembersList
channelId={activeChannel}
visible={effectiveShowMembers}
onMemberClick={(member) => {}}
/>
<SearchPanel
visible={showSearchResults}
onClose={handleCloseSearchResults}
channels={channels}
onJumpToMessage={handleJumpToMessage}
query={searchQuery}
sortOrder={searchSortOrder}
onSortChange={setSearchSortOrder}
/>
</div>
</div>
);
}
return (
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#b9bbbe', flexDirection: 'column' }}>
<h2>Welcome to {serverName}</h2>
<p>No channels found.</p>
<p>Click the <b>+</b> in the sidebar to create your first encrypted channel.</p>
</div>
);
}
const handleSetActiveDMChannel = useCallback((dm) => {
setActiveDMChannel(dm);
if (isMobile && dm) setMobileView('chat');
}, [isMobile]);
const handleViewChange = useCallback((newView) => {
setView(newView);
if (isMobile) setMobileView('sidebar');
}, [isMobile]);
if (!userId) {
return (
<div className="app-container">
<div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#b9bbbe' }}>
Loading...
</div>
</div>
);
}
const showSidebar = !isMobile || mobileView === 'sidebar';
const showMainContent = !isMobile || mobileView === 'chat';
return (
<PresenceProvider userId={userId}>
<div className={`app-container${isMobile ? ' is-mobile' : ''}`}>
{showSidebar && (
<Sidebar
channels={channels}
categories={categories}
activeChannel={activeChannel}
onSelectChannel={handleSelectChannel}
username={username}
channelKeys={channelKeys}
view={view}
onViewChange={handleViewChange}
onOpenDM={openDM}
activeDMChannel={activeDMChannel}
setActiveDMChannel={handleSetActiveDMChannel}
dmChannels={dmChannels}
userId={userId}
serverName={serverName}
serverIconUrl={serverIconUrl}
isMobile={isMobile}
/>
)}
{showMainContent && renderMainContent()}
{showPiP && <FloatingStreamPiP onGoBackToStream={handleGoBackToStream} />}
{showSearchDropdown && !isMobile && (
<SearchDropdown
visible={showSearchDropdown}
searchText={searchQuery}
channels={channels}
members={allMembers}
searchHistory={searchHistory}
onSelectFilter={handleSelectFilter}
onSelectHistoryItem={handleSelectHistoryItem}
onClearHistory={handleClearHistory}
onClearHistoryItem={handleClearHistoryItem}
anchorRef={searchInputRef}
onClose={handleCloseSearchDropdown}
/>
)}
<ToastContainer />
</div>
</PresenceProvider>
);
};
export default Chat;