Add private calls
All checks were successful
Build and Release / build-and-release (push) Successful in 11m59s

This commit is contained in:
Bryan1029384756
2026-02-16 13:46:26 -06:00
parent 73b9da2003
commit 5ca0eeb6a4
13 changed files with 286 additions and 57 deletions

View File

@@ -0,0 +1 @@
<svg x="0" y="0" class="icon__9293f" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M2 7.4A5.4 5.4 0 0 1 7.4 2c.36 0 .7.22.83.55l1.93 4.64a1 1 0 0 1-.43 1.25L7 10a8.52 8.52 0 0 0 7 7l1.12-2.24a1 1 0 0 1 1.19-.51l5.06 1.56c.38.11.63.46.63.85C22 19.6 19.6 22 16.66 22h-.37C8.39 22 2 15.6 2 7.71V7.4ZM13 3a1 1 0 0 1 1-1 8 8 0 0 1 8 8 1 1 0 1 1-2 0 6 6 0 0 0-6-6 1 1 0 0 1-1-1Z" class=""></path><path fill="currentColor" d="M13 7a1 1 0 0 1 1-1 4 4 0 0 1 4 4 1 1 0 1 1-2 0 2 2 0 0 0-2-2 1 1 0 0 1-1-1Z" class=""></path></svg>

After

Width:  |  Height:  |  Size: 622 B

View File

