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:
5
TODO.md
5
TODO.md
@@ -8,7 +8,4 @@
|
||||
|
||||
- Can we add a way to tell the user they are connecting to voice. Like show them its connecting so the user knows something is happening instead of them clicking on the voice stage again and again.
|
||||
|
||||
- Add photo / video albums like Commit https://commet.chat/
|
||||
|
||||
when i do filter image it has some other stuff like a .html file and a video file. In the search results but we shouldent show non images in the image filter. Videos should have its own filter and .html and other files should go under the file filter, but images should not go under the file filter. We should also show the video and file attachment like how we show it in the chat.
|
||||
|
||||
- Add photo / video albums like Commit https://commet.chat/
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@discord-clone/electron",
|
||||
"private": true,
|
||||
"version": "1.0.18",
|
||||
"version": "1.0.19",
|
||||
"description": "Discord Clone - Electron app",
|
||||
"author": "Moyettes",
|
||||
"type": "module",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@discord-clone/shared",
|
||||
"private": true,
|
||||
"version": "1.0.18",
|
||||
"version": "1.0.19",
|
||||
"type": "module",
|
||||
"main": "src/App.jsx",
|
||||
"dependencies": {
|
||||
|
||||
1
packages/shared/src/assets/icons/call.svg
Normal file
1
packages/shared/src/assets/icons/call.svg
Normal 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 |
@@ -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
|
||||
};
|
||||
|
||||
1
packages/shared/src/assets/icons/memebers.svg
Normal file
1
packages/shared/src/assets/icons/memebers.svg
Normal 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 |
@@ -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>
|
||||
|
||||
@@ -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)}>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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