mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Implement logic
This commit is contained in:
@@ -2,7 +2,8 @@ import { ElectronTitleBar } from "./components/Electron";
|
|||||||
import ChatScreen from "./screen/ChatScreen";
|
import ChatScreen from "./screen/ChatScreen";
|
||||||
import LoginScreen from "./screen/LoginScreen";
|
import LoginScreen from "./screen/LoginScreen";
|
||||||
import RegisterScreen from "./screen/RegisterScreen";
|
import RegisterScreen from "./screen/RegisterScreen";
|
||||||
import { useAppState } from "./state"
|
import { useAppState } from "./state";
|
||||||
|
import { DialogProvider } from "./contexts/DialogContext";
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { currentPage } = useAppState();
|
const { currentPage } = useAppState();
|
||||||
@@ -25,11 +26,11 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DialogProvider>
|
||||||
<ElectronTitleBar />
|
<ElectronTitleBar />
|
||||||
<div id="main-wrapper">
|
<div id="main-wrapper">
|
||||||
{page}
|
{page}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</DialogProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,15 @@
|
|||||||
|
import { useDialog } from "../../contexts/DialogContext";
|
||||||
|
|
||||||
export function BottomAppBar() {
|
export function BottomAppBar() {
|
||||||
|
const { openSettings } = useDialog();
|
||||||
|
|
||||||
|
const handleSettingsClick = () => {
|
||||||
|
openSettings();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mdui-bottom-app-bar>
|
<mdui-bottom-app-bar>
|
||||||
<mdui-button-icon icon="settings--filled" id="settings-open"></mdui-button-icon>
|
<mdui-button-icon icon="settings--filled" id="settings-open" onClick={handleSettingsClick}></mdui-button-icon>
|
||||||
<mdui-button-icon icon="group_add--filled"></mdui-button-icon>
|
<mdui-button-icon icon="group_add--filled"></mdui-button-icon>
|
||||||
<div style={{ flexGrow: 1 }}></div>
|
<div style={{ flexGrow: 1 }}></div>
|
||||||
<mdui-fab icon="edit--filled"></mdui-fab>
|
<mdui-fab icon="edit--filled"></mdui-fab>
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import { PRODUCT_NAME } from "../../../core/config";
|
import { PRODUCT_NAME } from "../../../core/config";
|
||||||
|
import { useDialog } from "../../contexts/DialogContext";
|
||||||
|
|
||||||
export function ChatHeader() {
|
export function ChatHeader() {
|
||||||
|
const { openProfile } = useDialog();
|
||||||
|
|
||||||
|
const handleProfileClick = () => {
|
||||||
|
openProfile();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="chat-header-left">
|
<header className="chat-header-left">
|
||||||
<div className="product-name">{PRODUCT_NAME}</div>
|
<div className="product-name">{PRODUCT_NAME}</div>
|
||||||
<div className="profile">
|
<div className="profile">
|
||||||
<a href="#" id="profile-open">
|
<a href="#" id="profile-open" onClick={handleProfileClick}>
|
||||||
<img src="./src/resources/images/default-avatar.png" alt="" id="preview1" />
|
<img src="./src/resources/images/default-avatar.png" alt="" id="preview1" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,31 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useChat } from "../../hooks/useChat";
|
||||||
|
|
||||||
export function ChatInputWrapper() {
|
export function ChatInputWrapper() {
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
const { sendMessage } = useChat();
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (message.trim()) {
|
||||||
|
await sendMessage(message);
|
||||||
|
setMessage("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-input-wrapper">
|
<div className="chat-input-wrapper">
|
||||||
<div className="chat-input">
|
<div className="chat-input">
|
||||||
<form className="input-group" id="message-form">
|
<form className="input-group" id="message-form" onSubmit={handleSubmit}>
|
||||||
<input type="text" className="message-input" id="message-input" placeholder="Напишите сообщение..." autoComplete="off" />
|
<input
|
||||||
|
type="text"
|
||||||
|
className="message-input"
|
||||||
|
id="message-input"
|
||||||
|
placeholder="Напишите сообщение..."
|
||||||
|
autoComplete="off"
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
/>
|
||||||
<button type="submit" className="send-btn">
|
<button type="submit" className="send-btn">
|
||||||
<span className="material-symbols filled">send</span>
|
<span className="material-symbols filled">send</span>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,16 +1,27 @@
|
|||||||
|
import { useChat } from "../../hooks/useChat";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
export function ChatMainHeader() {
|
export function ChatMainHeader() {
|
||||||
|
const { currentChat } = useChat();
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
|
|
||||||
|
const handleCollapse = () => {
|
||||||
|
setIsCollapsed(!isCollapsed);
|
||||||
|
// TODO: Implement chat collapse animation
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-header">
|
<div className="chat-header">
|
||||||
<img src="./src/resources/images/default-avatar.png" alt="Avatar" className="chat-header-avatar" />
|
<img src="./src/resources/images/default-avatar.png" alt="Avatar" className="chat-header-avatar" />
|
||||||
<div className="chat-header-info">
|
<div className="chat-header-info">
|
||||||
<div className="info-chat">
|
<div className="info-chat">
|
||||||
<h4 id="chat-name">Общий чат</h4>
|
<h4 id="chat-name">{currentChat}</h4>
|
||||||
<p>
|
<p>
|
||||||
<span className="online-status"></span>
|
<span className="online-status"></span>
|
||||||
Онлайн
|
Онлайн
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a href="#" id="hide-chat">Свернуть чат</a>
|
<a href="#" id="hide-chat" onClick={handleCollapse}>Свернуть чат</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,34 @@
|
|||||||
|
import { useChat } from "../../hooks/useChat";
|
||||||
|
import { Message } from "./Message";
|
||||||
|
import { useAppState } from "../../state";
|
||||||
|
import type { Message as MessageType } from "../../../core/types";
|
||||||
|
|
||||||
export function ChatMessages() {
|
export function ChatMessages() {
|
||||||
|
const { messages } = useChat();
|
||||||
|
const { user } = useAppState();
|
||||||
|
|
||||||
|
const handleProfileClick = (username: string) => {
|
||||||
|
// TODO: Show user profile dialog
|
||||||
|
console.log("Show profile for:", username);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleContextMenu = (e: React.MouseEvent, message: MessageType) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// TODO: Show context menu
|
||||||
|
console.log("Show context menu for message:", message.id);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-messages" id="chat-messages">
|
<div className="chat-messages" id="chat-messages">
|
||||||
|
{messages.map((message) => (
|
||||||
|
<Message
|
||||||
|
key={message.id}
|
||||||
|
message={message}
|
||||||
|
isAuthor={message.username === user.currentUser?.username}
|
||||||
|
onProfileClick={handleProfileClick}
|
||||||
|
onContextMenu={handleContextMenu}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,46 @@
|
|||||||
|
import { useChat } from "../../hooks/useChat";
|
||||||
|
|
||||||
export function ChatTabs() {
|
export function ChatTabs() {
|
||||||
|
const { activeTab, setActiveTab, setCurrentChat } = useChat();
|
||||||
|
|
||||||
|
const handleChatClick = (chatName: string) => {
|
||||||
|
setCurrentChat(chatName);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-tabs">
|
<div className="chat-tabs">
|
||||||
<mdui-tabs value="chats" full-width>
|
<mdui-tabs value={activeTab} full-width onChange={(e: any) => setActiveTab(e.detail.value)}>
|
||||||
<mdui-tab value="chats">Чаты</mdui-tab>
|
<mdui-tab value="chats">
|
||||||
<mdui-tab value="channels">Каналы</mdui-tab>
|
Чаты
|
||||||
<mdui-tab value="contacts">Контакты</mdui-tab>
|
</mdui-tab>
|
||||||
<mdui-tab value="dms">ЛС</mdui-tab>
|
<mdui-tab value="channels">
|
||||||
|
Каналы
|
||||||
|
</mdui-tab>
|
||||||
|
<mdui-tab value="contacts">
|
||||||
|
Контакты
|
||||||
|
</mdui-tab>
|
||||||
|
<mdui-tab value="dms">
|
||||||
|
ЛС
|
||||||
|
</mdui-tab>
|
||||||
|
|
||||||
<mdui-tab-panel slot="panel" value="chats">
|
<mdui-tab-panel slot="panel" value="chats">
|
||||||
<mdui-list>
|
<mdui-list>
|
||||||
<mdui-list-item headline="Общий чат" description="Вы: Последнее сообщение" id="chat-list-chat-1">
|
<mdui-list-item
|
||||||
|
headline="Общий чат"
|
||||||
|
description="Вы: Последнее сообщение"
|
||||||
|
id="chat-list-chat-1"
|
||||||
|
onClick={() => handleChatClick("Общий чат")}
|
||||||
|
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" />
|
||||||
</mdui-list-item>
|
</mdui-list-item>
|
||||||
<mdui-list-item headline="Общий чат 2" description="Вы: Последнее сообщение" id="chat-list-chat-2">
|
<mdui-list-item
|
||||||
|
headline="Общий чат 2"
|
||||||
|
description="Вы: Последнее сообщение"
|
||||||
|
id="chat-list-chat-2"
|
||||||
|
onClick={() => handleChatClick("Общий чат 2")}
|
||||||
|
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" />
|
||||||
</mdui-list-item>
|
</mdui-list-item>
|
||||||
</mdui-list>
|
</mdui-list>
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { formatTime } from "../../../utils/utils";
|
||||||
|
import type { Message as MessageType } from "../../../core/types";
|
||||||
|
import defaultAvatar from "../../../resources/images/default-avatar.png";
|
||||||
|
|
||||||
|
interface MessageProps {
|
||||||
|
message: MessageType;
|
||||||
|
isAuthor: boolean;
|
||||||
|
onProfileClick: (username: string) => void;
|
||||||
|
onContextMenu: (e: React.MouseEvent, message: MessageType) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Message({ message, isAuthor, onProfileClick, onContextMenu }: MessageProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`message ${isAuthor ? "sent" : "received"}`}
|
||||||
|
data-id={message.id}
|
||||||
|
onContextMenu={(e) => onContextMenu(e, message)}
|
||||||
|
>
|
||||||
|
<div className="message-inner">
|
||||||
|
{/* Add profile picture for received messages */}
|
||||||
|
{!isAuthor && (
|
||||||
|
<div className="message-profile-pic">
|
||||||
|
<img
|
||||||
|
src={message.profile_picture || defaultAvatar}
|
||||||
|
alt={message.username}
|
||||||
|
onClick={() => onProfileClick(message.username)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
onError={(e) => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
target.src = defaultAvatar;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isAuthor && (
|
||||||
|
<div
|
||||||
|
className="message-username"
|
||||||
|
onClick={() => onProfileClick(message.username)}
|
||||||
|
style={{ cursor: "pointer" }}> {/* TODO extract to SCSS */}
|
||||||
|
{message.username}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Add reply preview if this is a reply */}
|
||||||
|
{message.reply_to && (
|
||||||
|
<div className="message-reply">
|
||||||
|
<div className="reply-content">
|
||||||
|
<span className="reply-username">{message.reply_to.username}</span>
|
||||||
|
<span className="reply-text">{message.reply_to.content}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="message-content">
|
||||||
|
{message.content}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="message-time">
|
||||||
|
{formatTime(message.timestamp)}
|
||||||
|
{message.is_edited ? " (edited)" : undefined}
|
||||||
|
|
||||||
|
{isAuthor && message.is_read ? (
|
||||||
|
<span className="material-symbols outlined"></span>
|
||||||
|
) : undefined}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,22 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useAppState } from "../../state";
|
||||||
|
import { useDialog } from "../../contexts/DialogContext";
|
||||||
|
|
||||||
export function ProfileDialog() {
|
export function ProfileDialog() {
|
||||||
|
const [username, setUsername] = useState("user123");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const { user } = useAppState();
|
||||||
|
const { isProfileOpen, closeProfile } = useDialog();
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// TODO: Implement profile update logic
|
||||||
|
console.log("Profile update:", { username, description });
|
||||||
|
closeProfile();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mdui-dialog id="profile-dialog" close-on-overlay-click close-on-esc>
|
<mdui-dialog id="profile-dialog" close-on-overlay-click close-on-esc open={isProfileOpen}>
|
||||||
<div className="content">
|
<div className="content">
|
||||||
<div className="header-top">
|
<div className="header-top">
|
||||||
<div className="profile-picture-container">
|
<div className="profile-picture-container">
|
||||||
@@ -8,21 +24,29 @@ export function ProfileDialog() {
|
|||||||
<mdui-button-icon icon="camera_alt--filled" id="upload-pfp-btn" className="upload-overlay" variant="filled"></mdui-button-icon>
|
<mdui-button-icon icon="camera_alt--filled" id="upload-pfp-btn" className="upload-overlay" variant="filled"></mdui-button-icon>
|
||||||
<input type="file" id="pfp-file-input" accept="image/*" style={{ display: "none" }} />
|
<input type="file" id="pfp-file-input" accept="image/*" style={{ display: "none" }} />
|
||||||
</div>
|
</div>
|
||||||
<mdui-text-field id="username-field" label="Имя пользователя" variant="outlined" value="user123" autocomplete="username"></mdui-text-field>
|
<mdui-text-field
|
||||||
|
id="username-field"
|
||||||
|
label="Имя пользователя"
|
||||||
|
variant="outlined"
|
||||||
|
value={username}
|
||||||
|
onChange={(e: any) => setUsername(e.target.value)}
|
||||||
|
autocomplete="username">
|
||||||
|
</mdui-text-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form id="profile-form">
|
<form id="profile-form" onSubmit={handleSubmit}>
|
||||||
<mdui-text-field
|
<mdui-text-field
|
||||||
id="description-field"
|
id="description-field"
|
||||||
label="О себе"
|
label="О себе"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
// multiline={true}
|
value={description}
|
||||||
rows={3}
|
onChange={(e: any) => setDescription(e.target.value)}
|
||||||
placeholder="Расскажите о себе..."
|
placeholder="Расскажите о себе..."
|
||||||
autocomplete="none"></mdui-text-field>
|
autocomplete="none">
|
||||||
|
</mdui-text-field>
|
||||||
<div className="dialog-actions">
|
<div className="dialog-actions">
|
||||||
<mdui-button type="submit" id="profile-submit">Сохранить изменения</mdui-button>
|
<mdui-button type="submit" id="profile-submit">Сохранить изменения</mdui-button>
|
||||||
<mdui-button id="profile-dialog-close" variant="outlined">Закрыть</mdui-button>
|
<mdui-button id="profile-dialog-close" variant="outlined" onClick={closeProfile}>Закрыть</mdui-button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +1,91 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { useDialog } from "../../contexts/DialogContext";
|
||||||
|
import { PRODUCT_NAME } from "../../../core/config";
|
||||||
|
|
||||||
export function SettingsDialog() {
|
export function SettingsDialog() {
|
||||||
|
const { isSettingsOpen, closeSettings } = useDialog();
|
||||||
|
const [activePanel, setActivePanel] = useState("notifications-settings");
|
||||||
|
|
||||||
|
const handlePanelChange = (panelId: string) => {
|
||||||
|
setActivePanel(panelId);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mdui-dialog id="settings-dialog" close-on-overlay-click close-on-esc fullscreen>
|
<mdui-dialog id="settings-dialog" close-on-overlay-click close-on-esc fullscreen open={isSettingsOpen}>
|
||||||
<div className="fullscreen-wrapper">
|
<div className="fullscreen-wrapper">
|
||||||
<div id="settings-dialog-inner">
|
<div id="settings-dialog-inner">
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<mdui-button-icon icon="close" id="settings-close"></mdui-button-icon>
|
<mdui-button-icon icon="close" id="settings-close" onClick={closeSettings}></mdui-button-icon>
|
||||||
<mdui-top-app-bar-title>Настройки</mdui-top-app-bar-title>
|
<mdui-top-app-bar-title>Настройки</mdui-top-app-bar-title>
|
||||||
</div>
|
</div>
|
||||||
<div id="settings-menu">
|
<div id="settings-menu">
|
||||||
<mdui-list>
|
<mdui-list>
|
||||||
<mdui-list-item icon="notifications--filled" rounded active>Уведомления</mdui-list-item>
|
<mdui-list-item
|
||||||
<mdui-list-item icon="palette--filled" rounded>Внешний вид</mdui-list-item>
|
icon="notifications--filled"
|
||||||
<mdui-list-item icon="security--filled" rounded>Безопасность</mdui-list-item>
|
rounded
|
||||||
<mdui-list-item icon="language--filled" rounded>Язык</mdui-list-item>
|
active={activePanel === "notifications-settings"}
|
||||||
<mdui-list-item icon="storage--filled" rounded>Хранилище</mdui-list-item>
|
onClick={() => handlePanelChange("notifications-settings")}
|
||||||
<mdui-list-item icon="help--filled" rounded>Помощь</mdui-list-item>
|
style={{ cursor: "pointer" }}
|
||||||
<mdui-list-item icon="info--filled" rounded>О приложении</mdui-list-item>
|
>
|
||||||
|
Уведомления
|
||||||
|
</mdui-list-item>
|
||||||
|
<mdui-list-item
|
||||||
|
icon="palette--filled"
|
||||||
|
rounded
|
||||||
|
active={activePanel === "appearance-settings"}
|
||||||
|
onClick={() => handlePanelChange("appearance-settings")}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
Внешний вид
|
||||||
|
</mdui-list-item>
|
||||||
|
<mdui-list-item
|
||||||
|
icon="security--filled"
|
||||||
|
rounded
|
||||||
|
active={activePanel === "security-settings"}
|
||||||
|
onClick={() => handlePanelChange("security-settings")}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
Безопасность
|
||||||
|
</mdui-list-item>
|
||||||
|
<mdui-list-item
|
||||||
|
icon="language--filled"
|
||||||
|
rounded
|
||||||
|
active={activePanel === "language-settings"}
|
||||||
|
onClick={() => handlePanelChange("language-settings")}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
Язык
|
||||||
|
</mdui-list-item>
|
||||||
|
<mdui-list-item
|
||||||
|
icon="storage--filled"
|
||||||
|
rounded
|
||||||
|
active={activePanel === "storage-settings"}
|
||||||
|
onClick={() => handlePanelChange("storage-settings")}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
Хранилище
|
||||||
|
</mdui-list-item>
|
||||||
|
<mdui-list-item
|
||||||
|
icon="help--filled"
|
||||||
|
rounded
|
||||||
|
active={activePanel === "help-settings"}
|
||||||
|
onClick={() => handlePanelChange("help-settings")}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
Помощь
|
||||||
|
</mdui-list-item>
|
||||||
|
<mdui-list-item
|
||||||
|
icon="info--filled"
|
||||||
|
rounded
|
||||||
|
active={activePanel === "about-settings"}
|
||||||
|
onClick={() => handlePanelChange("about-settings")}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
О приложении
|
||||||
|
</mdui-list-item>
|
||||||
</mdui-list>
|
</mdui-list>
|
||||||
<div className="screen">
|
<div className="screen">
|
||||||
<div id="notifications-settings" className="settings-panel active">
|
<div id="notifications-settings" className={`settings-panel ${activePanel === "notifications-settings" ? "active" : ""}`}>
|
||||||
<h3>Уведомления</h3>
|
<h3>Уведомления</h3>
|
||||||
<mdui-switch checked>Новые сообщения</mdui-switch>
|
<mdui-switch checked>Новые сообщения</mdui-switch>
|
||||||
<mdui-switch checked>Звуковые уведомления</mdui-switch>
|
<mdui-switch checked>Звуковые уведомления</mdui-switch>
|
||||||
@@ -26,7 +93,7 @@ export function SettingsDialog() {
|
|||||||
<mdui-switch checked>Email уведомления</mdui-switch>
|
<mdui-switch checked>Email уведомления</mdui-switch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="appearance-settings" className="settings-panel">
|
<div id="appearance-settings" className={`settings-panel ${activePanel === "appearance-settings" ? "active" : ""}`}>
|
||||||
<h3>Внешний вид</h3>
|
<h3>Внешний вид</h3>
|
||||||
<mdui-select label="Тема" variant="outlined">
|
<mdui-select label="Тема" variant="outlined">
|
||||||
<mdui-menu-item value="dark">Тёмная</mdui-menu-item>
|
<mdui-menu-item value="dark">Тёмная</mdui-menu-item>
|
||||||
@@ -40,14 +107,14 @@ export function SettingsDialog() {
|
|||||||
</mdui-select>
|
</mdui-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="security-settings" className="settings-panel">
|
<div id="security-settings" className={`settings-panel ${activePanel === "security-settings" ? "active" : ""}`}>
|
||||||
<h3>Безопасность</h3>
|
<h3>Безопасность</h3>
|
||||||
<mdui-button variant="outlined">Изменить пароль</mdui-button>
|
<mdui-button variant="outlined">Изменить пароль</mdui-button>
|
||||||
<mdui-button variant="outlined">Двухфакторная аутентификация</mdui-button>
|
<mdui-button variant="outlined">Двухфакторная аутентификация</mdui-button>
|
||||||
<mdui-switch>Автоматический выход</mdui-switch>
|
<mdui-switch>Автоматический выход</mdui-switch>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="language-settings" className="settings-panel">
|
<div id="language-settings" className={`settings-panel ${activePanel === "language-settings" ? "active" : ""}`}>
|
||||||
<h3>Язык</h3>
|
<h3>Язык</h3>
|
||||||
<mdui-select label="Выберите язык" variant="outlined">
|
<mdui-select label="Выберите язык" variant="outlined">
|
||||||
<mdui-menu-item value="ru">Русский</mdui-menu-item>
|
<mdui-menu-item value="ru">Русский</mdui-menu-item>
|
||||||
@@ -56,24 +123,24 @@ export function SettingsDialog() {
|
|||||||
</mdui-select>
|
</mdui-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="storage-settings" className="settings-panel">
|
<div id="storage-settings" className={`settings-panel ${activePanel === "storage-settings" ? "active" : ""}`}>
|
||||||
<h3>Хранилище</h3>
|
<h3>Хранилище</h3>
|
||||||
<p>Использовано: 2.5 ГБ из 10 ГБ</p>
|
<p>Использовано: 2.5 ГБ из 10 ГБ</p>
|
||||||
<mdui-linear-progress value={25}></mdui-linear-progress>
|
<mdui-linear-progress value={25}></mdui-linear-progress>
|
||||||
<mdui-button variant="outlined">Очистить кэш</mdui-button>
|
<mdui-button variant="outlined">Очистить кэш</mdui-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="help-settings" className="settings-panel">
|
<div id="help-settings" className={`settings-panel ${activePanel === "help-settings" ? "active" : ""}`}>
|
||||||
<h3>Помощь</h3>
|
<h3>Помощь</h3>
|
||||||
<mdui-button variant="outlined">Руководство пользователя</mdui-button>
|
<mdui-button variant="outlined">Руководство пользователя</mdui-button>
|
||||||
<mdui-button variant="outlined">Связаться с поддержкой</mdui-button>
|
<mdui-button variant="outlined">Связаться с поддержкой</mdui-button>
|
||||||
<mdui-button variant="outlined">FAQ</mdui-button>
|
<mdui-button variant="outlined">FAQ</mdui-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="about-settings" className="settings-panel">
|
<div id="about-settings" className={`settings-panel ${activePanel === "about-settings" ? "active" : ""}`}>
|
||||||
<h3>О приложении</h3>
|
<h3>О приложении</h3>
|
||||||
<p>Версия: 1.0.0</p>
|
<p>Версия: 1.0.0</p>
|
||||||
<p>© 2025 <span className="product-name">Loading...</span>. Все права защищены.</p>
|
<p>© 2025 <span className="product-name">{PRODUCT_NAME}</span>. Все права защищены.</p>
|
||||||
<mdui-button variant="outlined">Политика конфиденциальности</mdui-button>
|
<mdui-button variant="outlined">Политика конфиденциальности</mdui-button>
|
||||||
<mdui-button variant="outlined">Условия использования</mdui-button>
|
<mdui-button variant="outlined">Условия использования</mdui-button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { createContext, useContext, useState } from "react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
interface DialogContextType {
|
||||||
|
isProfileOpen: boolean;
|
||||||
|
isSettingsOpen: boolean;
|
||||||
|
openProfile: () => void;
|
||||||
|
closeProfile: () => void;
|
||||||
|
openSettings: () => void;
|
||||||
|
closeSettings: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DialogContext = createContext<DialogContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export function DialogProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [isProfileOpen, setIsProfileOpen] = useState(false);
|
||||||
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
|
|
||||||
|
const openProfile = () => setIsProfileOpen(true);
|
||||||
|
const closeProfile = () => setIsProfileOpen(false);
|
||||||
|
const openSettings = () => setIsSettingsOpen(true);
|
||||||
|
const closeSettings = () => setIsSettingsOpen(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogContext.Provider value={{
|
||||||
|
isProfileOpen,
|
||||||
|
isSettingsOpen,
|
||||||
|
openProfile,
|
||||||
|
closeProfile,
|
||||||
|
openSettings,
|
||||||
|
closeSettings
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</DialogContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDialog() {
|
||||||
|
const context = useContext(DialogContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useDialog must be used within a DialogProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { useEffect, useCallback } from "react";
|
||||||
|
import { useAppState } from "../state";
|
||||||
|
import { request, websocket } from "../../websocket";
|
||||||
|
import { API_BASE_URL } from "../../core/config";
|
||||||
|
import type { Message, WebSocketMessage, User } from "../../core/types";
|
||||||
|
import { getAuthHeaders } from "../../auth/api";
|
||||||
|
|
||||||
|
export function useChat() {
|
||||||
|
const {
|
||||||
|
chat,
|
||||||
|
addMessage,
|
||||||
|
updateMessage,
|
||||||
|
removeMessage,
|
||||||
|
clearMessages,
|
||||||
|
setCurrentChat,
|
||||||
|
setActiveTab,
|
||||||
|
setDmUsers,
|
||||||
|
setActiveDm,
|
||||||
|
user
|
||||||
|
} = useAppState();
|
||||||
|
|
||||||
|
// Load messages for the current chat
|
||||||
|
const loadMessages = useCallback(async () => {
|
||||||
|
if (!user.authToken) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/get_messages`, {
|
||||||
|
headers: getAuthHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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]);
|
||||||
|
|
||||||
|
// Handle WebSocket messages
|
||||||
|
useEffect(() => {
|
||||||
|
const handleWebSocketMessage = (event: MessageEvent) => {
|
||||||
|
try {
|
||||||
|
const response: WebSocketMessage = JSON.parse(event.data);
|
||||||
|
|
||||||
|
switch (response.type) {
|
||||||
|
case 'messageEdited':
|
||||||
|
if (response.data) {
|
||||||
|
updateMessage(response.data.id, response.data);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'messageDeleted':
|
||||||
|
if (response.data && response.data.message_id) {
|
||||||
|
removeMessage(response.data.message_id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'newMessage':
|
||||||
|
if (response.data) {
|
||||||
|
const isAuthor = response.data.username === user.currentUser?.username;
|
||||||
|
addMessage(response.data);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error parsing WebSocket message:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.addEventListener("message", handleWebSocketMessage);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
websocket.removeEventListener("message", handleWebSocketMessage);
|
||||||
|
};
|
||||||
|
}, [addMessage, user.currentUser]);
|
||||||
|
|
||||||
|
// Load messages when component mounts or chat changes
|
||||||
|
useEffect(() => {
|
||||||
|
loadMessages();
|
||||||
|
}, [loadMessages]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages: chat.messages,
|
||||||
|
currentChat: chat.currentChat,
|
||||||
|
activeTab: chat.activeTab,
|
||||||
|
dmUsers: chat.dmUsers,
|
||||||
|
activeDm: chat.activeDm,
|
||||||
|
sendMessage,
|
||||||
|
updateMessage,
|
||||||
|
removeMessage,
|
||||||
|
clearMessages,
|
||||||
|
setCurrentChat,
|
||||||
|
setActiveTab,
|
||||||
|
setDmUsers,
|
||||||
|
setActiveDm
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ import { useImmer } from "use-immer";
|
|||||||
import { AlertsContainer, type Alert, type AlertType } from "../components/Alerts";
|
import { AlertsContainer, type Alert, type AlertType } from "../components/Alerts";
|
||||||
import { AuthContainer, AuthHeader } from "../components/Auth";
|
import { AuthContainer, AuthHeader } from "../components/Auth";
|
||||||
import type { ErrorResponse, LoginRequest, LoginResponse } from "../../core/types";
|
import type { ErrorResponse, LoginRequest, LoginResponse } from "../../core/types";
|
||||||
import { setUser } from "../../auth/api";
|
|
||||||
import { ensureKeysOnLogin } from "../../auth/crypto";
|
import { ensureKeysOnLogin } from "../../auth/crypto";
|
||||||
import { API_BASE_URL } from "../../core/config";
|
import { API_BASE_URL } from "../../core/config";
|
||||||
// import { initializeProfile } from "../../userPanel/profile/profile";
|
// import { initializeProfile } from "../../userPanel/profile/profile";
|
||||||
@@ -13,6 +12,7 @@ import { useAppState } from "../state";
|
|||||||
export default function LoginScreen() {
|
export default function LoginScreen() {
|
||||||
const [alerts, updateAlerts] = useImmer<Alert[]>([]);
|
const [alerts, updateAlerts] = useImmer<Alert[]>([]);
|
||||||
const setCurrentPage = useAppState(state => state.setCurrentPage);
|
const setCurrentPage = useAppState(state => state.setCurrentPage);
|
||||||
|
const setUser = useAppState(state => state.setUser);
|
||||||
|
|
||||||
function showAlert(type: AlertType, message: string) {
|
function showAlert(type: AlertType, message: string) {
|
||||||
updateAlerts((alerts) => { alerts.push({type: type, message: message}) });
|
updateAlerts((alerts) => { alerts.push({type: type, message: message}) });
|
||||||
|
|||||||
+110
-2
@@ -1,13 +1,121 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
import type { Message, User, UserProfile } from "../core/types";
|
||||||
|
|
||||||
type Page = "login" | "register" | "chat"
|
type Page = "login" | "register" | "chat"
|
||||||
|
|
||||||
|
interface ChatState {
|
||||||
|
messages: Message[];
|
||||||
|
currentChat: string;
|
||||||
|
activeTab: "chats" | "channels" | "contacts" | "dms";
|
||||||
|
dmUsers: User[];
|
||||||
|
activeDm: { userId: number; username: string; publicKey: string | null } | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UserState {
|
||||||
|
currentUser: User | null;
|
||||||
|
authToken: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
currentPage: Page;
|
currentPage: Page;
|
||||||
setCurrentPage: (page: Page) => void;
|
setCurrentPage: (page: Page) => void;
|
||||||
|
|
||||||
|
// Chat state
|
||||||
|
chat: ChatState;
|
||||||
|
addMessage: (message: Message) => void;
|
||||||
|
updateMessage: (messageId: number, updatedMessage: Partial<Message>) => void;
|
||||||
|
removeMessage: (messageId: number) => void;
|
||||||
|
setCurrentChat: (chat: string) => void;
|
||||||
|
setActiveTab: (tab: ChatState["activeTab"]) => void;
|
||||||
|
setDmUsers: (users: User[]) => void;
|
||||||
|
setActiveDm: (dm: ChatState["activeDm"]) => void;
|
||||||
|
clearMessages: () => void;
|
||||||
|
|
||||||
|
// User state
|
||||||
|
user: UserState;
|
||||||
|
setUser: (token: string, user: User) => void;
|
||||||
|
logout: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAppState = create<AppState>((set) => ({
|
export const useAppState = create<AppState>((set, get) => ({
|
||||||
currentPage: "login", // default page
|
currentPage: "login", // default page
|
||||||
setCurrentPage: (page: Page) => set({ currentPage: page })
|
setCurrentPage: (page: Page) => set({ currentPage: page }),
|
||||||
|
|
||||||
|
// Chat state
|
||||||
|
chat: {
|
||||||
|
messages: [],
|
||||||
|
currentChat: "Общий чат",
|
||||||
|
activeTab: "chats",
|
||||||
|
dmUsers: [],
|
||||||
|
activeDm: null
|
||||||
|
},
|
||||||
|
addMessage: (message: Message) => set((state) => ({
|
||||||
|
chat: {
|
||||||
|
...state.chat,
|
||||||
|
messages: [...state.chat.messages, message]
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
updateMessage: (messageId: number, updatedMessage: Partial<Message>) => set((state) => ({
|
||||||
|
chat: {
|
||||||
|
...state.chat,
|
||||||
|
messages: state.chat.messages.map(msg =>
|
||||||
|
msg.id === messageId ? { ...msg, ...updatedMessage } : msg
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
removeMessage: (messageId: number) => set((state) => ({
|
||||||
|
chat: {
|
||||||
|
...state.chat,
|
||||||
|
messages: state.chat.messages.filter(msg => msg.id !== messageId)
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
clearMessages: () => set((state) => ({
|
||||||
|
chat: {
|
||||||
|
...state.chat,
|
||||||
|
messages: []
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
setCurrentChat: (chat: string) => set((state) => ({
|
||||||
|
chat: {
|
||||||
|
...state.chat,
|
||||||
|
currentChat: chat
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
setActiveTab: (tab: ChatState["activeTab"]) => set((state) => ({
|
||||||
|
chat: {
|
||||||
|
...state.chat,
|
||||||
|
activeTab: tab
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
setDmUsers: (users: User[]) => set((state) => ({
|
||||||
|
chat: {
|
||||||
|
...state.chat,
|
||||||
|
dmUsers: users
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
setActiveDm: (dm: ChatState["activeDm"]) => set((state) => ({
|
||||||
|
chat: {
|
||||||
|
...state.chat,
|
||||||
|
activeDm: dm
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
|
||||||
|
// User state
|
||||||
|
user: {
|
||||||
|
currentUser: null,
|
||||||
|
authToken: null
|
||||||
|
},
|
||||||
|
setUser: (token: string, user: User) => set((state) => ({
|
||||||
|
user: {
|
||||||
|
currentUser: user,
|
||||||
|
authToken: token
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
logout: () => set((state) => ({
|
||||||
|
user: {
|
||||||
|
currentUser: null,
|
||||||
|
authToken: null
|
||||||
|
},
|
||||||
|
currentPage: "login"
|
||||||
|
}))
|
||||||
}));
|
}));
|
||||||
Reference in New Issue
Block a user