@@ -28,6 +28,7 @@ import FriendsIcon from './friends.svg';
import SharingIcon from './sharing.svg';
import PersonalMuteIcon from './personal_mute.svg';
import ServerMuteIcon from './server_mute.svg';
import CallIcon from './call.svg';
export {
AddIcon,
@@ -59,7 +60,8 @@ export {
FriendsIcon,
SharingIcon,
PersonalMuteIcon,
ServerMuteIcon
ServerMuteIcon,
CallIcon
};
export const Icons = {
@@ -92,5 +94,6 @@ export const Icons = {
Friends: FriendsIcon,
Sharing: SharingIcon,
PersonalMute: PersonalMuteIcon,
ServerMute: ServerMuteIcon
ServerMute: ServerMuteIcon,
Call: CallIcon
};

View File

@@ -0,0 +1 @@
<svg x="0" y="0" class="icon__9293f" aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" d="M14.5 8a3 3 0 1 0-2.7-4.3c-.2.4.06.86.44 1.12a5 5 0 0 1 2.14 3.08c.01.06.06.1.12.1ZM18.44 17.27c.15.43.54.73 1 .73h1.06c.83 0 1.5-.67 1.5-1.5a7.5 7.5 0 0 0-6.5-7.43c-.55-.08-.99.38-1.1.92-.06.3-.15.6-.26.87-.23.58-.05 1.3.47 1.63a9.53 9.53 0 0 1 3.83 4.78ZM12.5 9a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM2 20.5a7.5 7.5 0 0 1 15 0c0 .83-.67 1.5-1.5 1.5a.2.2 0 0 1-.2-.16c-.2-.96-.56-1.87-.88-2.54-.1-.23-.42-.15-.42.1v2.1a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-2.1c0-.25-.31-.33-.42-.1-.32.67-.67 1.58-.88 2.54a.2.2 0 0 1-.2.16A1.5 1.5 0 0 1 2 20.5Z" class=""></path></svg>

After

Width:  |  Height:  |  Size: 745 B

View File

@@ -11,6 +11,8 @@ const ChatHeader = ({
serverName,
isMobile,
onMobileBack,
onStartCall,
isDMCallActive,
// Search props
searchQuery,
onSearchQueryChange,
@@ -63,10 +65,23 @@ const ChatHeader = ({
</button>
</Tooltip>
)}
{isDM && !isMobile && (
<Tooltip text={isDMCallActive ? "Join Call" : "Start Call"} position="bottom">
<button
className={`chat-header-btn ${isDMCallActive ? 'active' : ''}`}
onClick={onStartCall}
style={isDMCallActive ? { color: '#3ba55c' } : undefined}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M6.62 10.79a15.053 15.053 0 006.59 6.59l2.2-2.2a1.003 1.003 0 011.01-.24c1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1C10.07 21 3 13.93 3 4c0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.1.31.03.66-.25 1.02l-2.2 2.2z"/>
</svg>
</button>
</Tooltip>
)}
<Tooltip text="Pinned Messages" position="bottom">
<button className="chat-header-btn" onClick={onTogglePinned}>
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M19.3 5.3a1 1 0 00-1.4-1.4L14.6 7.2l-1.5-.8a2 2 0 00-2.2.2L8.5 9a1 1 0 000 1.5l1.8 1.8-4.6 4.6a1 1 0 001.4 1.4l4.6-4.6 1.8 1.8a1 1 0 001.5 0l2.4-2.4a2 2 0 00.2-2.2l-.8-1.5 3.3-3.3z" />
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path fill="currentColor" d="M19.38 11.38a3 3 0 0 0 4.24 0l.03-.03a.5.5 0 0 0 0-.7L13.35.35a.5.5 0 0 0-.7 0l-.03.03a3 3 0 0 0 0 4.24L13 5l-2.92 2.92-3.65-.34a2 2 0 0 0-1.6.58l-.62.63a1 1 0 0 0 0 1.42l9.58 9.58a1 1 0 0 0 1.42 0l.63-.63a2 2 0 0 0 .58-1.6l-.34-3.64L19 11zM9.07 17.07a.5.5 0 0 1-.08.77l-5.15 3.43a.5.5 0 0 1-.63-.06l-.42-.42a.5.5 0 0 1-.06-.63L6.16 15a.5.5 0 0 1 .77-.08l2.14 2.14Z"/>
</svg>
</button>
</Tooltip>
@@ -76,9 +91,8 @@ const ChatHeader = ({
className={`chat-header-btn ${showMembers ? 'active' : ''}`}
onClick={onToggleMembers}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M14 8.006c0 2.206-1.346 4-3 4s-3-1.794-3-4 1.346-4 3-4 3 1.794 3 4zm-6.154 6.63c.896-.758 2.157-1.236 3.654-1.236s2.758.478 3.654 1.236c.898.76 1.346 1.773 1.346 2.87v.5h-10v-.5c0-1.097.448-2.11 1.346-2.87z" />
<path d="M20 10.006c0 1.655-1.01 3-2.25 3s-2.25-1.345-2.25-3S16.51 7.006 17.75 7.006 20 8.351 20 10.006zm-1.146 5.282c.674-.57 1.622-.93 2.646-.93.906 0 1.754.282 2.417.781.663.5 1.083 1.178 1.083 1.867V17.5h-4.6c-.173-.652-.52-1.262-1.032-1.794a5.86 5.86 0 00-.514-.418z" />
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path fill="currentColor" d="M14.5 8a3 3 0 1 0-2.7-4.3c-.2.4.06.86.44 1.12a5 5 0 0 1 2.14 3.08c.01.06.06.1.12.1ZM18.44 17.27c.15.43.54.73 1 .73h1.06c.83 0 1.5-.67 1.5-1.5a7.5 7.5 0 0 0-6.5-7.43c-.55-.08-.99.38-1.1.92-.06.3-.15.6-.26.87-.23.58-.05 1.3.47 1.63a9.53 9.53 0 0 1 3.83 4.78ZM12.5 9a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM2 20.5a7.5 7.5 0 0 1 15 0c0 .83-.67 1.5-1.5 1.5a.2.2 0 0 1-.2-.16c-.2-.96-.56-1.87-.88-2.54-.1-.23-.42-.15-.42.1v2.1a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-2.1c0-.25-.31-.33-.42-.1-.32.67-.67 1.58-.88 2.54a.2.2 0 0 1-.2.16A1.5 1.5 0 0 1 2 20.5Z"/>
</svg>
</button>
</Tooltip>

View File

@@ -33,7 +33,7 @@ const getUserColor = (username) => {
return colors[Math.abs(hash) % colors.length];
};
const DMList = ({ dmChannels, activeDMChannel, onSelectDM, onOpenDM }) => {
const DMList = ({ dmChannels, activeDMChannel, onSelectDM, onOpenDM, voiceStates }) => {
const [showUserPicker, setShowUserPicker] = useState(false);
const [allUsers, setAllUsers] = useState([]);
const [searchQuery, setSearchQuery] = useState('');
@@ -217,6 +217,11 @@ const DMList = ({ dmChannels, activeDMChannel, onSelectDM, onOpenDM }) => {
<div style={{ color: isActive ? 'var(--header-primary)' : 'var(--text-normal)', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden', fontWeight: '500' }}>
{dm.other_username}
</div>
{voiceStates && voiceStates[dm.channel_id]?.length > 0 && (
<div style={{ color: '#3ba55c', fontSize: '11px', fontWeight: '500' }}>
In Call
</div>
)}
</div>
</div>
<div className="dm-close-btn" onClick={(e) => handleCloseDM(e, dm)}>

View File

@@ -214,18 +214,10 @@ const MessageItem = React.memo(({
const currentDate = new Date(msg.created_at);
const userColor = getUserColor(msg.username || 'Unknown');
const systemMsg = parseSystemMessage(msg.content);
const renderMessageContent = () => {
const systemMsg = parseSystemMessage(msg.content);
if (systemMsg) {
return (
<div className="system-message">
<svg width="16" height="16" viewBox="0 0 24 24" fill="#3ba55c" style={{ marginRight: '8px', flexShrink: 0 }}>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
<span style={{ fontStyle: 'italic', color: 'var(--header-secondary)' }}>{systemMsg.text || 'System event'}</span>
</div>
);
}
if (systemMsg) return null;
const attachmentMetadata = parseAttachment(msg.content);
if (attachmentMetadata) {
@@ -279,6 +271,20 @@ const MessageItem = React.memo(({
</div>
)}
{showDateDivider && <div className="date-divider"><span>{dateLabel}</span></div>}
{systemMsg ? (
<div id={`msg-${msg.id}`} className="system-message-row">
<svg width="18" height="18" viewBox="0 0 24 24" fill="#23a559" style={{ flexShrink: 0 }}>
<path d="M2 7.4A5.4 5.4 0 0 1 7.4 2c.36 0 .7.22.83.55l1.93 4.64a1 1 0 0 1-.43 1.25L7 10a8.52 8.52 0 0 0 7 7l1.12-2.24a1 1 0 0 1 1.19-.51l5.06 1.56c.38.11.63.46.63.85C22 19.6 19.6 22 16.66 22h-.37C8.39 22 2 15.6 2 7.71V7.4ZM13 3a1 1 0 0 1 1-1 8 8 0 0 1 8 8 1 1 0 1 1-2 0 6 6 0 0 0-6-6 1 1 0 0 1-1-1Z"/>
<path d="M13 7a1 1 0 0 1 1-1 4 4 0 0 1 4 4 1 1 0 1 1-2 0 2 2 0 0 0-2-2 1 1 0 0 1-1-1Z"/>
</svg>
<span className="system-message-text">
{systemMsg.text || 'System event'}
</span>
<span className="system-message-time">
{currentDate.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}
</span>
</div>
) : (
<div
id={`msg-${msg.id}`}
className={`message-item${isGrouped ? ' message-grouped' : ''}`}
@@ -361,6 +367,7 @@ const MessageItem = React.memo(({
</div>
</div>
</div>
)}
</React.Fragment>
);
}, (prevProps, nextProps) => {

View File

@@ -1045,6 +1045,7 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
activeDMChannel={activeDMChannel}
onSelectDM={(dm) => setActiveDMChannel(dm === 'friends' ? null : dm)}
onOpenDM={onOpenDM}
voiceStates={voiceStates}
/>
</div>
);
@@ -1605,7 +1606,7 @@ const Sidebar = ({ channels, categories, activeChannel, onSelectChannel, usernam
<ColoredIcon src={disconnectIcon} color="var(--header-secondary)" size="20px" />
</button>
</div>
<div style={{ color: 'var(--text-normal)', fontSize: 12, marginBottom: 4 }}>{voiceChannelName} / {serverName}</div>
<div style={{ color: 'var(--text-normal)', fontSize: 12, marginBottom: 4 }}>{dmChannels?.some(dm => dm.channel_id === voiceChannelId) ? `Call with ${voiceChannelName}` : `${voiceChannelName} / ${serverName}`}</div>
<div style={{ marginBottom: 8 }}><VoiceTimer /></div>
<div style={{ display: 'flex', gap: 4 }}>
<button onClick={() => room?.localParticipant.setCameraEnabled(!room.localParticipant.isCameraEnabled)} title="Turn On Camera" style={voicePanelButtonStyle}>

View File

@@ -64,6 +64,7 @@ export const VoiceProvider = ({ children }) => {
parseInt(localStorage.getItem('voiceOutputVolume') || '100')
);
const isMovingRef = useRef(false);
const isDMCallRef = useRef(false);
const [isReceivingScreenShareAudio, setIsReceivingScreenShareAudio] = useState(false);
const convex = useConvex();
@@ -226,8 +227,9 @@ export const VoiceProvider = ({ children }) => {
}
}
const connectToVoice = async (channelId, channelName, userId) => {
const connectToVoice = async (channelId, channelName, userId, isDMCall = false) => {
if (activeChannelId === channelId) return;
isDMCallRef.current = isDMCall;
if (room) await room.disconnect();
@@ -406,6 +408,7 @@ export const VoiceProvider = ({ children }) => {
useEffect(() => {
if (!activeChannelId || !serverSettings?.afkChannelId || isInAfkChannel) return;
if (!idle?.getSystemIdleTime) return;
if (isDMCallRef.current) return; // Skip AFK for DM calls
const afkTimeout = serverSettings.afkTimeout || 300;
const interval = setInterval(async () => {
@@ -603,6 +606,7 @@ export const VoiceProvider = ({ children }) => {
const disconnectVoice = () => {
console.log('User manually disconnected voice');
isDMCallRef.current = false;
if (room) room.disconnect();
};

View File

@@ -1888,10 +1888,23 @@ body {
/* ============================================
SYSTEM MESSAGES
============================================ */
.system-message {
.system-message-row {
display: flex;
align-items: center;
padding: 2px 0;
justify-content: center;
gap: 8px;
padding: 8px 16px;
}
.system-message-text {
color: var(--text-muted);
font-size: 14px;
}
.system-message-time {
color: var(--text-muted);
font-size: 12px;
opacity: 0.7;
}
/* ============================================
@@ -3691,4 +3704,81 @@ img.search-dropdown-avatar {
margin-top: 4px;
margin-right: 4px;
text-transform: uppercase;
}
/* DM Call Styles */
.dm-call-stage {
height: 45%;
min-height: 200px;
max-height: 80%;
display: flex;
flex-direction: column;
border-bottom: 2px solid var(--bg-tertiary);
overflow: hidden;
position: relative;
}
.dm-call-expand-btn {
position: absolute;
left: 8px;
top: 8px;
z-index: 5;
background: var(--bg-tertiary);
border: none;
border-radius: 4px;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--interactive-normal);
opacity: 0.7;
transition: opacity 0.15s;
}
.dm-call-expand-btn:hover {
opacity: 1;
color: var(--interactive-hover);
}
.dm-call-resize-handle {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 6px;
cursor: ns-resize;
z-index: 5;
}
.dm-call-resize-handle:hover {
background: var(--interactive-normal);
opacity: 0.3;
}
.dm-call-banner {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 16px;
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--bg-tertiary);
color: var(--text-normal);
font-size: 14px;
}
.dm-call-join-btn {
background-color: #3ba55c;
color: white;
border: none;
border-radius: 4px;
padding: 6px 16px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.dm-call-join-btn:hover {
background-color: #2d7d46;
}

View File

@@ -49,7 +49,7 @@ const Chat = () => {
const convex = useConvex();
const { toasts, addToast, removeToast, ToastContainer } = useToasts();
const prevDmChannelsRef = useRef(null);
const { toggleMute } = useVoice();
const { toggleMute, connectToVoice, disconnectVoice, activeChannelId: voiceActiveChannelId, voiceStates, room } = useVoice();
// Keyboard shortcuts
useEffect(() => {
@@ -197,15 +197,93 @@ const Chat = () => {
}, []);
const activeChannelObj = channels.find(c => c._id === activeChannel);
const { room, voiceStates, activeChannelId: voiceActiveChannelId, watchingStreamOf } = useVoice();
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);
useEffect(() => {
if (!isInDMCall) setDmCallExpanded(false);
}, [isInDMCall]);
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;
// 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 isViewingVoiceStage = view === 'server' && activeChannelObj?.type === 'voice' && activeChannel === voiceActiveChannelId;
const isViewingDMCallStage = isDMView && isInDMCall;
const isViewingVoiceStage = (view === 'server' && activeChannelObj?.type === 'voice' && activeChannel === voiceActiveChannelId) || isViewingDMCallStage;
const showPiP = watchingStreamOf !== null && !isViewingVoiceStage;
const handleGoBackToStream = useCallback(() => {
@@ -349,33 +427,61 @@ const Chat = () => {
onTogglePinned={() => setShowPinned(p => !p)}
isMobile={isMobile}
onMobileBack={handleMobileBack}
onStartCall={handleStartDMCall}
isDMCallActive={dmCallActive}
{...searchProps}
/>
<div className="chat-content">
<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)}
/>
<SearchPanel
visible={showSearchResults}
onClose={handleCloseSearchResults}
channels={channels}
isDM={true}
dmChannelId={activeDMChannel.channel_id}
onJumpToMessage={handleJumpToMessage}
query={searchQuery}
sortOrder={searchSortOrder}
onSortChange={setSearchSortOrder}
/>
<div className="chat-content" style={isInDMCall ? { flexDirection: 'column' } : undefined}>
{dmCallActive && !isInDMCall && (
<div className="dm-call-banner">
<span>{activeDMChannel.other_username} is in a call</span>
<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)}
/>
<SearchPanel
visible={showSearchResults}
onClose={handleCloseSearchResults}
channels={channels}
isDM={true}
dmChannelId={activeDMChannel.channel_id}
onJumpToMessage={handleJumpToMessage}
query={searchQuery}
sortOrder={searchSortOrder}
onSortChange={setSearchSortOrder}
/>
</div>
)}
</div>
</div>
);