diff --git a/frontend/src/pages/chat/css/_callWindow.scss b/frontend/src/pages/chat/css/_callWindow.scss index 46f7176..fc5244c 100644 --- a/frontend/src/pages/chat/css/_callWindow.scss +++ b/frontend/src/pages/chat/css/_callWindow.scss @@ -1,44 +1,177 @@ @use "../../../css/material" as *; +@use "sass:color"; .call-window { $transition: cubic-bezier(0.4, 0, 0.2, 1); position: fixed; z-index: 1000; - min-width: 800px; - width: fit-content; - max-width: 1200px; - background-color: rgba($color-dark-surface, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid rgba($color-dark-outline, 0.3); - border-radius: 12px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); - cursor: grab; - transition: opacity 0.3s $transition, transform 0.3s $transition; - transform-origin: center; - user-select: none; display: flex; flex-direction: column; - - &.visible { - opacity: 1; - transform: scale(1); - } - - &.hidden { - opacity: 0; - transform: scale(0.8) translateY(-20px); - pointer-events: none; - } - + user-select: none; + + // Base transition for all properties + transition: all 0.4s $transition; + + // Disable all transitions while dragging for immediate feedback &.dragging { - cursor: grabbing; + transition: none !important; + } + + // Maximized mode (fullscreen) + &.maximized { + top: 0; + left: 0; + right: 0; + bottom: 0; + width: 100vw; + height: 100vh; + background-color: rgba($color-dark-surface, 0.98); + backdrop-filter: blur(40px); + -webkit-backdrop-filter: blur(40px); + border: none; + border-radius: 0; + cursor: default; + + &.visible { + opacity: 1; + transform: translateY(0); + } + + &.hidden { + opacity: 0; + transform: translateY(-100%); // Slide up when ending in fullscreen + pointer-events: none; + } + + .call-content { + padding: 32px; + gap: 24px; + + .video-tiles-grid { + gap: 24px; + min-height: 400px; + } + + .screen-share-tile { + min-height: 60vh; + } + } + + .call-header { + .window-controls { + .window-control-btn { + color: $color-dark-on-surface-variant; + + &:hover { + color: $color-dark-on-surface; + background: rgba($color-dark-on-surface, 0.08); + } + } + } + } + } + + // Minimized mode (PiP) + &.minimized { + width: 400px; + height: 300px; + background-color: rgba($color-dark-surface, 0.95); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border: 2px solid rgba($color-dark-outline, 0.4); + border-radius: 16px; + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5); + cursor: grab; + + &.visible { + opacity: 1; + transform: scale(1); + } + + &.hidden { + opacity: 0 !important; + transform: scale(0.7) !important; // Scale down and fade when ending in PiP + pointer-events: none; + transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1), transform 0.4s cubic-bezier(0.4, 0, 0.2, 1) !important; + } + + &.dragging { + cursor: grabbing; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.7); + } + + .call-header { + padding: 12px; + min-height: auto; + + .call-header-info { + .username { + font-size: 14px; + } + + .status { + font-size: 12px; + } + } + + .window-controls { + .window-control-btn { + color: $color-dark-primary; + + &:hover { + color: color.adjust($color-dark-primary, $lightness: 10%); + background: rgba($color-dark-primary, 0.1); + } + } + } + } + + .call-content { + padding: 8px; + gap: 8px; + + .video-tiles-grid { + gap: 8px; + min-height: 120px; + } + + .video-tile { + .tile-label { + font-size: 10px; + padding: 2px 6px; + } + + .video-placeholder { + .placeholder-avatar { + width: 40px; + height: 40px; + } + + .placeholder-username { + font-size: 12px; + } + } + } + + .screen-share-tile { + min-height: 150px; + } + } + + .call-controls { + padding: 8px; + gap: 8px; + + mdui-button-icon { + --mdui-comp-icon-button-size: 36px; + } + } } // Dynamic gradients based on call state &.gradient-calling { - border-color: rgba(255, 193, 7, 0.4); + border-color: rgba(255, 193, 7, 0.5); &::before { content: ''; @@ -48,16 +181,17 @@ right: 0; bottom: 0; background: linear-gradient(135deg, - rgba(255, 193, 7, 0.15) 0%, - rgba(255, 152, 0, 0.15) 50%, - rgba(255, 193, 7, 0.15) 100%); - border-radius: 12px; + rgba(255, 193, 7, 0.12) 0%, + rgba(255, 152, 0, 0.12) 50%, + rgba(255, 193, 7, 0.12) 100%); + border-radius: inherit; animation: pulse-gradient 2s ease-in-out infinite; + pointer-events: none; } } &.gradient-connecting { - border-color: rgba(33, 150, 243, 0.4); + border-color: rgba(33, 150, 243, 0.5); &::before { content: ''; @@ -67,16 +201,17 @@ right: 0; bottom: 0; background: linear-gradient(135deg, - rgba(33, 150, 243, 0.15) 0%, - rgba(63, 81, 181, 0.15) 50%, - rgba(33, 150, 243, 0.15) 100%); - border-radius: 12px; + rgba(33, 150, 243, 0.12) 0%, + rgba(63, 81, 181, 0.12) 50%, + rgba(33, 150, 243, 0.12) 100%); + border-radius: inherit; animation: connecting-gradient 1.5s ease-in-out infinite; + pointer-events: none; } } &.gradient-active { - border-color: rgba(76, 175, 80, 0.4); + border-color: rgba(76, 175, 80, 0.5); &::before { content: ''; @@ -86,11 +221,12 @@ right: 0; bottom: 0; background: linear-gradient(135deg, - rgba(76, 175, 80, 0.15) 0%, - rgba(56, 142, 60, 0.15) 50%, - rgba(76, 175, 80, 0.15) 100%); - border-radius: 12px; + rgba(76, 175, 80, 0.12) 0%, + rgba(56, 142, 60, 0.12) 50%, + rgba(76, 175, 80, 0.12) 100%); + border-radius: inherit; animation: active-gradient 3s ease-in-out infinite; + pointer-events: none; } } @@ -100,25 +236,25 @@ } .call-header { - padding: 16px; + padding: 20px; border-bottom: 1px solid $color-dark-outline-variant; position: relative; z-index: 1; display: flex; align-items: center; justify-content: space-between; + flex-shrink: 0; .window-controls { display: flex; gap: 8px; + position: absolute; + left: 16px; + top: 50%; + transform: translateY(-50%); - .minimize-btn { - color: $color-dark-on-surface-variant; - transition: color 0.2s ease; - - &:hover { - color: $color-dark-on-surface; - } + .window-control-btn { + transition: all 0.2s ease; } } @@ -127,12 +263,12 @@ display: flex; flex-direction: column; align-items: center; - gap: 4px; + gap: 6px; text-align: center; .username { margin: 0; - font-size: 18px; + font-size: 20px; font-weight: 600; color: $color-dark-on-surface; } @@ -147,11 +283,11 @@ .encryption-emojis { display: flex; justify-content: center; - gap: 8px; + gap: 10px; margin-top: 8px; .encryption-emoji { - font-size: 20px; + font-size: 24px; display: inline-block; animation: emoji-pulse 2s ease-in-out infinite; @@ -165,31 +301,38 @@ } .call-content { - padding: 16px; + padding: 24px; display: flex; flex-direction: column; - gap: 16px; + gap: 20px; position: relative; z-index: 1; flex: 1; + overflow-y: auto; .video-tiles-grid { display: grid; grid-template-columns: repeat(2, 1fr); - gap: 16px; - min-height: 240px; + gap: 20px; + min-height: 300px; } .video-tile { position: relative; - background: rgba($color-dark-surface-variant, 0.4); - border-radius: 12px; + background: rgba($color-dark-surface-variant, 0.5); + border-radius: 16px; overflow: hidden; aspect-ratio: 4 / 3; display: flex; align-items: center; justify-content: center; border: 1px solid rgba($color-dark-outline, 0.3); + transition: all 0.3s ease; + + &:hover { + border-color: rgba($color-dark-outline, 0.5); + transform: scale(1.02); + } .video-element { width: 100%; @@ -202,36 +345,36 @@ flex-direction: column; align-items: center; justify-content: center; - gap: 12px; + gap: 16px; width: 100%; height: 100%; background: rgba($color-dark-surface-variant, 0.6); .placeholder-avatar { - width: 80px; - height: 80px; + width: 100px; + height: 100px; border-radius: 50%; object-fit: cover; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); } .placeholder-username { - font-size: 16px; - font-weight: 500; + font-size: 18px; + font-weight: 600; color: $color-dark-on-surface; } } .tile-label { position: absolute; - bottom: 8px; - left: 8px; - padding: 4px 8px; - background: rgba(0, 0, 0, 0.7); + bottom: 12px; + left: 12px; + padding: 6px 12px; + background: rgba(0, 0, 0, 0.75); color: white; - font-size: 12px; - font-weight: 500; - border-radius: 4px; + font-size: 13px; + font-weight: 600; + border-radius: 8px; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); } @@ -245,180 +388,61 @@ .screen-share-tile { aspect-ratio: 16 / 9; - min-height: 400px; + min-height: 480px; grid-column: 1 / -1; - border: 2px solid rgba($color-dark-primary, 0.5); + border: 2px solid rgba($color-dark-primary, 0.6); .video-element { object-fit: contain; - background: rgba(0, 0, 0, 0.8); + background: rgba(0, 0, 0, 0.9); } .tile-label { font-size: 14px; - padding: 6px 12px; - background: rgba($color-dark-primary, 0.8); + padding: 8px 16px; + background: rgba($color-dark-primary, 0.9); color: $color-dark-on-primary; } } } .call-controls { - padding: 16px; + padding: 20px; display: flex; justify-content: center; - gap: 12px; + gap: 16px; border-top: 1px solid $color-dark-outline-variant; position: relative; z-index: 1; + flex-shrink: 0; mdui-button-icon { + transition: all 0.2s ease; + &[icon="call_end"] { background: rgba(244, 67, 54, 0.2); color: rgb(244, 67, 54); &:hover { - background: rgba(244, 67, 54, 0.3); + background: rgba(244, 67, 54, 0.35); + transform: scale(1.1); } } &[icon="videocam"], &[icon="videocam_off"], &[icon="screen_share"], - &[icon="stop_screen_share"] { + &[icon="stop_screen_share"], + &[icon="mic"], + &[icon="mic_off"] { &:hover { - background: rgba($color-dark-primary, 0.1); + background: rgba($color-dark-primary, 0.15); + transform: scale(1.1); } } } } -// Minimized call bar -.minimized-call-bar { - position: fixed; - bottom: 0; - left: 0; - right: 0; - height: 60px; - background: rgba($color-dark-surface, 0.85); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-top: 1px solid rgba($color-dark-outline, 0.3); - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 16px; - z-index: 999; - cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - position: relative; - overflow: hidden; - - &::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, - transparent 0%, - rgba(255, 255, 255, 0.1) 50%, - transparent 100%); - animation: shimmer 2s ease-in-out infinite; - } - - .call-info { - display: flex; - align-items: center; - gap: 12px; - - .avatar { - width: 40px; - height: 40px; - border-radius: 50%; - object-fit: cover; - } - - .user-details { - display: flex; - flex-direction: column; - gap: 2px; - - .username { - font-size: 14px; - font-weight: 600; - color: $color-dark-on-surface; - } - - .status { - font-size: 12px; - color: $color-dark-on-surface-variant; - } - } - } - - .call-actions { - display: flex; - gap: 8px; - } - - // Dynamic gradients for minimized bar - &.gradient-calling { - border-color: rgba(255, 193, 7, 0.4); - - &::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, - transparent 0%, - rgba(255, 193, 7, 0.1) 50%, - transparent 100%); - animation: shimmer-calling 2s ease-in-out infinite; - } - } - - &.gradient-connecting { - border-color: rgba(33, 150, 243, 0.4); - - &::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, - transparent 0%, - rgba(33, 150, 243, 0.1) 50%, - transparent 100%); - animation: shimmer-connecting 1.5s ease-in-out infinite; - } - } - - &.gradient-active { - border-color: rgba(76, 175, 80, 0.4); - - &::before { - content: ''; - position: absolute; - top: 0; - left: -100%; - width: 100%; - height: 100%; - background: linear-gradient(90deg, - transparent 0%, - rgba(76, 175, 80, 0.1) 50%, - transparent 100%); - animation: shimmer-active 3s ease-in-out infinite; - } - } -} - .remote-audio { position: fixed; bottom: 0px; @@ -432,68 +456,28 @@ // Animations @keyframes pulse-gradient { 0%, 100% { - opacity: 0.3; - transform: scale(1); + opacity: 0.4; } 50% { - opacity: 0.6; - transform: scale(1.02); + opacity: 0.7; } } @keyframes connecting-gradient { 0%, 100% { - opacity: 0.2; - transform: translateX(0); + opacity: 0.3; } 50% { - opacity: 0.4; - transform: translateX(10px); + opacity: 0.6; } } @keyframes active-gradient { 0%, 100% { - opacity: 0.15; + opacity: 0.2; } 50% { - opacity: 0.25; - } -} - -@keyframes shimmer { - 0% { - left: -100%; - } - 100% { - left: 100%; - } -} - -@keyframes shimmer-calling { - 0% { - left: -100%; - } - 100% { - left: 100%; - } -} - -@keyframes shimmer-connecting { - 0% { - left: -100%; - } - 100% { - left: 100%; - } -} - -@keyframes shimmer-active { - 0% { - left: -100%; - } - 100% { - left: 100%; + opacity: 0.4; } } @@ -503,7 +487,7 @@ opacity: 0.8; } 50% { - transform: scale(1.2); + transform: scale(1.15); opacity: 1; } -} \ No newline at end of file +} diff --git a/frontend/src/pages/chat/state.ts b/frontend/src/pages/chat/state.ts index 76150e8..cf16791 100644 --- a/frontend/src/pages/chat/state.ts +++ b/frontend/src/pages/chat/state.ts @@ -87,6 +87,7 @@ interface AppState { toggleScreenShare: () => void; setRemoteVideoEnabled: (enabled: boolean) => void; setRemoteScreenSharing: (enabled: boolean) => void; + toggleCallMinimized: () => void; // User state user: UserState; @@ -567,5 +568,14 @@ export const useAppState = create((set, get) => ({ isRemoteScreenSharing: enabled } } + })), + toggleCallMinimized: () => set((state) => ({ + chat: { + ...state.chat, + call: { + ...state.chat.call, + isMinimized: !state.chat.call.isMinimized + } + } })) })); \ No newline at end of file diff --git a/frontend/src/pages/chat/ui/right/calls/CallWindow.tsx b/frontend/src/pages/chat/ui/right/calls/CallWindow.tsx index 41c059d..800438e 100644 --- a/frontend/src/pages/chat/ui/right/calls/CallWindow.tsx +++ b/frontend/src/pages/chat/ui/right/calls/CallWindow.tsx @@ -1,21 +1,36 @@ import { useState, useEffect } from "react"; -import { useAppState } from "@/pages/chat/state"; -import useAudioCall from "@/pages/chat/hooks/useAudioCall"; +import { useAppState, type CallStatus } from "@/pages/chat/state"; +import useCall from "@/pages/chat/hooks/useCall"; import defaultAvatar from "@/images/default-avatar.png"; +import { createPortal } from "react-dom"; +import { id } from "@/utils/utils"; export function CallWindow() { - const { chat, toggleMute, toggleCallMinimize } = useAppState(); + const { chat, toggleCallMinimize, user } = useAppState(); const { call } = chat; - const { acceptCall, rejectCall, remoteAudioRef, endCall } = useAudioCall(); - const [position, setPosition] = useState({ x: 100, y: 100 }); + const { + acceptCall, + rejectCall, + remoteAudioRef, + endCall, + toggleMute, + toggleVideo, + toggleScreenShare, + localVideoRef, + remoteVideoRef, + localScreenShareRef, + remoteScreenShareRef + } = useCall(); + const [pipPosition, setPipPosition] = useState({ x: window.innerWidth - 420, y: window.innerHeight - 320 }); const [isDragging, setIsDragging] = useState(false); - const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); + const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); const [callDuration, setCallDuration] = useState(0); const [isVisible, setIsVisible] = useState(false); const [shouldRender, setShouldRender] = useState(false); + const [wasMinimized, setWasMinimized] = useState(false); const [callData, setCallData] = useState<{ remoteUsername: string | null; - status: "calling" | "connecting" | "active" | "ended"; + status: CallStatus; isInitiator: boolean; isMuted: boolean; } | null>(null); @@ -53,40 +68,73 @@ export function CallWindow() { } }, [call.isActive, call.remoteUsername, call.status, call.isInitiator, call.isMuted]); + // Track minimized state for exit animation + useEffect(() => { + if (call.isActive) { + setWasMinimized(call.isMinimized); + } + }, [call.isActive, call.isMinimized]); + // Handle visibility animation with entrance and exit delays useEffect(() => { if (call.isActive) { setShouldRender(true); - if (!call.isMinimized) { - // Call is active and not minimized - show window with entrance animation - if (!isVisible) { - // Only animate in if not already visible (prevents animation on rapid calls) - requestAnimationFrame(() => { - setIsVisible(true); - }); - } - } else { - // Call is active but minimized - hide window but keep rendered - setIsVisible(false); - } + // Small delay to ensure DOM is ready, then trigger animation + requestAnimationFrame(() => { + requestAnimationFrame(() => { + setIsVisible(true); + }); + }); } else { if (shouldRender) { // Call ended - start exit animation + console.log(`Call ended - starting exit animation. Was minimized: ${wasMinimized}`); setIsVisible(false); // After animation completes, stop rendering const timer = setTimeout(() => { + console.log("Exit animation complete, removing window"); setShouldRender(false); setCallData(null); - }, 300); // Match the CSS transition duration + setWasMinimized(false); + }, 400); // Match the CSS transition duration return () => clearTimeout(timer); } else { // Call not active and not rendered - ensure clean state setShouldRender(false); setIsVisible(false); setCallData(null); + setWasMinimized(false); } } - }, [call.isActive, call.isMinimized, shouldRender, isVisible]); + }, [call.isActive, shouldRender, wasMinimized]); + + // Handle dragging for PiP mode + useEffect(() => { + const handleMouseMove = (e: MouseEvent) => { + if (isDragging && call.isMinimized) { + setPipPosition({ + x: e.clientX - dragOffset.x, + y: e.clientY - dragOffset.y + }); + } + }; + + const handleMouseUp = () => { + if (isDragging) { + setIsDragging(false); + } + }; + + if (isDragging) { + window.addEventListener("mousemove", handleMouseMove); + window.addEventListener("mouseup", handleMouseUp); + } + + return () => { + window.removeEventListener("mousemove", handleMouseMove); + window.removeEventListener("mouseup", handleMouseUp); + }; + }, [isDragging, call.isMinimized, dragOffset]); // Cleanup effect to reset state when component unmounts useEffect(() => { @@ -131,80 +179,150 @@ export function CallWindow() { return ( - <> -