Use Motion for animations

This commit is contained in:
2025-10-29 19:45:48 +03:00
Unverified
parent c5d0b20350
commit d20800b681
7 changed files with 156 additions and 239 deletions
@@ -1,4 +1,5 @@
import { useState, useEffect, useRef, useMemo, type ReactNode } from "react";
import { motion, AnimatePresence } from "motion/react";
import { useAppState } from "@/pages/chat/state";
import { MessagePanel, type MessagePanelState } from "./panels/MessagePanel";
import { ChatMessages } from "./ChatMessages";
@@ -7,7 +8,6 @@ import { ProfileDialog } from "@/pages/chat/ui/ProfileDialog";
import { setGlobalMessageHandler } from "@/core/websocket";
import type { Message, WebSocketMessage } from "@/core/types";
import defaultAvatar from "@/images/default-avatar.png";
import AnimatedOpacity from "@/core/components/animations/AnimatedOpacity";
import { DMPanel } from "./panels/DMPanel";
import useCall from "@/pages/chat/hooks/useCall";
import { TypingIndicator } from "./TypingIndicator";
@@ -48,8 +48,6 @@ export function MessagePanelRenderer({ panel }: MessagePanelRendererProps) {
const { applyPendingPanel, chat, setProfileDialog } = useAppState();
const messagePanelRef = useRef<HTMLDivElement>(null);
const [panelState, setPanelState] = useState<MessagePanelState | null>(null);
const [switchIn, setSwitchIn] = useState(false);
const [switchOut, setSwitchOut] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const previousMessageCountRef = useRef(0);
const [replyTo, setReplyTo] = useState<Message | null>(null);
@@ -118,51 +116,32 @@ export function MessagePanelRenderer({ panel }: MessagePanelRendererProps) {
};
}, [panel]);
// Handle chat switching animation with event listeners
// Handle chat switching animation
useEffect(() => {
if (chat.isSwitching) {
setSwitchOut(true);
// Use animation event listeners instead of hardcoded delays
function handleAnimationEnd(event: Event) {
const animationEvent = event as AnimationEvent;
if (animationEvent.animationName === 'fadeOutUp') {
// Apply pending panel exactly at the boundary between animations
applyPendingPanel();
setSwitchOut(false);
setSwitchIn(true);
} else if (animationEvent.animationName === 'fadeInDown') {
setSwitchIn(false);
// End the chat switching state
chat.setIsSwitching(false);
}
};
// Add event listener to document to catch all animation events
document.addEventListener('animationend', handleAnimationEnd);
// Cleanup function
return () => {
document.removeEventListener('animationend', handleAnimationEnd);
};
if (chat.isSwitching && chat.pendingPanel) {
// Apply pending panel when animation starts
applyPendingPanel();
// End switching state after a brief delay to allow animation
setTimeout(() => {
chat.setIsSwitching(false);
}, 200);
}
}, [chat.isSwitching]);
}, [chat.isSwitching, chat.pendingPanel, applyPendingPanel]);
// Load messages when panel changes and animation is not running
useEffect(() => {
if (!chat.activePanel || chat.isSwitching || switchOut || switchIn) return;
if (!chat.activePanel || chat.isSwitching) return;
const panelState = chat.activePanel.getState();
if (panelState.messages.length === 0 && !panelState.isLoading) {
chat.activePanel.loadMessages();
}
}, [chat.activePanel, chat.isSwitching, switchOut, switchIn]);
}, [chat.activePanel, chat.isSwitching]);
// Scroll to bottom only when new messages are added
useEffect(() => {
if (!panelState || chat.isSwitching || switchOut || switchIn) return;
if (!panelState || chat.isSwitching) return;
const currentMessageCount = panelState.messages.length;
const previousMessageCount = previousMessageCountRef.current;
@@ -186,7 +165,7 @@ export function MessagePanelRenderer({ panel }: MessagePanelRendererProps) {
// Update the previous message count
previousMessageCountRef.current = currentMessageCount;
}, [panelState?.messages, panelState?.isLoading, chat.isSwitching, switchOut, switchIn]);
}, [panelState?.messages, panelState?.isLoading, chat.isSwitching]);
function handleCallClick() {
if (panel && panelState && panel.isDm()) {
@@ -213,12 +192,20 @@ export function MessagePanelRenderer({ panel }: MessagePanelRendererProps) {
}
}
const panelKey = chat.activePanel?.getState().title || "empty";
return (
<div className={`chat-container ${switchIn ? "chat-switch-in" : ""} ${switchOut ? "chat-switch-out" : ""}`}>
<div
ref={messagePanelRef}
className="chat-main"
id="chat-inner"
<div className="chat-container">
<AnimatePresence mode="wait">
<motion.div
key={panelKey}
ref={messagePanelRef}
className="chat-main"
id="chat-inner"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
onDragEnter={panel ? (e) => {
if (!e.dataTransfer) return;
e.preventDefault();
@@ -324,18 +311,26 @@ export function MessagePanelRenderer({ panel }: MessagePanelRendererProps) {
{panel && (
<>
<AnimatedOpacity
visible={isDragging}
className="file-overlay"
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => e.preventDefault()}>
<div className="file-overlay-wrapper">
<div className="file-overlay-inner">
<mdui-icon name="upload_file" />
<span>Отпустите файл(ы) для добавления</span>
</div>
</div>
</AnimatedOpacity>
<AnimatePresence>
{isDragging && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.5 }}
className="file-overlay"
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => e.preventDefault()}
>
<div className="file-overlay-wrapper">
<div className="file-overlay-inner">
<mdui-icon name="upload_file" />
<span>Отпустите файл(ы) для добавления</span>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
<ChatInputWrapper
@@ -396,7 +391,8 @@ export function MessagePanelRenderer({ panel }: MessagePanelRendererProps) {
/>
</>
)}
</div>
</motion.div>
</AnimatePresence>
{/* Profile Dialog */}
<ProfileDialog />