mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Fix the chat switching animation
This commit is contained in:
@@ -20,3 +20,4 @@ When working with this project, follow these rules:
|
|||||||
- When you complete your task, remove unused imports if there are any.
|
- When you complete your task, remove unused imports if there are any.
|
||||||
- Follow DRY, SOLID, YAGNI and KISS principles.
|
- Follow DRY, SOLID, YAGNI and KISS principles.
|
||||||
- Do NOT use old, outdated or deprecated APIs and functions.
|
- Do NOT use old, outdated or deprecated APIs and functions.
|
||||||
|
- Don't talk like a robot. Behave more like a human.
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { PRODUCT_NAME } from "../../../core/config";
|
import { PRODUCT_NAME } from "../../../core/config";
|
||||||
import { useProfile } from "../../hooks/useProfile";
|
import useProfile from "../../hooks/useProfile";
|
||||||
import defaultAvatar from "../../../resources/images/default-avatar.png";
|
import defaultAvatar from "../../../resources/images/default-avatar.png";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ProfileDialog } from "../profile/ProfileDialog";
|
import { ProfileDialog } from "../profile/ProfileDialog";
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useChat } from "../../hooks/useChat";
|
import { useAppState } from "../../state";
|
||||||
import defaultAvatar from "../../../resources/images/default-avatar.png";
|
import defaultAvatar from "../../../resources/images/default-avatar.png";
|
||||||
|
|
||||||
export function ChatMainHeader() {
|
export function ChatMainHeader() {
|
||||||
const { currentChat } = useChat();
|
const { currentChat } = useAppState().chat;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-header">
|
<div className="chat-header">
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useChat } from "../../hooks/useChat";
|
|
||||||
import { Message } from "./Message";
|
import { Message } from "./Message";
|
||||||
import { useAppState } from "../../state";
|
import { useAppState } from "../../state";
|
||||||
import type { Message as MessageType } from "../../../core/types";
|
import type { Message as MessageType } from "../../../core/types";
|
||||||
@@ -21,12 +20,10 @@ interface ChatMessagesProps {
|
|||||||
dmRecipientPublicKey?: string;
|
dmRecipientPublicKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatMessages({ messages: propMessages, children, isDm = false, onReplySelect, onEditSelect, onDelete, onRetryMessage, dmRecipientPublicKey }: ChatMessagesProps) {
|
export function ChatMessages({ messages = [], children, isDm = false, onReplySelect, onEditSelect, onDelete, onRetryMessage, dmRecipientPublicKey }: ChatMessagesProps) {
|
||||||
const { messages: hookMessages } = useChat();
|
|
||||||
const { user } = useAppState();
|
const { user } = useAppState();
|
||||||
|
|
||||||
// Use prop messages if provided, otherwise use hook messages
|
// Use prop messages (panels provide their own messages)
|
||||||
const messages = propMessages || hookMessages;
|
|
||||||
const [profileDialogOpen, setProfileDialogOpen] = useState(false);
|
const [profileDialogOpen, setProfileDialogOpen] = useState(false);
|
||||||
const [selectedUserProfile, setSelectedUserProfile] = useState<UserProfile | null>(null);
|
const [selectedUserProfile, setSelectedUserProfile] = useState<UserProfile | null>(null);
|
||||||
const [isLoadingProfile, setIsLoadingProfile] = useState(false);
|
const [isLoadingProfile, setIsLoadingProfile] = useState(false);
|
||||||
@@ -127,7 +124,7 @@ export function ChatMessages({ messages: propMessages, children, isDm = false, o
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="chat-messages" id="chat-messages">
|
<div className="chat-messages" id="chat-messages">
|
||||||
{messages.map((message) => (
|
{messages.map((message: MessageType) => (
|
||||||
<Message
|
<Message
|
||||||
key={message.id}
|
key={message.id}
|
||||||
message={message}
|
message={message}
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import { useChat } from "../../hooks/useChat";
|
import { useAppState } from "../../state";
|
||||||
|
|
||||||
export function ChatTabs() {
|
export function ChatTabs() {
|
||||||
const { activeTab, setActiveTab, setCurrentChat } = useChat();
|
const { chat, setActiveTab, switchToPublicChat } = useAppState();
|
||||||
|
|
||||||
const handleChatClick = (chatName: string) => {
|
|
||||||
setCurrentChat(chatName);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-tabs">
|
<div className="chat-tabs">
|
||||||
<mdui-tabs value={activeTab} full-width onChange={(e: any) => setActiveTab(e.detail.value)}>
|
<mdui-tabs value={chat.activeTab} full-width onChange={(e: any) => setActiveTab(e.detail.value)}>
|
||||||
<mdui-tab value="chats">
|
<mdui-tab value="chats">
|
||||||
Чаты
|
Чаты
|
||||||
</mdui-tab>
|
</mdui-tab>
|
||||||
@@ -29,7 +25,7 @@ export function ChatTabs() {
|
|||||||
headline="Общий чат"
|
headline="Общий чат"
|
||||||
description="Вы: Последнее сообщение"
|
description="Вы: Последнее сообщение"
|
||||||
id="chat-list-chat-1"
|
id="chat-list-chat-1"
|
||||||
onClick={() => handleChatClick("Общий чат")}
|
onClick={async () => await switchToPublicChat("Общий чат")}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
<img src="./src/resources/images/default-avatar.png" alt="" slot="icon" />
|
<img src="./src/resources/images/default-avatar.png" alt="" slot="icon" />
|
||||||
@@ -38,7 +34,7 @@ export function ChatTabs() {
|
|||||||
headline="Общий чат 2"
|
headline="Общий чат 2"
|
||||||
description="Вы: Последнее сообщение"
|
description="Вы: Последнее сообщение"
|
||||||
id="chat-list-chat-2"
|
id="chat-list-chat-2"
|
||||||
onClick={() => handleChatClick("Общий чат 2")}
|
onClick={async () => await switchToPublicChat("Общий чат 2")}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
<img src="./src/resources/images/default-avatar.png" alt="" slot="icon" />
|
<img src="./src/resources/images/default-avatar.png" alt="" slot="icon" />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useDM } from "../../hooks/useDM";
|
import { useDM, type DMUser } from "../../hooks/useDM";
|
||||||
import { useAppState } from "../../state";
|
import { useAppState } from "../../state";
|
||||||
import { fetchUserPublicKey } from "../../../api/dmApi";
|
import { fetchUserPublicKey } from "../../../api/dmApi";
|
||||||
import defaultAvatar from "../../../resources/images/default-avatar.png";
|
import defaultAvatar from "../../../resources/images/default-avatar.png";
|
||||||
@@ -34,7 +34,7 @@ export function DMUsersList() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUserClick = async (user: any) => {
|
async function handleUserClick(user: DMUser) {
|
||||||
if (!user.publicKey) {
|
if (!user.publicKey) {
|
||||||
// Get public key if not already loaded
|
// Get public key if not already loaded
|
||||||
const authToken = useAppState.getState().user.authToken;
|
const authToken = useAppState.getState().user.authToken;
|
||||||
@@ -60,7 +60,7 @@ export function DMUsersList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<mdui-list>
|
<mdui-list>
|
||||||
{dmUsers.map((user) => (
|
{dmUsers.map((user: DMUser) => (
|
||||||
<mdui-list-item
|
<mdui-list-item
|
||||||
key={user.id}
|
key={user.id}
|
||||||
headline={user.username}
|
headline={user.username}
|
||||||
|
|||||||
@@ -37,17 +37,16 @@ function BottomAppBar() {
|
|||||||
|
|
||||||
|
|
||||||
function ChatTabs() {
|
function ChatTabs() {
|
||||||
const { chat, switchToTab, switchToPublicChat } = useAppState();
|
const { chat, setActiveTab, switchToPublicChat } = useAppState();
|
||||||
const { activeTab } = chat;
|
const { activeTab } = chat;
|
||||||
|
|
||||||
const handleChatClick = async (chatName: string) => {
|
async function handleChatClick(chatName: string) {
|
||||||
await switchToPublicChat(chatName);
|
await switchToPublicChat(chatName);
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleTabChange = async (e: FormEvent<Tabs>) => {
|
function handleTabChange(e: FormEvent<Tabs>) {
|
||||||
const tab = (e.target as Tabs).value as ChatTabs;
|
setActiveTab((e.target as Tabs).value as ChatTabs);
|
||||||
await switchToTab(tab);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-tabs">
|
<div className="chat-tabs">
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { useAppState } from "../../state";
|
||||||
import { MessagePanel, type MessagePanelState } from "../../panels/MessagePanel";
|
import { MessagePanel, type MessagePanelState } from "../../panels/MessagePanel";
|
||||||
import { ChatMessages } from "./ChatMessages";
|
import { ChatMessages } from "./ChatMessages";
|
||||||
import { ChatInputWrapper } from "./ChatInputWrapper";
|
import { ChatInputWrapper } from "./ChatInputWrapper";
|
||||||
@@ -10,10 +11,10 @@ import type { DMPanel } from "../../panels/DMPanel";
|
|||||||
|
|
||||||
interface MessagePanelRendererProps {
|
interface MessagePanelRendererProps {
|
||||||
panel: MessagePanel | null;
|
panel: MessagePanel | null;
|
||||||
isChatSwitching: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRendererProps) {
|
export function MessagePanelRenderer({ panel }: MessagePanelRendererProps) {
|
||||||
|
const { applyPendingPanel, chat } = useAppState();
|
||||||
const messagePanelRef = useRef<HTMLDivElement>(null);
|
const messagePanelRef = useRef<HTMLDivElement>(null);
|
||||||
const [panelState, setPanelState] = useState<MessagePanelState | null>(null);
|
const [panelState, setPanelState] = useState<MessagePanelState | null>(null);
|
||||||
const [switchIn, setSwitchIn] = useState(false);
|
const [switchIn, setSwitchIn] = useState(false);
|
||||||
@@ -25,6 +26,7 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
|
|||||||
const [editVisible, setEditVisible] = useState(Boolean(editMessage));
|
const [editVisible, setEditVisible] = useState(Boolean(editMessage));
|
||||||
const [pendingAction, setPendingAction] = useState<null | { type: "reply" | "edit"; message: Message }>(null);
|
const [pendingAction, setPendingAction] = useState<null | { type: "reply" | "edit"; message: Message }>(null);
|
||||||
|
|
||||||
|
|
||||||
// Drag & drop
|
// Drag & drop
|
||||||
const [isDragging, setIsDragging] = useState(false);
|
const [isDragging, setIsDragging] = useState(false);
|
||||||
const dragCounterRef = useRef(0);
|
const dragCounterRef = useRef(0);
|
||||||
@@ -56,31 +58,26 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
|
|||||||
if (panel) {
|
if (panel) {
|
||||||
setPanelState(panel.getState());
|
setPanelState(panel.getState());
|
||||||
|
|
||||||
// Set up state change listener
|
// Store the handler for cleanup
|
||||||
const handleStateChange = (newState: MessagePanelState) => {
|
panel.onStateChange = (newState: MessagePanelState) => {
|
||||||
setPanelState(newState);
|
setPanelState(newState);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store the handler for cleanup
|
|
||||||
panel.onStateChange = handleStateChange;
|
|
||||||
|
|
||||||
// Set up WebSocket message handler for this panel
|
// Set up WebSocket message handler for this panel
|
||||||
if (panel.handleWebSocketMessage) {
|
if (panel.handleWebSocketMessage) {
|
||||||
setGlobalMessageHandler(panel.handleWebSocketMessage);
|
setGlobalMessageHandler(panel.handleWebSocketMessage);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setPanelState(null);
|
setPanelState(null);
|
||||||
// Clear global message handler when no panel is active
|
|
||||||
setGlobalMessageHandler(null);
|
setGlobalMessageHandler(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup function
|
|
||||||
return () => {
|
return () => {
|
||||||
if (panel) {
|
if (panel) {
|
||||||
if (panel.onStateChange) {
|
if (panel.onStateChange) {
|
||||||
panel.onStateChange = null;
|
panel.onStateChange = null;
|
||||||
}
|
}
|
||||||
// Call destroy to clean up pending timeouts
|
|
||||||
if (typeof panel.destroy === 'function') {
|
if (typeof panel.destroy === 'function') {
|
||||||
panel.destroy();
|
panel.destroy();
|
||||||
}
|
}
|
||||||
@@ -88,60 +85,62 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
|
|||||||
};
|
};
|
||||||
}, [panel]);
|
}, [panel]);
|
||||||
|
|
||||||
// Handle chat switching animation
|
// Handle chat switching animation with event listeners
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log("🎬 [DEBUG] MessagePanelRenderer: isChatSwitching changed to:", isChatSwitching);
|
if (chat.isSwitching) {
|
||||||
if (isChatSwitching) {
|
|
||||||
console.log("🎬 [DEBUG] MessagePanelRenderer: Starting switch-out animation");
|
|
||||||
setSwitchOut(true);
|
setSwitchOut(true);
|
||||||
setTimeout(() => {
|
|
||||||
console.log("🎬 [DEBUG] MessagePanelRenderer: Starting switch-in animation");
|
// Use animation event listeners instead of hardcoded delays
|
||||||
setSwitchOut(false);
|
function handleAnimationEnd(event: Event) {
|
||||||
setSwitchIn(true);
|
const animationEvent = event as AnimationEvent;
|
||||||
setTimeout(() => {
|
|
||||||
console.log("🎬 [DEBUG] MessagePanelRenderer: Ending switch-in animation");
|
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);
|
setSwitchIn(false);
|
||||||
}, 200);
|
// End the chat switching state
|
||||||
}, 250);
|
chat.setIsSwitching(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add event listener to document to catch all animation events
|
||||||
|
document.addEventListener('animationend', handleAnimationEnd);
|
||||||
|
|
||||||
|
// Cleanup function
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('animationend', handleAnimationEnd);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}, [isChatSwitching]);
|
}, [chat.isSwitching]);
|
||||||
|
|
||||||
// Scroll to bottom when messages change
|
// Load messages when panel changes and animation is not running
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
if (!chat.activePanel || chat.isSwitching || switchOut || switchIn) return;
|
||||||
}, [panelState?.messages]);
|
|
||||||
|
|
||||||
if (!panel || !panelState) {
|
const panelState = chat.activePanel.getState();
|
||||||
return (
|
|
||||||
<div className="chat-container">
|
if (panelState.messages.length === 0 && !panelState.isLoading) {
|
||||||
<div className="chat-main" id="chat-inner">
|
chat.activePanel.loadMessages();
|
||||||
<div className="chat-header">
|
}
|
||||||
<img src={defaultAvatar} alt="Avatar" className="chat-header-avatar" />
|
}, [chat.activePanel, chat.isSwitching, switchOut, switchIn]);
|
||||||
<div className="chat-header-info">
|
|
||||||
<div className="info-chat">
|
// Scroll to bottom when messages change, but only when no animation is running
|
||||||
<h4 id="chat-name">Выбор чата</h4>
|
useEffect(() => {
|
||||||
<p>
|
if (!panelState || chat.isSwitching || switchOut || switchIn || panelState.isLoading) return;
|
||||||
<span className="online-status"></span>
|
|
||||||
Выберите чат, чтобы начать переписку
|
const el = messagesEndRef.current;
|
||||||
</p>
|
if (!el) return;
|
||||||
</div>
|
|
||||||
</div>
|
// Defer to next frame to ensure layout is stable
|
||||||
</div>
|
const id = requestAnimationFrame(() => {
|
||||||
<div className="chat-messages" id="chat-messages">
|
el.scrollIntoView({ behavior: "smooth", block: "end" });
|
||||||
<div style={{
|
});
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
return () => cancelAnimationFrame(id);
|
||||||
alignItems: "center",
|
}, [panelState?.messages, panelState?.isLoading, chat.isSwitching, switchOut, switchIn]);
|
||||||
height: "100%",
|
|
||||||
color: "var(--mdui-color-on-surface-variant)"
|
|
||||||
}}>
|
|
||||||
Выберите чат на боковой панели, чтобы начать переписку
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`chat-container ${switchIn ? "chat-switch-in" : ""} ${switchOut ? "chat-switch-out" : ""}`}>
|
<div className={`chat-container ${switchIn ? "chat-switch-in" : ""} ${switchOut ? "chat-switch-out" : ""}`}>
|
||||||
@@ -149,7 +148,7 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
|
|||||||
ref={messagePanelRef}
|
ref={messagePanelRef}
|
||||||
className="chat-main"
|
className="chat-main"
|
||||||
id="chat-inner"
|
id="chat-inner"
|
||||||
onDragEnter={(e) => {
|
onDragEnter={panel ? (e) => {
|
||||||
if (!e.dataTransfer) return;
|
if (!e.dataTransfer) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -157,20 +156,20 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
|
|||||||
// Only show overlay when actual files are dragged
|
// Only show overlay when actual files are dragged
|
||||||
const hasFiles = Array.from(e.dataTransfer.types || []).includes("Files");
|
const hasFiles = Array.from(e.dataTransfer.types || []).includes("Files");
|
||||||
if (hasFiles) setIsDragging(true);
|
if (hasFiles) setIsDragging(true);
|
||||||
}}
|
} : undefined}
|
||||||
onDragOver={(e) => {
|
onDragOver={panel ? (e) => {
|
||||||
if (!e.dataTransfer) return;
|
if (!e.dataTransfer) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
e.dataTransfer.dropEffect = "copy";
|
e.dataTransfer.dropEffect = "copy";
|
||||||
}}
|
} : undefined}
|
||||||
onDragLeave={(e) => {
|
onDragLeave={panel ? (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);
|
dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);
|
||||||
if (dragCounterRef.current === 0) setIsDragging(false);
|
if (dragCounterRef.current === 0) setIsDragging(false);
|
||||||
}}
|
} : undefined}
|
||||||
onDrop={(e) => {
|
onDrop={panel ? (e) => {
|
||||||
if (!e.dataTransfer) return;
|
if (!e.dataTransfer) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -180,28 +179,34 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
|
|||||||
}
|
}
|
||||||
setIsDragging(false);
|
setIsDragging(false);
|
||||||
dragCounterRef.current = 0;
|
dragCounterRef.current = 0;
|
||||||
}}>
|
} : undefined}>
|
||||||
<div className="chat-header">
|
<div className="chat-header">
|
||||||
<img
|
<img
|
||||||
src={panelState.profilePicture || defaultAvatar}
|
src={panelState?.profilePicture || defaultAvatar}
|
||||||
alt="Avatar"
|
alt="Avatar"
|
||||||
className="chat-header-avatar"
|
className="chat-header-avatar"
|
||||||
onClick={panel.handleProfileClick}
|
onClick={panel?.handleProfileClick}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: panel ? "pointer" : "default" }}
|
||||||
/>
|
/>
|
||||||
<div className="chat-header-info">
|
<div className="chat-header-info">
|
||||||
<div className="info-chat">
|
<div className="info-chat">
|
||||||
<h4 id="chat-name">{panelState.title}</h4>
|
<h4 id="chat-name">{panelState?.title || "Выбор чата"}</h4>
|
||||||
<p>
|
<p>
|
||||||
<span className={`online-status ${panelState.online ? "online" : "offline"}`}></span>
|
<span className={`online-status ${panelState?.online ? "online" : ""}`}></span>
|
||||||
{panelState.online ? "Online" : "Offline"}
|
{panelState ? (
|
||||||
{panelState.isTyping && " • Typing..."}
|
<>
|
||||||
|
{panelState.online ? "Online" : "Offline"}
|
||||||
|
{panelState.isTyping && " • Typing..."}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Выберите чат, чтобы начать переписку"
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{panelState.isLoading ? (
|
{panelState?.isLoading ? (
|
||||||
<div className="chat-messages" id="chat-messages">
|
<div className="chat-messages" id="chat-messages">
|
||||||
<div style={{
|
<div style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -213,7 +218,7 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
|
|||||||
Загрузка сообщений...
|
Загрузка сообщений...
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
): (
|
) : panelState && panel ? (
|
||||||
<ChatMessages
|
<ChatMessages
|
||||||
messages={panelState.messages}
|
messages={panelState.messages}
|
||||||
isDm={panel.isDm()}
|
isDm={panel.isDm()}
|
||||||
@@ -239,61 +244,77 @@ export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRen
|
|||||||
>
|
>
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</ChatMessages>
|
</ChatMessages>
|
||||||
)}
|
) : (
|
||||||
|
<div className="chat-messages" id="chat-messages">
|
||||||
<AnimatedOpacity
|
<div style={{
|
||||||
visible={isDragging}
|
display: "flex",
|
||||||
className="file-overlay"
|
justifyContent: "center",
|
||||||
onDragOver={(e) => e.preventDefault()}
|
alignItems: "center",
|
||||||
onDrop={(e) => e.preventDefault()}>
|
height: "100%",
|
||||||
<div className="file-overlay-wrapper">
|
color: "var(--mdui-color-on-surface-variant)"
|
||||||
<div className="file-overlay-inner">
|
}}>
|
||||||
<mdui-icon name="upload_file" />
|
Выберите чат на боковой панели, чтобы начать переписку
|
||||||
<span>Отпустите файл(ы) для добавления</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AnimatedOpacity>
|
)}
|
||||||
|
|
||||||
<ChatInputWrapper
|
{panel && (
|
||||||
onSendMessage={(text, files) => {
|
<>
|
||||||
panel.handleSendMessage(text, replyTo?.id, files);
|
<AnimatedOpacity
|
||||||
setReplyTo(null);
|
visible={isDragging}
|
||||||
}}
|
className="file-overlay"
|
||||||
onSaveEdit={(content) => {
|
onDragOver={(e) => e.preventDefault()}
|
||||||
if (editMessage) {
|
onDrop={(e) => e.preventDefault()}>
|
||||||
panel.handleEditMessage(editMessage.id, content);
|
<div className="file-overlay-wrapper">
|
||||||
setEditMessage(null);
|
<div className="file-overlay-inner">
|
||||||
}
|
<mdui-icon name="upload_file" />
|
||||||
}}
|
<span>Отпустите файл(ы) для добавления</span>
|
||||||
replyTo={replyTo}
|
</div>
|
||||||
replyToVisible={replyToVisible}
|
</div>
|
||||||
onClearReply={() => {
|
</AnimatedOpacity>
|
||||||
setPendingAction(null);
|
|
||||||
setReplyToVisible(false);
|
<ChatInputWrapper
|
||||||
}}
|
onSendMessage={(text, files) => {
|
||||||
onCloseReply={() => {
|
panel.handleSendMessage(text, replyTo?.id, files);
|
||||||
setReplyTo(null);
|
setReplyTo(null);
|
||||||
if (pendingAction && pendingAction.type === "edit") {
|
}}
|
||||||
setEditMessage(pendingAction.message);
|
onSaveEdit={(content) => {
|
||||||
setPendingAction(null);
|
if (editMessage) {
|
||||||
}
|
panel.handleEditMessage(editMessage.id, content);
|
||||||
}}
|
setEditMessage(null);
|
||||||
editingMessage={editMessage}
|
}
|
||||||
editVisible={editVisible}
|
}}
|
||||||
onClearEdit={() => {
|
replyTo={replyTo}
|
||||||
setPendingAction(null);
|
replyToVisible={replyToVisible}
|
||||||
setEditVisible(false);
|
onClearReply={() => {
|
||||||
}}
|
setPendingAction(null);
|
||||||
onCloseEdit={() => {
|
setReplyToVisible(false);
|
||||||
setEditMessage(null);
|
}}
|
||||||
if (pendingAction && pendingAction.type === "reply") {
|
onCloseReply={() => {
|
||||||
setReplyTo(pendingAction.message);
|
setReplyTo(null);
|
||||||
setPendingAction(null);
|
if (pendingAction && pendingAction.type === "edit") {
|
||||||
}
|
setEditMessage(pendingAction.message);
|
||||||
}}
|
setPendingAction(null);
|
||||||
onProvideFileAdder={(adder) => { addFilesRef.current = adder; }}
|
}
|
||||||
messagePanelRef={messagePanelRef}
|
}}
|
||||||
/>
|
editingMessage={editMessage}
|
||||||
|
editVisible={editVisible}
|
||||||
|
onClearEdit={() => {
|
||||||
|
setPendingAction(null);
|
||||||
|
setEditVisible(false);
|
||||||
|
}}
|
||||||
|
onCloseEdit={() => {
|
||||||
|
setEditMessage(null);
|
||||||
|
if (pendingAction && pendingAction.type === "reply") {
|
||||||
|
setReplyTo(pendingAction.message);
|
||||||
|
setPendingAction(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onProvideFileAdder={(adder) => { addFilesRef.current = adder; }}
|
||||||
|
messagePanelRef={messagePanelRef}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,10 +4,5 @@ import { MessagePanelRenderer } from "./MessagePanelRenderer";
|
|||||||
export function RightPanel() {
|
export function RightPanel() {
|
||||||
const { chat } = useAppState();
|
const { chat } = useAppState();
|
||||||
|
|
||||||
return (
|
return <MessagePanelRenderer panel={chat.activePanel} />
|
||||||
<MessagePanelRenderer
|
|
||||||
panel={chat.activePanel}
|
|
||||||
isChatSwitching={chat.isChatSwitching}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ import defaultAvatar from "../../../resources/images/default-avatar.png";
|
|||||||
import type { TextField } from "mdui/components/text-field";
|
import type { TextField } from "mdui/components/text-field";
|
||||||
import type { DialogProps } from "../../../core/types";
|
import type { DialogProps } from "../../../core/types";
|
||||||
import { MaterialDialog } from "../core/Dialog";
|
import { MaterialDialog } from "../core/Dialog";
|
||||||
import { useProfile } from "../../hooks/useProfile";
|
import useProfile from "../../hooks/useProfile";
|
||||||
import { ImageCropper } from "./ImageCropper";
|
import { ImageCropper } from "./ImageCropper";
|
||||||
import { MaterialTextField } from "../core/TextField";
|
import { MaterialTextField } from "../core/TextField";
|
||||||
|
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
import { useEffect, useCallback, useRef } from "react";
|
|
||||||
import { useAppState } from "../state";
|
|
||||||
import { request } from "../../core/websocket";
|
|
||||||
import { API_BASE_URL } from "../../core/config";
|
|
||||||
import type { Message } from "../../core/types";
|
|
||||||
import { getAuthHeaders } from "../../auth/api";
|
|
||||||
|
|
||||||
export function useChat() {
|
|
||||||
const {
|
|
||||||
chat,
|
|
||||||
addMessage,
|
|
||||||
updateMessage,
|
|
||||||
removeMessage,
|
|
||||||
clearMessages,
|
|
||||||
setCurrentChat,
|
|
||||||
setActiveTab,
|
|
||||||
setDmUsers,
|
|
||||||
setActiveDm,
|
|
||||||
setIsChatSwitching,
|
|
||||||
user
|
|
||||||
} = useAppState();
|
|
||||||
|
|
||||||
const messagesLoadedRef = useRef(false);
|
|
||||||
|
|
||||||
// Load messages for the current chat
|
|
||||||
const loadMessages = useCallback(async () => {
|
|
||||||
if (!user.authToken || messagesLoadedRef.current) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${API_BASE_URL}/get_messages`, {
|
|
||||||
headers: getAuthHeaders(user.authToken)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
if (data.messages && data.messages.length > 0) {
|
|
||||||
// Clear existing messages and add new ones
|
|
||||||
clearMessages();
|
|
||||||
data.messages.forEach((msg: Message) => {
|
|
||||||
addMessage(msg);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
messagesLoadedRef.current = true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error loading messages:", error);
|
|
||||||
}
|
|
||||||
}, [user.authToken, addMessage, clearMessages]);
|
|
||||||
|
|
||||||
// Send a message
|
|
||||||
const sendMessage = useCallback(async (content: string) => {
|
|
||||||
if (!user.authToken || !content.trim()) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await request({
|
|
||||||
data: { content: content.trim() },
|
|
||||||
credentials: {
|
|
||||||
scheme: "Bearer",
|
|
||||||
credentials: user.authToken
|
|
||||||
},
|
|
||||||
type: "sendMessage"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.error) {
|
|
||||||
console.error("Error sending message:", response.error);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error sending message:", error);
|
|
||||||
}
|
|
||||||
}, [user.authToken]);
|
|
||||||
|
|
||||||
// WebSocket messages are now handled by the active panel
|
|
||||||
// No need for duplicate handling here
|
|
||||||
|
|
||||||
// Load messages only once when component mounts and user is authenticated
|
|
||||||
useEffect(() => {
|
|
||||||
if (user.authToken && !messagesLoadedRef.current) {
|
|
||||||
loadMessages();
|
|
||||||
}
|
|
||||||
}, [user.authToken, loadMessages]);
|
|
||||||
|
|
||||||
// Reset messages loaded flag and clear messages when chat changes
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("🔄 [DEBUG] useChat useEffect triggered for chat change:", chat.currentChat);
|
|
||||||
console.log("📝 [DEBUG] useChat: Resetting messages loaded flag and clearing messages");
|
|
||||||
messagesLoadedRef.current = false;
|
|
||||||
clearMessages(); // Clear messages when switching chats
|
|
||||||
console.log("📥 [DEBUG] useChat: Loading messages for new chat");
|
|
||||||
loadMessages();
|
|
||||||
}, [chat.currentChat, clearMessages]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
messages: chat.messages,
|
|
||||||
currentChat: chat.currentChat,
|
|
||||||
activeTab: chat.activeTab,
|
|
||||||
dmUsers: chat.dmUsers,
|
|
||||||
activeDm: chat.activeDm,
|
|
||||||
isChatSwitching: chat.isChatSwitching,
|
|
||||||
setIsChatSwitching,
|
|
||||||
sendMessage,
|
|
||||||
updateMessage,
|
|
||||||
removeMessage,
|
|
||||||
clearMessages,
|
|
||||||
setCurrentChat,
|
|
||||||
setActiveTab,
|
|
||||||
setDmUsers,
|
|
||||||
setActiveDm
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
import type { User, Message, DmEncryptedJSON } from "../../core/types";
|
import type { User, Message, DmEncryptedJSON } from "../../core/types";
|
||||||
import { websocket } from "../../core/websocket";
|
import { websocket } from "../../core/websocket";
|
||||||
|
|
||||||
interface DMUser extends User {
|
export interface DMUser extends User {
|
||||||
lastMessage?: string;
|
lastMessage?: string;
|
||||||
unreadCount: number;
|
unreadCount: number;
|
||||||
publicKey?: string | null;
|
publicKey?: string | null;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useAppState } from "../state";
|
|||||||
import { loadProfile, updateProfile, uploadProfilePicture, type ProfileData } from "../../api/profileApi";
|
import { loadProfile, updateProfile, uploadProfilePicture, type ProfileData } from "../../api/profileApi";
|
||||||
import { showSuccess, showError } from "../../utils/notification";
|
import { showSuccess, showError } from "../../utils/notification";
|
||||||
|
|
||||||
export function useProfile() {
|
export default function useProfile() {
|
||||||
const { user } = useAppState();
|
const { user } = useAppState();
|
||||||
const [profileData, setProfileData] = useState<ProfileData | null>(null);
|
const [profileData, setProfileData] = useState<ProfileData | null>(null);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|||||||
@@ -33,15 +33,19 @@ export class DMPanel extends MessagePanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async activate(): Promise<void> {
|
async activate(): Promise<void> {
|
||||||
if (this.dmData && !this.messagesLoaded) {
|
// Don't load messages immediately during activation to prevent animation freeze
|
||||||
await this.loadMessages();
|
// Messages will be loaded after the animation completes
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deactivate(): void {
|
deactivate(): void {
|
||||||
// DM doesn't need special cleanup
|
// DM doesn't need special cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearMessages(): void {
|
||||||
|
super.clearMessages();
|
||||||
|
this.messagesLoaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
private async parseTextPayload(env: DmEnvelope, decryptedMessages: Message[]) {
|
private async parseTextPayload(env: DmEnvelope, decryptedMessages: Message[]) {
|
||||||
const plaintext = await decryptDm(env, this.dmData!.publicKey);
|
const plaintext = await decryptDm(env, this.dmData!.publicKey);
|
||||||
const isAuthor = env.senderId !== this.dmData!.userId;
|
const isAuthor = env.senderId !== this.dmData!.userId;
|
||||||
@@ -164,7 +168,7 @@ export class DMPanel extends MessagePanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle incoming WebSocket DM messages
|
// Handle incoming WebSocket DM messages
|
||||||
handleWebSocketMessage = async (response: DMWebSocketMessage): Promise<void> => {
|
async handleWebSocketMessage(response: DMWebSocketMessage): Promise<void> {
|
||||||
if (response.type === "dmNew" && this.dmData) {
|
if (response.type === "dmNew" && this.dmData) {
|
||||||
const envelope = response.data;
|
const envelope = response.data;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Message } from "../../core/types";
|
import type { Message, WebSocketMessage } from "../../core/types";
|
||||||
import type { UserState } from "../state";
|
import type { UserState } from "../state";
|
||||||
|
|
||||||
export interface MessagePanelState {
|
export interface MessagePanelState {
|
||||||
@@ -46,9 +46,7 @@ export abstract class MessagePanel {
|
|||||||
abstract loadMessages(): Promise<void>;
|
abstract loadMessages(): Promise<void>;
|
||||||
protected abstract sendMessage(content: string, replyToId?: number, files?: File[]): Promise<void>;
|
protected abstract sendMessage(content: string, replyToId?: number, files?: File[]): Promise<void>;
|
||||||
abstract isDm(): boolean;
|
abstract isDm(): boolean;
|
||||||
|
abstract handleWebSocketMessage(response: WebSocketMessage<any>): Promise<void>;
|
||||||
// Optional WebSocket message handler (can be overridden by subclasses)
|
|
||||||
handleWebSocketMessage?: (response: any) => void;
|
|
||||||
|
|
||||||
// Common methods
|
// Common methods
|
||||||
protected updateState(updates: Partial<MessagePanelState>): void {
|
protected updateState(updates: Partial<MessagePanelState>): void {
|
||||||
|
|||||||
@@ -24,15 +24,19 @@ export class PublicChatPanel extends MessagePanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async activate(): Promise<void> {
|
async activate(): Promise<void> {
|
||||||
if (!this.messagesLoaded) {
|
// Don't load messages immediately during activation to prevent animation freeze
|
||||||
await this.loadMessages();
|
// Messages will be loaded after the animation completes
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deactivate(): void {
|
deactivate(): void {
|
||||||
// Public chat doesn't need special cleanup
|
// Public chat doesn't need special cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearMessages(): void {
|
||||||
|
super.clearMessages();
|
||||||
|
this.messagesLoaded = false;
|
||||||
|
}
|
||||||
|
|
||||||
async loadMessages(): Promise<void> {
|
async loadMessages(): Promise<void> {
|
||||||
if (!this.currentUser.authToken || this.messagesLoaded) return;
|
if (!this.currentUser.authToken || this.messagesLoaded) return;
|
||||||
|
|
||||||
@@ -100,7 +104,7 @@ export class PublicChatPanel extends MessagePanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle incoming WebSocket messages
|
// Handle incoming WebSocket messages
|
||||||
handleWebSocketMessage = (response: ChatWebSocketMessage): void => {
|
async handleWebSocketMessage(response: ChatWebSocketMessage): Promise<void> {
|
||||||
switch (response.type) {
|
switch (response.type) {
|
||||||
case 'messageEdited':
|
case 'messageEdited':
|
||||||
if (response.data) {
|
if (response.data) {
|
||||||
|
|||||||
@@ -24,10 +24,12 @@ interface ChatState {
|
|||||||
activeTab: ChatTabs;
|
activeTab: ChatTabs;
|
||||||
dmUsers: User[];
|
dmUsers: User[];
|
||||||
activeDm: ActiveDM | null;
|
activeDm: ActiveDM | null;
|
||||||
isChatSwitching: boolean;
|
isSwitching: boolean;
|
||||||
|
setIsSwitching: (value: boolean) => void;
|
||||||
activePanel: MessagePanel | null;
|
activePanel: MessagePanel | null;
|
||||||
publicChatPanel: PublicChatPanel | null;
|
publicChatPanel: PublicChatPanel | null;
|
||||||
dmPanel: DMPanel | null;
|
dmPanel: DMPanel | null;
|
||||||
|
pendingPanel?: MessagePanel | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserState {
|
export interface UserState {
|
||||||
@@ -46,11 +48,11 @@ interface AppState {
|
|||||||
setDmUsers: (users: User[]) => void;
|
setDmUsers: (users: User[]) => void;
|
||||||
setActiveDm: (dm: ChatState["activeDm"]) => void;
|
setActiveDm: (dm: ChatState["activeDm"]) => void;
|
||||||
clearMessages: () => void;
|
clearMessages: () => void;
|
||||||
setIsChatSwitching: (value: boolean) => void;
|
|
||||||
setActivePanel: (panel: MessagePanel | null) => void;
|
setActivePanel: (panel: MessagePanel | null) => void;
|
||||||
|
setPendingPanel: (panel: MessagePanel | null) => void;
|
||||||
|
applyPendingPanel: () => void;
|
||||||
switchToPublicChat: (chatName: string) => Promise<void>;
|
switchToPublicChat: (chatName: string) => Promise<void>;
|
||||||
switchToDM: (dmData: DMPanelData) => Promise<void>;
|
switchToDM: (dmData: DMPanelData) => Promise<void>;
|
||||||
switchToTab: (tab: ChatTabs) => Promise<void>;
|
|
||||||
|
|
||||||
// User state
|
// User state
|
||||||
user: UserState;
|
user: UserState;
|
||||||
@@ -67,19 +69,17 @@ export const useAppState = create<AppState>((set, get) => ({
|
|||||||
activeTab: "chats",
|
activeTab: "chats",
|
||||||
dmUsers: [],
|
dmUsers: [],
|
||||||
activeDm: null,
|
activeDm: null,
|
||||||
isChatSwitching: false,
|
isSwitching: false,
|
||||||
activePanel: null,
|
setIsSwitching: (value: boolean) => set((state) => ({
|
||||||
publicChatPanel: null,
|
|
||||||
dmPanel: null
|
|
||||||
},
|
|
||||||
setIsChatSwitching: (value: boolean) => {
|
|
||||||
console.log("🎬 [DEBUG] setIsChatSwitching called with:", value);
|
|
||||||
set((state) => ({
|
|
||||||
chat: {
|
chat: {
|
||||||
...state.chat,
|
...state.chat,
|
||||||
isChatSwitching: value
|
isSwitching: value
|
||||||
}
|
}
|
||||||
}));
|
})),
|
||||||
|
activePanel: null,
|
||||||
|
publicChatPanel: null,
|
||||||
|
dmPanel: null,
|
||||||
|
pendingPanel: null
|
||||||
},
|
},
|
||||||
addMessage: (message: Message) => set((state) => {
|
addMessage: (message: Message) => set((state) => {
|
||||||
// Check if message already exists to prevent duplicates
|
// Check if message already exists to prevent duplicates
|
||||||
@@ -258,104 +258,95 @@ export const useAppState = create<AppState>((set, get) => ({
|
|||||||
activePanel: panel
|
activePanel: panel
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
|
// Stash a panel to be applied after switch-out animation ends
|
||||||
|
setPendingPanel: (panel: MessagePanel | null) => set((state) => ({
|
||||||
|
chat: {
|
||||||
|
...state.chat,
|
||||||
|
pendingPanel: panel
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
// Apply pending panel atomically and update related fields
|
||||||
|
applyPendingPanel: () => set((state) => ({
|
||||||
|
chat: {
|
||||||
|
...state.chat,
|
||||||
|
activePanel: state.chat.pendingPanel || state.chat.activePanel,
|
||||||
|
// when switching to public chat, keep reference if type matches
|
||||||
|
publicChatPanel: (state.chat.pendingPanel instanceof PublicChatPanel)
|
||||||
|
? (state.chat.pendingPanel as PublicChatPanel)
|
||||||
|
: state.chat.publicChatPanel,
|
||||||
|
dmPanel: (state.chat.pendingPanel instanceof DMPanel)
|
||||||
|
? (state.chat.pendingPanel as DMPanel)
|
||||||
|
: state.chat.dmPanel,
|
||||||
|
// update currentChat from panel title if available
|
||||||
|
currentChat: state.chat.pendingPanel ? state.chat.pendingPanel.getState().title || state.chat.currentChat : state.chat.currentChat,
|
||||||
|
pendingPanel: null
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
|
||||||
switchToPublicChat: async (chatName: string) => {
|
switchToPublicChat: async (chatName: string) => {
|
||||||
console.log("🔄 [DEBUG] switchToPublicChat called with:", chatName);
|
const { user, chat } = get();
|
||||||
const state = get();
|
|
||||||
const { user, chat } = state;
|
|
||||||
|
|
||||||
if (!user.authToken) {
|
if (!user.authToken) return;
|
||||||
console.log("❌ [DEBUG] No auth token, returning early");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🎬 [DEBUG] Starting chat switching animation for:", chatName);
|
|
||||||
console.log("🎬 [DEBUG] Current isChatSwitching state:", chat.isChatSwitching);
|
|
||||||
|
|
||||||
// Start chat switching animation
|
// Start chat switching animation
|
||||||
state.setIsChatSwitching(true);
|
chat.setIsSwitching(true);
|
||||||
|
|
||||||
// Create or get public chat panel
|
// Create or get public chat panel
|
||||||
let publicChatPanel = chat.publicChatPanel;
|
let publicChatPanel = chat.publicChatPanel;
|
||||||
if (!publicChatPanel) {
|
if (!publicChatPanel) {
|
||||||
console.log("🆕 [DEBUG] Creating new PublicChatPanel for:", chatName);
|
|
||||||
publicChatPanel = new PublicChatPanel(chatName, user);
|
publicChatPanel = new PublicChatPanel(chatName, user);
|
||||||
} else {
|
} else {
|
||||||
console.log("♻️ [DEBUG] Reusing existing PublicChatPanel, setting chat name to:", chatName);
|
|
||||||
publicChatPanel.setChatName(chatName);
|
publicChatPanel.setChatName(chatName);
|
||||||
publicChatPanel.setAuthToken(user.authToken);
|
publicChatPanel.setAuthToken(user.authToken);
|
||||||
|
// Reset messages for the new chat
|
||||||
|
publicChatPanel.clearMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("⏳ [DEBUG] Waiting 250ms for animation...");
|
|
||||||
// Wait for animation
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 250));
|
|
||||||
|
|
||||||
console.log("🚀 [DEBUG] Activating panel...");
|
|
||||||
// Activate panel
|
// Activate panel
|
||||||
await publicChatPanel.activate();
|
await publicChatPanel.activate();
|
||||||
|
|
||||||
console.log("📝 [DEBUG] Updating state with new panel and chat name");
|
// Defer panel swap until animation switch-out completes
|
||||||
// Update state
|
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
chat: {
|
chat: {
|
||||||
...state.chat,
|
...state.chat,
|
||||||
activePanel: publicChatPanel,
|
pendingPanel: publicChatPanel,
|
||||||
publicChatPanel: publicChatPanel,
|
|
||||||
currentChat: chatName,
|
|
||||||
activeTab: "chats"
|
activeTab: "chats"
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log("✅ [DEBUG] Ending chat switching animation");
|
// Let MessagePanelRenderer handle the animation timing completely
|
||||||
// End animation
|
// It will set isChatSwitching to false when the fadeInDown animation completes
|
||||||
state.setIsChatSwitching(false);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
switchToDM: async (dmData: DMPanelData) => {
|
switchToDM: async (dmData: DMPanelData) => {
|
||||||
console.log("🔄 [DEBUG] switchToDM called with:", dmData);
|
const { user, chat } = get();
|
||||||
const state = get();
|
|
||||||
const { user, chat } = state;
|
|
||||||
|
|
||||||
if (!user.authToken) {
|
if (!user.authToken) return;
|
||||||
console.log("❌ [DEBUG] No auth token, returning early");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("🎬 [DEBUG] Starting DM switching animation for user:", dmData.username);
|
|
||||||
console.log("🎬 [DEBUG] Current isChatSwitching state:", chat.isChatSwitching);
|
|
||||||
|
|
||||||
// Start chat switching animation
|
// Start chat switching animation
|
||||||
state.setIsChatSwitching(true);
|
chat.setIsSwitching(true);
|
||||||
|
|
||||||
// Create or get DM panel
|
// Create or get DM panel
|
||||||
let dmPanel = chat.dmPanel;
|
let dmPanel = chat.dmPanel;
|
||||||
if (!dmPanel) {
|
if (!dmPanel) {
|
||||||
console.log("🆕 [DEBUG] Creating new DMPanel for user:", dmData.username);
|
|
||||||
dmPanel = new DMPanel(user);
|
dmPanel = new DMPanel(user);
|
||||||
} else {
|
} else {
|
||||||
console.log("♻️ [DEBUG] Reusing existing DMPanel, updating auth token");
|
|
||||||
dmPanel.setAuthToken(user.authToken);
|
dmPanel.setAuthToken(user.authToken);
|
||||||
|
// Reset messages for the new DM
|
||||||
|
dmPanel.clearMessages();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set DM data
|
// Set DM data
|
||||||
console.log("📝 [DEBUG] Setting DM data for user:", dmData.username);
|
|
||||||
dmPanel.setDMData(dmData);
|
dmPanel.setDMData(dmData);
|
||||||
|
|
||||||
console.log("⏳ [DEBUG] Waiting 250ms for animation...");
|
|
||||||
// Wait for animation
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 250));
|
|
||||||
|
|
||||||
console.log("🚀 [DEBUG] Activating DM panel...");
|
|
||||||
// Activate panel
|
// Activate panel
|
||||||
await dmPanel.activate();
|
await dmPanel.activate();
|
||||||
|
|
||||||
console.log("📝 [DEBUG] Updating state with new DM panel");
|
// Defer panel swap until animation switch-out completes
|
||||||
// Update state
|
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
chat: {
|
chat: {
|
||||||
...state.chat,
|
...state.chat,
|
||||||
activePanel: dmPanel,
|
pendingPanel: dmPanel,
|
||||||
dmPanel: dmPanel,
|
|
||||||
activeDm: {
|
activeDm: {
|
||||||
userId: dmData.userId,
|
userId: dmData.userId,
|
||||||
username: dmData.username,
|
username: dmData.username,
|
||||||
@@ -365,24 +356,7 @@ export const useAppState = create<AppState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log("✅ [DEBUG] Ending DM switching animation");
|
// Let MessagePanelRenderer handle the animation timing completely
|
||||||
// End animation
|
// It will set isChatSwitching to false when the fadeInDown animation completes
|
||||||
state.setIsChatSwitching(false);
|
|
||||||
},
|
|
||||||
|
|
||||||
switchToTab: async (tab: ChatTabs) => {
|
|
||||||
console.log("🔄 [DEBUG] switchToTab called with:", tab);
|
|
||||||
const state = get();
|
|
||||||
console.log("📝 [DEBUG] Setting active tab to:", tab);
|
|
||||||
state.setActiveTab(tab);
|
|
||||||
|
|
||||||
if (tab === "chats") {
|
|
||||||
console.log("💬 [DEBUG] Switching to chats tab, calling switchToPublicChat");
|
|
||||||
await state.switchToPublicChat("Общий чат");
|
|
||||||
} else if (tab === "dms") {
|
|
||||||
console.log("💬 [DEBUG] Switching to DMs tab, clearing active panel");
|
|
||||||
// DM tab - no specific panel until user is selected
|
|
||||||
state.setActivePanel(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
Reference in New Issue
Block a user