Add private calls
All checks were successful
Build and Release / build-and-release (push) Successful in 11m59s
All checks were successful
Build and Release / build-and-release (push) Successful in 11m59s
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user