Implement chat panels

This commit is contained in:
2025-09-05 22:36:24 +03:00
Unverified
parent 04d3e4d995
commit cad42e1584
11 changed files with 775 additions and 55 deletions
@@ -1,14 +1,22 @@
import { useState } from "react";
import { useChat } from "../../hooks/useChat";
export function ChatInputWrapper() {
interface ChatInputWrapperProps {
onSendMessage?: (message: string) => void;
}
export function ChatInputWrapper({ onSendMessage }: ChatInputWrapperProps) {
const [message, setMessage] = useState("");
const { sendMessage } = useChat();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (message.trim()) {
await sendMessage(message);
if (onSendMessage) {
onSendMessage(message);
} else {
await sendMessage(message);
}
setMessage("");
}
};
@@ -10,9 +10,16 @@ import { useState } from "react";
import { delay } from "../../../utils/utils";
import { request } from "../../../websocket";
export function ChatMessages() {
const { messages } = useChat();
interface ChatMessagesProps {
messages?: MessageType[];
}
export function ChatMessages({ messages: propMessages }: ChatMessagesProps) {
const { messages: hookMessages } = useChat();
const { user } = useAppState();
// Use prop messages if provided, otherwise use hook messages
const messages = propMessages || hookMessages;
const [profileDialogOpen, setProfileDialogOpen] = useState(false);
const [selectedUserProfile, setSelectedUserProfile] = useState<UserProfile | null>(null);
const [isLoadingProfile, setIsLoadingProfile] = useState(false);
@@ -1,11 +1,12 @@
import { useEffect } from "react";
import { useDM } from "../../../hooks/useDM";
import { useAppState } from "../../state";
import { fetchUserPublicKey } from "../../../api/dmApi";
import defaultAvatar from "../../../resources/images/default-avatar.png";
export function DMUsersList() {
const { dmUsers, isLoadingUsers, loadUsers, startDMConversation } = useDM();
const { chat } = useAppState();
const { dmUsers, isLoadingUsers, loadUsers } = useDM();
const { chat, switchToDM } = useAppState();
useEffect(() => {
if (chat.activeTab === "dms") {
@@ -33,6 +34,33 @@ export function DMUsersList() {
);
}
const handleUserClick = async (user: any) => {
if (!user.publicKey) {
// Get public key if not already loaded
const authToken = useAppState.getState().user.authToken;
if (!authToken) {
console.error("No auth token available");
return;
}
const publicKey = await fetchUserPublicKey(user.id, authToken);
if (publicKey) {
user.publicKey = publicKey;
} else {
console.error("Failed to get public key for user:", user.id);
return;
}
}
await switchToDM({
userId: user.id,
username: user.username,
publicKey: user.publicKey,
profilePicture: user.profile_picture,
online: user.online || false
});
};
return (
<mdui-list>
{dmUsers.map((user) => (
@@ -40,7 +68,7 @@ export function DMUsersList() {
key={user.id}
headline={user.username}
description={user.lastMessage || "Нет сообщений"}
onClick={() => startDMConversation(user)}
onClick={() => handleUserClick(user)}
style={{ cursor: "pointer" }}
>
<img
@@ -1,6 +1,7 @@
import { PRODUCT_NAME } from "../../../core/config";
import { useDialog } from "../../contexts/DialogContext";
import { useChat } from "../../hooks/useChat";
import { useAppState } from "../../state";
import defaultAvatar from "../../../resources/images/default-avatar.png";
import { useState, type FormEvent } from "react";
import { ProfileDialog } from "../profile/ProfileDialog";
@@ -27,14 +28,16 @@ function BottomAppBar() {
function ChatTabs() {
const { activeTab, setActiveTab, setCurrentChat } = useChat();
const { chat, switchToTab, switchToPublicChat } = useAppState();
const { activeTab } = chat;
const handleChatClick = (chatName: string) => {
setCurrentChat(chatName);
const handleChatClick = async (chatName: string) => {
await switchToPublicChat(chatName);
};
const handleTabChange = (e: FormEvent<Tabs>) => {
setActiveTab((e.target as Tabs).value as ChatTabs);
const handleTabChange = async (e: FormEvent<Tabs>) => {
const tab = (e.target as Tabs).value as ChatTabs;
await switchToTab(tab);
};
return (
@@ -0,0 +1,132 @@
import { useState, useEffect, useRef } from "react";
import { MessagePanel, type MessagePanelState } from "../../panels/MessagePanel";
import { ChatMainHeader } from "./ChatMainHeader";
import { ChatMessages } from "./ChatMessages";
import { ChatInputWrapper } from "./ChatInputWrapper";
import defaultAvatar from "../../../resources/images/default-avatar.png";
interface MessagePanelRendererProps {
panel: MessagePanel | null;
isChatSwitching: boolean;
}
export function MessagePanelRenderer({ panel, isChatSwitching }: MessagePanelRendererProps) {
const [panelState, setPanelState] = useState<MessagePanelState | null>(null);
const [switchIn, setSwitchIn] = useState(false);
const [switchOut, setSwitchOut] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
// Handle panel state changes
useEffect(() => {
if (panel) {
setPanelState(panel.getState());
// Set up state change listener
const handleStateChange = (newState: MessagePanelState) => {
setPanelState(newState);
};
// Store the handler for cleanup
(panel as any).onStateChange = handleStateChange;
} else {
setPanelState(null);
}
}, [panel]);
// Handle chat switching animation
useEffect(() => {
if (isChatSwitching) {
setSwitchOut(true);
setTimeout(() => {
setSwitchOut(false);
setSwitchIn(true);
setTimeout(() => setSwitchIn(false), 200);
}, 250);
}
}, [isChatSwitching]);
// Scroll to bottom when messages change
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [panelState?.messages]);
if (!panel || !panelState) {
return (
<div className="chat-container">
<div className="chat-main" id="chat-inner">
<div className="chat-header">
<img src={defaultAvatar} alt="Avatar" className="chat-header-avatar" />
<div className="chat-header-info">
<div className="info-chat">
<h4 id="chat-name">Select a chat</h4>
<p>
<span className="online-status"></span>
Choose a chat to start messaging
</p>
</div>
</div>
</div>
<div className="chat-messages" id="chat-messages">
<div style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
color: "var(--mdui-color-on-surface-variant)"
}}>
Select a chat from the sidebar to start messaging
</div>
</div>
</div>
</div>
);
}
return (
<div className={`chat-container ${switchIn ? "chat-switch-in" : ""} ${switchOut ? "chat-switch-out" : ""}`}>
<div className="chat-main" id="chat-inner">
<div className="chat-header">
<img
src={panelState.profilePicture || defaultAvatar}
alt="Avatar"
className="chat-header-avatar"
onClick={panel.handleProfileClick}
style={{ cursor: "pointer" }}
/>
<div className="chat-header-info">
<div className="info-chat">
<h4 id="chat-name">{panelState.title}</h4>
<p>
<span className={`online-status ${panelState.online ? "online" : "offline"}`}></span>
{panelState.online ? "Online" : "Offline"}
{panelState.isTyping && " • Typing..."}
</p>
</div>
<a href="#" id="hide-chat">Свернуть чат</a>
</div>
</div>
<div className="chat-messages" id="chat-messages">
{panelState.isLoading ? (
<div style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%",
color: "var(--mdui-color-on-surface-variant)"
}}>
Loading messages...
</div>
) : (
<>
<ChatMessages messages={panelState.messages} />
<div ref={messagesEndRef} />
</>
)}
</div>
<ChatInputWrapper onSendMessage={panel.handleSendMessage} />
</div>
</div>
);
}
+5 -40
View File
@@ -1,48 +1,13 @@
import { useEffect, useState } from "react";
import { useChat } from "../../hooks/useChat";
import { useAppState } from "../../state";
import { ChatInputWrapper } from "./ChatInputWrapper";
import { ChatMainHeader } from "./ChatMainHeader";
import { ChatMessages } from "./ChatMessages";
import { DMPanel } from "./DMPanel";
import { delay } from "../../../utils/utils";
import { MessagePanelRenderer } from "./MessagePanelRenderer";
export function RightPanel() {
const { isChatSwitching } = useChat();
const { chat } = useAppState();
const [switchIn, setSwitchIn] = useState(false);
const [switchOut, setSwitchOut] = useState(false);
useEffect(() => {
(async () => {
if (isChatSwitching) {
setSwitchOut(true);
} else {
setSwitchIn(true);
await delay(200);
setSwitchIn(false);
setSwitchOut(false);
}
})();
}, [isChatSwitching])
let content: React.ReactNode;
if (chat.activeTab === "dms") {
content = <DMPanel />
} else {
content = (
<div className="chat-main" id="chat-inner">
<ChatMainHeader />
<ChatMessages />
<ChatInputWrapper />
</div>
)
}
return (
<div className={`chat-container ${switchIn ? "chat-switch-in" : ""} ${switchOut ? "chat-switch-out" : ""}`}>
{content}
</div>
<MessagePanelRenderer
panel={chat.activePanel}
isChatSwitching={chat.isChatSwitching}
/>
);
}