Restructure CSS

This commit is contained in:
2025-10-09 22:32:00 +03:00
Unverified
parent 2312569455
commit 0ccade3a91
67 changed files with 110 additions and 509 deletions
@@ -1,5 +1,5 @@
import { PRODUCT_NAME } from "../../core/config";
import { isElectron } from "../../electron/electron";
import { PRODUCT_NAME } from "../../../../core/config";
import { isElectron } from "../../../../core/electron/electron";
export function ElectronTitleBar() {
return isElectron && (
@@ -1,4 +1,4 @@
import { PRODUCT_NAME } from "../../../core/config";
import { PRODUCT_NAME } from "../../../../../core/config";
import useProfile from "../../hooks/useProfile";
import defaultAvatar from "../../../resources/images/default-avatar.png";
import { useState } from "react";
@@ -1,7 +1,7 @@
import { useState, useEffect, useRef } from "react";
import { MaterialDialog } from "../core/Dialog";
import { RichTextArea } from "../core/RichTextArea";
import type { Message } from "../../../core/types";
import type { Message } from "../../../../../core/types";
import Quote from "../core/Quote";
import AnimatedHeight from "../core/animations/AnimatedHeight";
import { useImmer } from "use-immer";
@@ -1,15 +1,15 @@
import { Message } from "./Message";
import { useAppState } from "../../state";
import type { Message as MessageType } from "../../../core/types";
import type { UserProfile } from "../../../core/types";
import type { Message as MessageType } from "../../../../../core/types";
import type { UserProfile } from "../../../../../core/types";
import { UserProfileDialog } from "./UserProfileDialog";
import { MessageContextMenu, type ContextMenuState } from "./MessageContextMenu";
import { fetchUserProfile } from "../../../../../api/profileApi";
import { fetchUserProfile } from "../../../../../core/api/profileApi";
import { useEffect, useState, type ReactNode } from "react";
import { delay } from "../../../../../utils/utils";
import { MaterialDialog } from "../core/Dialog";
import { request } from "../../../core/websocket";
import type { AddReactionRequest, AddDmReactionRequest } from "../../../core/types";
import { request } from "../../../../../core/websocket";
import type { AddReactionRequest, AddDmReactionRequest } from "../../../../../core/types";
interface ChatMessagesProps {
messages?: MessageType[];
@@ -1,7 +1,7 @@
import { useEffect } from "react";
import { useDM, type DMUser } from "../../hooks/useDM";
import { useAppState } from "../../state";
import { fetchUserPublicKey } from "../../../../../api/dmApi";
import { fetchUserPublicKey } from "../../../../../core/api/dmApi";
import defaultAvatar from "../../../../../images/default-avatar.png";
export function DMUsersList() {
@@ -1,6 +1,6 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { EMOJI_CATEGORIES, getRecentEmojis, addRecentEmoji } from "./emojiData";
import type { Size2D } from "../../../core/types";
import type { Size2D } from "../../../../../core/types";
interface BaseEmojiMenuProps {
isOpen: boolean;
@@ -1,4 +1,4 @@
import { PRODUCT_NAME } from "../../../core/config";
import { PRODUCT_NAME } from "../../../../../core/config";
import { useAppState } from "../../state";
import defaultAvatar from "../../../../../images/default-avatar.png";
import { useState, type FormEvent } from "react";
@@ -1,14 +1,14 @@
import { formatTime, id } from "../../../../../utils/utils";
import type { Attachment, Message as MessageType } from "../../../core/types";
import type { Attachment, Message as MessageType } from "../../../../../core/types";
import defaultAvatar from "../../../../../images/default-avatar.png";
import Quote from "../core/Quote";
import { parse } from "marked";
import DOMPurify from "dompurify";
import { useEffect, useState, useRef } from "react";
import { getCurrentKeys } from "../../../../../api/authApi";
import { getCurrentKeys } from "../../../../../core/api/authApi";
import { ecdhSharedSecret, deriveWrappingKey } from "../../../../../utils/crypto/asymmetric";
import { importAesGcmKey, aesGcmDecrypt } from "../../../../../utils/crypto/symmetric";
import { getAuthHeaders } from "../../../../../api/authApi";
import { getAuthHeaders } from "../../../../../core/api/authApi";
import { useAppState } from "../../state";
import { ub64 } from "../../../../../utils/utils";
import { useImmer } from "use-immer";
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef } from "react";
import type { Message, Size2D } from "../../../core/types";
import type { Message, Size2D } from "../../../../../core/types";
import { EmojiMenu } from "./EmojiMenu";
interface MessageContextMenuProps {
@@ -3,8 +3,8 @@ import { useAppState } from "../../state";
import { MessagePanel, type MessagePanelState } from "../../panels/MessagePanel";
import { ChatMessages } from "./ChatMessages";
import { ChatInputWrapper } from "./ChatInputWrapper";
import { setGlobalMessageHandler } from "../../../core/websocket";
import type { Message, WebSocketMessage } from "../../../core/types";
import { setGlobalMessageHandler } from "../../../../../core/websocket";
import type { Message, WebSocketMessage } from "../../../../../core/types";
import defaultAvatar from "../../../../../images/default-avatar.png";
import AnimatedOpacity from "../core/animations/AnimatedOpacity";
import type { DMPanel } from "../../panels/DMPanel";
@@ -1,6 +1,6 @@
import { useAppState } from "../../state";
import { useState, useEffect } from "react";
import type { Reaction } from "../../../core/types";
import type { Reaction } from "../../../../../core/types";
interface MessageReactionsProps {
reactions?: Reaction[];
@@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import type { Message } from "../../../core/types";
import type { Message } from "../../../../../core/types";
import { MaterialDialog } from "../core/Dialog";
import { MaterialTextField } from "../core/TextField";
@@ -1,5 +1,5 @@
import type { DialogProps } from "../../../core/types";
import type { UserProfile } from "../../../core/types";
import type { DialogProps } from "../../../../../core/types";
import type { UserProfile } from "../../../../../core/types";
import { MaterialDialog } from "../core/Dialog";
import { formatTime } from "../../../../../utils/utils";
import defaultAvatar from "../../../../../images/default-avatar.png";
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "react";
import type { Size2D, Rect } from "../../../core/types";
import type { Size2D, Rect } from "../../../../../core/types";
interface ImageCropperProps {
onCrop: (croppedImageData: string) => void;
@@ -1,7 +1,7 @@
import { useState, useEffect, useRef, type FormEvent } from "react";
import defaultAvatar from "../../../../../images/default-avatar.png";
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 useProfile from "../../hooks/useProfile";
import { ImageCropper } from "./ImageCropper";
@@ -1,12 +1,12 @@
import { useState, useEffect } from "react";
import { PRODUCT_NAME, API_BASE_URL } from "../../../core/config";
import type { DialogProps } from "../../../core/types";
import { PRODUCT_NAME, API_BASE_URL } from "../../../../../core/config";
import type { DialogProps } from "../../../../../core/types";
import { MaterialDialog } from "../core/Dialog";
import { initialize, isSupported, startElectronReceiver, stopElectronReceiver, subscribe, unsubscribe } from "../../../push-notifications";
import { isElectron } from "../../../electron/electron";
import { initialize, isSupported, startElectronReceiver, stopElectronReceiver, subscribe, unsubscribe } from "../../../../../core/push-notifications/push-notifications";
import { isElectron } from "../../../../../core/electron/electron";
import { useAppState } from "../../state";
import type { Switch } from "mdui/components/switch";
import { getAuthHeaders } from "../../../../../api/authApi";
import { getAuthHeaders } from "../../../../../core/api/authApi";
export function SettingsDialog({ isOpen, onOpenChange }: DialogProps) {
const [activePanel, setActivePanel] = useState("notifications-settings");
+3 -3
View File
@@ -6,9 +6,9 @@ import {
fetchDMHistory,
decryptDm,
sendDMViaWebSocket
} from "../../../../api/dmApi";
import type { User, Message, DmEncryptedJSON } from "../../core/types";
import { websocket } from "../../core/websocket";
} from "../../../../core/api/dmApi";
import type { User, Message, DmEncryptedJSON } from "../../../../core/types";
import { websocket } from "../../../../core/websocket";
export interface DMUser extends User {
lastMessage?: string;
@@ -1,6 +1,6 @@
import { useState, useCallback, useEffect } from "react";
import { useAppState } from "../state";
import { loadProfile, updateProfile, uploadProfilePicture, type ProfileData } from "../../../../api/profileApi";
import { loadProfile, updateProfile, uploadProfilePicture, type ProfileData } from "../../../../core/api/profileApi";
import { showSuccess, showError } from "../../../../utils/notification";
export default function useProfile() {
+2 -2
View File
@@ -6,8 +6,8 @@ import {
sendDmWithFiles,
editDmEnvelope,
deleteDmEnvelope
} from "../../../../api/dmApi";
import type { DmEncryptedJSON, DmEnvelope, DMWebSocketMessage, EncryptedMessageJson, Message } from "../../core/types";
} from "../../../../core/api/dmApi";
import type { DmEncryptedJSON, DmEnvelope, DMWebSocketMessage, EncryptedMessageJson, Message } from "../../../../core/types";
import type { UserState } from "../state";
export interface DMPanelData {
@@ -1,4 +1,4 @@
import type { Message, WebSocketMessage } from "../../core/types";
import type { Message, WebSocketMessage } from "../../../../core/types";
import type { UserState } from "../state";
export interface MessagePanelState {
@@ -1,8 +1,8 @@
import { MessagePanel } from "./MessagePanel";
import { API_BASE_URL } from "../../core/config";
import { getAuthHeaders } from "../../../../api/authApi";
import { request } from "../../core/websocket";
import type { ChatWebSocketMessage, Message, SendMessageRequest, ReactionUpdateWebSocketMessage } from "../../core/types";
import { API_BASE_URL } from "../../../../core/config";
import { getAuthHeaders } from "../../../../core/api/authApi";
import { request } from "../../../../core/websocket";
import type { ChatWebSocketMessage, Message, SendMessageRequest, ReactionUpdateWebSocketMessage } from "../../../../core/types";
import type { UserState } from "../state";
export class PublicChatPanel extends MessagePanel {
@@ -1,13 +0,0 @@
import { LeftPanel } from "../components/chat/LeftPanel";
import { RightPanel } from "../components/chat/RightPanel";
export default function ChatScreen() {
return (
<div id="chat-interface">
<div className="all-container">
<LeftPanel />
<RightPanel />
</div>
</div>
);
}
@@ -1,25 +0,0 @@
export default function DownloadAppScreen() {
return (
<div className="download-app-screen">
<div className="inner">
<h1>Чтобы пользоваться мессенджером, скачайте приложение</h1>
<p>
Этот сайт <b>не предназначен</b> для работы на маленьких экранах, поэтому
вам нужно скачать приложение мессенджера.
</p>
<a href="https://github.com/denis0001-dev/FromChat-android/releases/latest">
<mdui-button>Скачать на GitHub</mdui-button>
</a>
<p>
Если возникнут сложности или есть вопросы, нажмите кнопку!
</p>
<a href="https://t.me/denis0001-dev">
<mdui-button>Написать в поддержку</mdui-button>
</a>
</div>
</div>
)
}
@@ -1,142 +0,0 @@
import { useImmer } from "use-immer";
import { AlertsContainer, type Alert, type AlertType } from "../components/Alerts";
import { AuthContainer, AuthHeader } from "../components/Auth";
import type { ErrorResponse, LoginRequest, LoginResponse } from "../../core/types";
import { ensureKeysOnLogin } from "../../../../api/authApi";
import { API_BASE_URL } from "../../core/config";
import { useRef } from "react";
import type { TextField } from "mdui/components/text-field";
import { useAppState } from "../state";
import { useNavigate } from "react-router-dom";
import { MaterialTextField } from "../components/core/TextField";
import { initialize, isSupported, startElectronReceiver, subscribe } from "../../push-notifications";
import { isElectron } from "../../electron/electron";
export default function LoginScreen() {
const [alerts, updateAlerts] = useImmer<Alert[]>([]);
const setUser = useAppState(state => state.setUser);
const navigate = useNavigate();
function showAlert(type: AlertType, message: string) {
updateAlerts((alerts) => { alerts.push({type: type, message: message}) });
}
const usernameElement = useRef<TextField>(null);
const passwordElement = useRef<TextField>(null);
return (
<AuthContainer>
<AuthHeader icon="login" title="Добро пожаловать!" subtitle="Войдите в свой аккаунт" />
<div className="auth-body">
<AlertsContainer alerts={alerts} />
<form
onSubmit={async (e) => {
e.preventDefault();
const username = usernameElement.current!.value.trim();
const password = passwordElement.current!.value.trim();
if (!username || !password) {
showAlert("danger", "Пожалуйста, заполните все поля");
return;
}
try {
const request: LoginRequest = {
username: username,
password: password
}
const response = await fetch(`${API_BASE_URL}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(request)
});
if (response.ok) {
const data: LoginResponse = await response.json();
// Store the JWT token first
setUser(data.token, data.user);
// Setup keys with the token we just received
try {
await ensureKeysOnLogin(password, data.token);
} catch (e) {
console.error("Key setup failed:", e);
}
navigate("/chat");
// Initialize notifications
try {
if (isSupported()) {
const initialized = await initialize();
if (initialized) {
await subscribe(data.token);
// For Electron, start the notification receiver
if (isElectron) {
await startElectronReceiver();
}
console.log("Notifications enabled");
} else {
console.log("Notification permission denied");
}
} else {
console.log("Notifications not supported");
}
} catch (e) {
console.error("Notification setup failed:", e);
}
} else {
const data: ErrorResponse = await response.json();
showAlert("danger", data.message || "Неверное имя пользователя или пароль");
}
} catch (error) {
showAlert("danger", "Ошибка соединения с сервером");
}
}}>
<MaterialTextField
label="Имя пользователя"
id="login-username"
name="username"
variant="outlined"
icon="person--filled"
autocomplete="username"
required
ref={usernameElement} />
<MaterialTextField
label="Пароль"
id="login-password"
name="password"
variant="outlined"
type="password"
toggle-password
icon="password--filled"
autocomplete="current-password"
required
ref={passwordElement} />
<mdui-button type="submit">Войти</mdui-button>
</form>
<div className="text-center">
<p>
Ещё нет аккаунта?
<a
href="#"
className="link"
onClick={() => navigate("/register")}>
Зарегистрируйтесь
</a>
</p>
</div>
</div>
</AuthContainer>
)
}
@@ -1,148 +0,0 @@
import { useImmer } from "use-immer";
// import { showLogin } from "../../navigation";
import { AuthContainer, AuthHeader } from "../components/Auth";
import { AlertsContainer, type Alert, type AlertType } from "../components/Alerts";
import { useRef } from "react";
import { TextField } from "mdui/components/text-field";
import type { ErrorResponse, RegisterRequest, LoginResponse } from "../../core/types";
import { API_BASE_URL } from "../../core/config";
import { useAppState } from "../state";
import { useNavigate } from "react-router-dom";
import { MaterialTextField } from "../components/core/TextField";
import { ensureKeysOnLogin } from "../../../../api/authApi";
export default function RegisterScreen() {
const [alerts, updateAlerts] = useImmer<Alert[]>([]);
const setUser = useAppState(state => state.setUser);
const navigate = useNavigate();
function showAlert(type: AlertType, message: string) {
updateAlerts((alerts) => { alerts.push({type: type, message: message}) });
}
const usernameElement = useRef<TextField>(null);
const passwordElement = useRef<TextField>(null);
const confirmPasswordElement = useRef<TextField>(null);
return (
<AuthContainer>
<AuthHeader icon="person_add" title="Регистрация" subtitle="Создайте новый аккаунт" />
<div className="auth-body">
<AlertsContainer alerts={alerts} />
<form onSubmit={async (e) => {
e.preventDefault();
const username = usernameElement.current!.value.trim();
const password = passwordElement.current!.value.trim();
const confirmPassword = confirmPasswordElement.current!.value.trim();
if (!username || !password || !confirmPassword) {
showAlert("danger", "Пожалуйста, заполните все поля");
return;
}
if (password !== confirmPassword) {
showAlert("danger", "Пароли не совпадают");
return;
}
if (username.length < 3 || username.length > 20) {
showAlert("danger", "Имя пользователя должно быть от 3 до 20 символов");
return;
}
if (password.length < 5 || password.length > 50) {
showAlert("danger", "Пароль должен быть от 5 до 50 символов");
return;
}
try {
const request: RegisterRequest = {
username: username,
password: password,
confirm_password: confirmPassword
}
const response = await fetch(`${API_BASE_URL}/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(request)
});
if (response.ok) {
const data: LoginResponse = await response.json();
// Store the JWT token first
setUser(data.token, data.user);
// Setup keys with the token we just received
try {
await ensureKeysOnLogin(password, data.token);
} catch (e) {
console.error("Key setup failed:", e);
}
navigate("/chat");
} else {
const data: ErrorResponse = await response.json();
showAlert("danger", data.message || "Ошибка при регистрации");
}
} catch (error) {
showAlert("danger", "Ошибка соединения с сервером");
}
}}>
<MaterialTextField
label="Имя пользователя"
id="register-username"
name="username"
variant="outlined"
icon="person--filled"
autocomplete="username"
maxlength={20}
counter
required
ref={usernameElement} />
<MaterialTextField
label="Пароль"
id="register-password"
name="password"
variant="outlined"
type="password"
toggle-password
icon="password--filled"
autocomplete="new-password"
required
ref={passwordElement} />
<MaterialTextField
label="Подтвердите пароль"
id="register-confirm-password"
name="confirm_password"
variant="outlined"
type="password"
toggle-password
icon="password--filled"
autocomplete="new-password"
required
ref={confirmPasswordElement} />
<mdui-button type="submit">Зарегистрироваться</mdui-button>
</form>
<div className="text-center">
<p>
Уже есть аккаунт?
<a
href="#"
id="login-link"
className="link"
onClick={() => navigate("/login")}>
Войдите
</a>
</p>
</div>
</div>
</AuthContainer>
)
}
+7 -7
View File
@@ -1,14 +1,14 @@
import { create } from "zustand";
import type { Message, User } from "../core/types";
import { request } from "../core/websocket";
import type { Message, User } from "../../../core/types";
import { request } from "../../../core/websocket";
import { MessagePanel } from "./panels/MessagePanel";
import { PublicChatPanel } from "./panels/PublicChatPanel";
import { DMPanel, type DMPanelData } from "./panels/DMPanel";
import { getAuthHeaders } from "../../../api/authApi";
import { restoreKeys } from "../../../api/authApi";
import { API_BASE_URL } from "../core/config";
import { initialize, subscribe, startElectronReceiver, isSupported } from "../push-notifications";
import { isElectron } from "../electron/electron";
import { getAuthHeaders } from "../../../core/api/authApi";
import { restoreKeys } from "../../../core/api/authApi";
import { API_BASE_URL } from "../../../core/config";
import { initialize, subscribe, startElectronReceiver, isSupported } from "../../../core/push-notifications/push-notifications";
import { isElectron } from "../../../core/electron/electron";
export type ChatTabs = "chats" | "channels" | "contacts" | "dms"