From 737974dfa8f38ee242cf5cfced1348844cb8bea6 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Wed, 8 Oct 2025 18:15:23 +0300 Subject: [PATCH 01/10] Change the structure --- .gitignore | 2 +- frontend/src/App.tsx | 24 +- .../app/auth/crypto.ts => api/authApi.ts} | 29 +- frontend/src/{pages/app => }/api/dmApi.ts | 18 +- .../src/{pages/app => }/api/profileApi.ts | 6 +- .../css/common/_animations.scss | 0 .../resources => }/css/common/_colors.scss | 0 .../css/common/_components.scss | 0 .../resources => }/css/common/_material.scss | 0 .../css/lib/fonts/material-symbols.scss | 0 .../css/lib/fonts/material-symbols.woff2 | Bin .../css/lib/fonts/montserrat.scss | 0 .../JTUSjIg1_i6t8kCHKm459W1hyyTh89ZNpQ.woff2 | Bin .../JTUSjIg1_i6t8kCHKm459WRhyyTh89ZNpQ.woff2 | Bin .../JTUSjIg1_i6t8kCHKm459WZhyyTh89ZNpQ.woff2 | Bin .../JTUSjIg1_i6t8kCHKm459WdhyyTh89ZNpQ.woff2 | Bin .../JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2 | Bin .../{pages/app/resources => }/css/style.scss | 11 - .../resources => }/images/default-avatar.png | Bin .../{pages/app/resources => }/images/logo.png | Bin frontend/src/main.tsx | 9 +- frontend/src/pages/ProtectedRoute.tsx | 2 +- frontend/src/pages/app/auth/api.ts | 19 - .../pages/app/resources/css/_panelchat.scss | 150 --- .../src/pages/app/resources/css/_profile.scss | 249 ----- .../pages/app/resources/css/_reactions.scss | 389 -------- .../pages/app/resources/css/_settings.scss | 99 -- .../app/resources/css/dialogs/_reply.scss | 31 - frontend/src/pages/{ => auth}/LoginPage.tsx | 19 +- .../src/pages/{ => auth}/RegisterPage.tsx | 15 +- .../css/_auth.scss => auth/auth.scss} | 4 +- frontend/src/pages/{ => chat}/ChatPage.tsx | 5 +- .../css/_chat.scss => chat/chat.scss} | 916 +++++++++++++++++- .../src/pages/{app => chat}/core/config.ts | 0 frontend/src/pages/{app => chat}/core/init.ts | 0 .../src/pages/{app => chat}/core/types.d.ts | 0 .../src/pages/{app => chat}/core/websocket.ts | 0 .../electron/electron.scss} | 2 +- .../pages/{app => chat}/electron/electron.ts | 2 + .../service-worker/service-worker.ts | 0 .../{app => chat}/ui/components/Alerts.tsx | 0 .../{app => chat}/ui/components/Auth.tsx | 0 .../{app => chat}/ui/components/Electron.tsx | 0 .../ui/components/chat/BottomAppBar.tsx | 0 .../ui/components/chat/ChatHeader.tsx | 0 .../ui/components/chat/ChatInputWrapper.tsx | 0 .../ui/components/chat/ChatMainHeader.tsx | 0 .../ui/components/chat/ChatMessages.tsx | 2 +- .../ui/components/chat/ChatTabs.tsx | 0 .../ui/components/chat/DMUsersList.tsx | 4 +- .../ui/components/chat/EmojiMenu.tsx | 0 .../ui/components/chat/LeftPanel.tsx | 2 +- .../ui/components/chat/Message.tsx | 6 +- .../ui/components/chat/MessageContextMenu.tsx | 0 .../components/chat/MessagePanelRenderer.tsx | 2 +- .../ui/components/chat/MessageReactions.tsx | 0 .../ui/components/chat/ReplyMessageDialog.tsx | 0 .../ui/components/chat/RightPanel.tsx | 0 .../ui/components/chat/UserProfileDialog.tsx | 2 +- .../ui/components/chat/emojiData.ts | 0 .../ui/components/core/Dialog.tsx | 0 .../ui/components/core/Quote.tsx | 0 .../ui/components/core/RichTextArea.tsx | 0 .../ui/components/core/TextField.tsx | 0 .../core/animations/AnimatedHeight.tsx | 0 .../core/animations/AnimatedOpacity.tsx | 0 .../ui/components/core/animations/types.d.ts | 0 .../ui/components/profile/CropperDialog.tsx | 0 .../ui/components/profile/ImageCropper.tsx | 0 .../ui/components/profile/ProfileDialog.tsx | 2 +- .../ui/components/settings/SettingsDialog.tsx | 2 +- .../{app => chat}/ui/hooks/useCombinedRefs.ts | 0 .../src/pages/{app => chat}/ui/hooks/useDM.ts | 2 +- .../{app => chat}/ui/hooks/useProfile.ts | 2 +- .../{app => chat}/ui/hooks/useWindowSize.ts | 0 .../pages/{app => chat}/ui/panels/DMPanel.ts | 2 +- .../{app => chat}/ui/panels/MessagePanel.ts | 0 .../ui/panels/PublicChatPanel.ts | 2 +- .../{app => chat}/ui/screen/ChatScreen.tsx | 0 .../ui/screen/DownloadAppScreen.tsx | 0 .../{app => chat}/ui/screen/LoginScreen.tsx | 2 +- .../ui/screen/RegisterScreen.tsx | 2 +- frontend/src/pages/{app => chat}/ui/state.ts | 4 +- .../{app => chat}/utils/crypto/asymmetric.ts | 0 .../{app => chat}/utils/crypto/backup.ts | 0 .../pages/{app => chat}/utils/crypto/kdf.ts | 0 .../{app => chat}/utils/crypto/symmetric.ts | 0 .../src/pages/{app => chat}/utils/material.ts | 0 .../pages/{app => chat}/utils/notification.ts | 0 .../{app => chat}/utils/push-notifications.ts | 0 .../src/pages/{app => chat}/utils/utils.ts | 0 .../{ => download-app}/DownloadAppPage.tsx | 2 + .../download-app.scss} | 0 frontend/src/pages/{ => home}/HomePage.tsx | 5 +- .../css/_homepage.scss => home/home.scss} | 2 +- .../pages/{ => not-found}/NotFoundPage.tsx | 1 + .../_404.scss => not-found/not-found.scss} | 0 97 files changed, 1019 insertions(+), 1030 deletions(-) rename frontend/src/{pages/app/auth/crypto.ts => api/authApi.ts} (80%) rename frontend/src/{pages/app => }/api/dmApi.ts (92%) rename frontend/src/{pages/app => }/api/profileApi.ts (95%) rename frontend/src/{pages/app/resources => }/css/common/_animations.scss (100%) rename frontend/src/{pages/app/resources => }/css/common/_colors.scss (100%) rename frontend/src/{pages/app/resources => }/css/common/_components.scss (100%) rename frontend/src/{pages/app/resources => }/css/common/_material.scss (100%) rename frontend/src/{pages/app/resources => }/css/lib/fonts/material-symbols.scss (100%) rename frontend/src/{pages/app/resources => }/css/lib/fonts/material-symbols.woff2 (100%) rename frontend/src/{pages/app/resources => }/css/lib/fonts/montserrat.scss (100%) rename frontend/src/{pages/app/resources => }/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459W1hyyTh89ZNpQ.woff2 (100%) rename frontend/src/{pages/app/resources => }/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WRhyyTh89ZNpQ.woff2 (100%) rename frontend/src/{pages/app/resources => }/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WZhyyTh89ZNpQ.woff2 (100%) rename frontend/src/{pages/app/resources => }/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WdhyyTh89ZNpQ.woff2 (100%) rename frontend/src/{pages/app/resources => }/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2 (100%) rename frontend/src/{pages/app/resources => }/css/style.scss (78%) rename frontend/src/{pages/app/resources => }/images/default-avatar.png (100%) rename frontend/src/{pages/app/resources => }/images/logo.png (100%) delete mode 100644 frontend/src/pages/app/auth/api.ts delete mode 100644 frontend/src/pages/app/resources/css/_panelchat.scss delete mode 100644 frontend/src/pages/app/resources/css/_profile.scss delete mode 100644 frontend/src/pages/app/resources/css/_reactions.scss delete mode 100644 frontend/src/pages/app/resources/css/_settings.scss delete mode 100644 frontend/src/pages/app/resources/css/dialogs/_reply.scss rename frontend/src/pages/{ => auth}/LoginPage.tsx (92%) rename frontend/src/pages/{ => auth}/RegisterPage.tsx (93%) rename frontend/src/pages/{app/resources/css/_auth.scss => auth/auth.scss} (93%) rename frontend/src/pages/{ => chat}/ChatPage.tsx (61%) rename frontend/src/pages/{app/resources/css/_chat.scss => chat/chat.scss} (59%) rename frontend/src/pages/{app => chat}/core/config.ts (100%) rename frontend/src/pages/{app => chat}/core/init.ts (100%) rename frontend/src/pages/{app => chat}/core/types.d.ts (100%) rename frontend/src/pages/{app => chat}/core/websocket.ts (100%) rename frontend/src/pages/{app/resources/css/_electron.scss => chat/electron/electron.scss} (94%) rename frontend/src/pages/{app => chat}/electron/electron.ts (94%) rename frontend/src/pages/{app => chat}/service-worker/service-worker.ts (100%) rename frontend/src/pages/{app => chat}/ui/components/Alerts.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/Auth.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/Electron.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/chat/BottomAppBar.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/chat/ChatHeader.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/chat/ChatInputWrapper.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/chat/ChatMainHeader.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/chat/ChatMessages.tsx (99%) rename frontend/src/pages/{app => chat}/ui/components/chat/ChatTabs.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/chat/DMUsersList.tsx (96%) rename frontend/src/pages/{app => chat}/ui/components/chat/EmojiMenu.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/chat/LeftPanel.tsx (98%) rename frontend/src/pages/{app => chat}/ui/components/chat/Message.tsx (99%) rename frontend/src/pages/{app => chat}/ui/components/chat/MessageContextMenu.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/chat/MessagePanelRenderer.tsx (99%) rename frontend/src/pages/{app => chat}/ui/components/chat/MessageReactions.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/chat/ReplyMessageDialog.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/chat/RightPanel.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/chat/UserProfileDialog.tsx (97%) rename frontend/src/pages/{app => chat}/ui/components/chat/emojiData.ts (100%) rename frontend/src/pages/{app => chat}/ui/components/core/Dialog.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/core/Quote.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/core/RichTextArea.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/core/TextField.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/core/animations/AnimatedHeight.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/core/animations/AnimatedOpacity.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/core/animations/types.d.ts (100%) rename frontend/src/pages/{app => chat}/ui/components/profile/CropperDialog.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/profile/ImageCropper.tsx (100%) rename frontend/src/pages/{app => chat}/ui/components/profile/ProfileDialog.tsx (99%) rename frontend/src/pages/{app => chat}/ui/components/settings/SettingsDialog.tsx (99%) rename frontend/src/pages/{app => chat}/ui/hooks/useCombinedRefs.ts (100%) rename frontend/src/pages/{app => chat}/ui/hooks/useDM.ts (99%) rename frontend/src/pages/{app => chat}/ui/hooks/useProfile.ts (98%) rename frontend/src/pages/{app => chat}/ui/hooks/useWindowSize.ts (100%) rename frontend/src/pages/{app => chat}/ui/panels/DMPanel.ts (99%) rename frontend/src/pages/{app => chat}/ui/panels/MessagePanel.ts (100%) rename frontend/src/pages/{app => chat}/ui/panels/PublicChatPanel.ts (99%) rename frontend/src/pages/{app => chat}/ui/screen/ChatScreen.tsx (100%) rename frontend/src/pages/{app => chat}/ui/screen/DownloadAppScreen.tsx (100%) rename frontend/src/pages/{app => chat}/ui/screen/LoginScreen.tsx (99%) rename frontend/src/pages/{app => chat}/ui/screen/RegisterScreen.tsx (99%) rename frontend/src/pages/{app => chat}/ui/state.ts (99%) rename frontend/src/pages/{app => chat}/utils/crypto/asymmetric.ts (100%) rename frontend/src/pages/{app => chat}/utils/crypto/backup.ts (100%) rename frontend/src/pages/{app => chat}/utils/crypto/kdf.ts (100%) rename frontend/src/pages/{app => chat}/utils/crypto/symmetric.ts (100%) rename frontend/src/pages/{app => chat}/utils/material.ts (100%) rename frontend/src/pages/{app => chat}/utils/notification.ts (100%) rename frontend/src/pages/{app => chat}/utils/push-notifications.ts (100%) rename frontend/src/pages/{app => chat}/utils/utils.ts (100%) rename frontend/src/pages/{ => download-app}/DownloadAppPage.tsx (97%) rename frontend/src/pages/{app/resources/css/_download-app.scss => download-app/download-app.scss} (100%) rename frontend/src/pages/{ => home}/HomePage.tsx (99%) rename frontend/src/pages/{app/resources/css/_homepage.scss => home/home.scss} (99%) rename frontend/src/pages/{ => not-found}/NotFoundPage.tsx (98%) rename frontend/src/pages/{app/resources/css/_404.scss => not-found/not-found.scss} (100%) diff --git a/.gitignore b/.gitignore index 404f860..096357a 100644 --- a/.gitignore +++ b/.gitignore @@ -573,4 +573,4 @@ package-lock.json backend/alembic/** !backend/alembic/env.py !backend/alembic/script.py.mako -!frontend/src/pages/app/resources/css/lib \ No newline at end of file +!frontend/src/css/lib \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 15466af..2e07646 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,17 +1,17 @@ import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; -import { ElectronTitleBar } from "./pages/app/ui/components/Electron"; -import { useAppState } from "./pages/app/ui/state"; +import { ElectronTitleBar } from "./pages/chat/ui/components/Electron"; +import { useAppState } from "./pages/chat/ui/state"; import { useEffect, useState } from "react"; -import HomePage from "./pages/HomePage"; -import LoginPage from "./pages/LoginPage"; -import RegisterPage from "./pages/RegisterPage"; -import ChatPage from "./pages/ChatPage"; -import DownloadAppPage from "./pages/DownloadAppPage"; -import NotFoundPage from "./pages/NotFoundPage"; +import HomePage from "./pages/home/HomePage"; +import LoginPage from "./pages/auth/LoginPage"; +import RegisterPage from "./pages/auth/RegisterPage"; +import ChatPage from "./pages/chat/ChatPage"; +import DownloadAppPage from "./pages/download-app/DownloadAppPage"; +import NotFoundPage from "./pages/not-found/NotFoundPage"; import ProtectedRoute from "./pages/ProtectedRoute"; -import { isElectron } from "./pages/app/electron/electron"; -import { MINIMUM_WIDTH } from "./pages/app/core/config"; -import useWindowSize from "./pages/app/ui/hooks/useWindowSize"; +import { isElectron } from "./pages/chat/electron/electron"; +import { MINIMUM_WIDTH } from "./pages/chat/core/config"; +import useWindowSize from "./pages/chat/ui/hooks/useWindowSize"; export default function App() { const { restoreUserFromStorage } = useAppState(); @@ -31,7 +31,7 @@ export default function App() {
- z} /> + } /> } /> } /> } /> diff --git a/frontend/src/pages/app/auth/crypto.ts b/frontend/src/api/authApi.ts similarity index 80% rename from frontend/src/pages/app/auth/crypto.ts rename to frontend/src/api/authApi.ts index 1e16ff1..86fab4e 100644 --- a/frontend/src/pages/app/auth/crypto.ts +++ b/frontend/src/api/authApi.ts @@ -1,9 +1,26 @@ -import { API_BASE_URL } from "../core/config"; -import { getAuthHeaders } from "./api"; -import { generateX25519KeyPair } from "../utils/crypto/asymmetric"; -import { encryptBackupWithPassword, decryptBackupWithPassword, encodeBlob, decodeBlob } from "../utils/crypto/backup"; -import { b64, ub64 } from "../utils/utils"; -import type { BackupBlob, UploadPublicKeyRequest } from "../core/types"; +import type { Headers, UploadPublicKeyRequest, BackupBlob } from "../pages/chat/core/types"; +import { generateX25519KeyPair } from "../pages/chat/utils/crypto/asymmetric"; +import { encodeBlob, encryptBackupWithPassword, decryptBackupWithPassword, decodeBlob } from "../pages/chat/utils/crypto/backup"; +import { b64, ub64 } from "../pages/chat/utils/utils"; +import { API_BASE_URL } from "../pages/chat/core/config"; + +/** + * Generates authentication headers for API requests + * @param {boolean} json - Whether to include JSON content type header + * @returns {Headers} Headers object with authentication and content type + */ +export function getAuthHeaders(token: string | null, json: boolean = true): Headers { + const headers: Headers = {}; + + if (json) { + headers["Content-Type"] = "application/json"; + } + + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + return headers; +} let currentPublicKey: Uint8Array | null = null; let currentPrivateKey: Uint8Array | null = null; diff --git a/frontend/src/pages/app/api/dmApi.ts b/frontend/src/api/dmApi.ts similarity index 92% rename from frontend/src/pages/app/api/dmApi.ts rename to frontend/src/api/dmApi.ts index 84d6347..7c195af 100644 --- a/frontend/src/pages/app/api/dmApi.ts +++ b/frontend/src/api/dmApi.ts @@ -1,12 +1,12 @@ -import { API_BASE_URL } from "../core/config"; -import { getAuthHeaders } from "../auth/api"; -import { ecdhSharedSecret, deriveWrappingKey } from "../utils/crypto/asymmetric"; -import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../utils/crypto/symmetric"; -import { randomBytes } from "../utils/crypto/kdf"; -import { getCurrentKeys } from "../auth/crypto"; -import { request } from "../core/websocket"; -import type { SendDMRequest, DmEnvelope, User, DMEditRequest, DmEncryptedJSON, BaseDmEnvelope } from "../core/types"; -import { b64, ub64 } from "../utils/utils"; +import { API_BASE_URL } from "../pages/chat/core/config"; +import { getAuthHeaders } from "./authApi"; +import { ecdhSharedSecret, deriveWrappingKey } from "../pages/chat/utils/crypto/asymmetric"; +import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../pages/chat/utils/crypto/symmetric"; +import { randomBytes } from "../pages/chat/utils/crypto/kdf"; +import { getCurrentKeys } from "./authApi"; +import { request } from "../pages/chat/core/websocket"; +import type { SendDMRequest, DmEnvelope, User, DMEditRequest, DmEncryptedJSON, BaseDmEnvelope } from "../pages/chat/core/types"; +import { b64, ub64 } from "../pages/chat/utils/utils"; export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string): Promise { const keys = getCurrentKeys(); diff --git a/frontend/src/pages/app/api/profileApi.ts b/frontend/src/api/profileApi.ts similarity index 95% rename from frontend/src/pages/app/api/profileApi.ts rename to frontend/src/api/profileApi.ts index a555821..85c382b 100644 --- a/frontend/src/pages/app/api/profileApi.ts +++ b/frontend/src/api/profileApi.ts @@ -1,6 +1,6 @@ -import { getAuthHeaders } from "../auth/api"; -import { API_BASE_URL } from "../core/config"; -import type { UserProfile } from "../core/types"; +import { getAuthHeaders } from "./authApi"; +import { API_BASE_URL } from "../pages/chat/core/config"; +import type { UserProfile } from "../pages/chat/core/types"; export interface ProfileData { profile_picture?: string; diff --git a/frontend/src/pages/app/resources/css/common/_animations.scss b/frontend/src/css/common/_animations.scss similarity index 100% rename from frontend/src/pages/app/resources/css/common/_animations.scss rename to frontend/src/css/common/_animations.scss diff --git a/frontend/src/pages/app/resources/css/common/_colors.scss b/frontend/src/css/common/_colors.scss similarity index 100% rename from frontend/src/pages/app/resources/css/common/_colors.scss rename to frontend/src/css/common/_colors.scss diff --git a/frontend/src/pages/app/resources/css/common/_components.scss b/frontend/src/css/common/_components.scss similarity index 100% rename from frontend/src/pages/app/resources/css/common/_components.scss rename to frontend/src/css/common/_components.scss diff --git a/frontend/src/pages/app/resources/css/common/_material.scss b/frontend/src/css/common/_material.scss similarity index 100% rename from frontend/src/pages/app/resources/css/common/_material.scss rename to frontend/src/css/common/_material.scss diff --git a/frontend/src/pages/app/resources/css/lib/fonts/material-symbols.scss b/frontend/src/css/lib/fonts/material-symbols.scss similarity index 100% rename from frontend/src/pages/app/resources/css/lib/fonts/material-symbols.scss rename to frontend/src/css/lib/fonts/material-symbols.scss diff --git a/frontend/src/pages/app/resources/css/lib/fonts/material-symbols.woff2 b/frontend/src/css/lib/fonts/material-symbols.woff2 similarity index 100% rename from frontend/src/pages/app/resources/css/lib/fonts/material-symbols.woff2 rename to frontend/src/css/lib/fonts/material-symbols.woff2 diff --git a/frontend/src/pages/app/resources/css/lib/fonts/montserrat.scss b/frontend/src/css/lib/fonts/montserrat.scss similarity index 100% rename from frontend/src/pages/app/resources/css/lib/fonts/montserrat.scss rename to frontend/src/css/lib/fonts/montserrat.scss diff --git a/frontend/src/pages/app/resources/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459W1hyyTh89ZNpQ.woff2 b/frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459W1hyyTh89ZNpQ.woff2 similarity index 100% rename from frontend/src/pages/app/resources/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459W1hyyTh89ZNpQ.woff2 rename to frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459W1hyyTh89ZNpQ.woff2 diff --git a/frontend/src/pages/app/resources/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WRhyyTh89ZNpQ.woff2 b/frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WRhyyTh89ZNpQ.woff2 similarity index 100% rename from frontend/src/pages/app/resources/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WRhyyTh89ZNpQ.woff2 rename to frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WRhyyTh89ZNpQ.woff2 diff --git a/frontend/src/pages/app/resources/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WZhyyTh89ZNpQ.woff2 b/frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WZhyyTh89ZNpQ.woff2 similarity index 100% rename from frontend/src/pages/app/resources/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WZhyyTh89ZNpQ.woff2 rename to frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WZhyyTh89ZNpQ.woff2 diff --git a/frontend/src/pages/app/resources/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WdhyyTh89ZNpQ.woff2 b/frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WdhyyTh89ZNpQ.woff2 similarity index 100% rename from frontend/src/pages/app/resources/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WdhyyTh89ZNpQ.woff2 rename to frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WdhyyTh89ZNpQ.woff2 diff --git a/frontend/src/pages/app/resources/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2 b/frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2 similarity index 100% rename from frontend/src/pages/app/resources/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2 rename to frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2 diff --git a/frontend/src/pages/app/resources/css/style.scss b/frontend/src/css/style.scss similarity index 78% rename from frontend/src/pages/app/resources/css/style.scss rename to frontend/src/css/style.scss index e129c88..b490119 100644 --- a/frontend/src/pages/app/resources/css/style.scss +++ b/frontend/src/css/style.scss @@ -1,18 +1,7 @@ -@use "auth"; -@use "chat"; -@use "profile"; -@use "settings"; -@use "panelchat"; @use "common/animations"; @use "common/components"; @use "common/colors" as *; @use "common/material" as *; -@use "electron"; -@use "dialogs/reply"; -@use "download-app"; -@use "404" as not-found; -@use "homepage"; -@use "reactions"; @use "lib/fonts/montserrat"; @use "lib/fonts/material-symbols"; diff --git a/frontend/src/pages/app/resources/images/default-avatar.png b/frontend/src/images/default-avatar.png similarity index 100% rename from frontend/src/pages/app/resources/images/default-avatar.png rename to frontend/src/images/default-avatar.png diff --git a/frontend/src/pages/app/resources/images/logo.png b/frontend/src/images/logo.png similarity index 100% rename from frontend/src/pages/app/resources/images/logo.png rename to frontend/src/images/logo.png diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 2296930..c7f0e07 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -5,17 +5,16 @@ * @version 1.0.0 */ -import './pages/app/resources/css/style.scss'; +import './css/style.scss'; import "mdui/mdui.css"; -import "./pages/app/utils/material"; -import "./pages/app/core/init"; -import "./pages/app/electron/electron"; +import "./pages/chat/utils/material"; +import "./pages/chat/core/init"; +import "./pages/chat/electron/electron"; import { createRoot } from 'react-dom/client'; import App from './App'; import { StrictMode } from 'react'; -// Initialize React app createRoot(document.getElementById("root")!).render( diff --git a/frontend/src/pages/ProtectedRoute.tsx b/frontend/src/pages/ProtectedRoute.tsx index 40c4652..5036612 100644 --- a/frontend/src/pages/ProtectedRoute.tsx +++ b/frontend/src/pages/ProtectedRoute.tsx @@ -1,5 +1,5 @@ import { useEffect } from "react"; -import { useAppState } from "./app/ui/state"; +import { useAppState } from "./chat/ui/state"; import { useNavigate } from "react-router-dom"; interface ProtectedRouteProps { diff --git a/frontend/src/pages/app/auth/api.ts b/frontend/src/pages/app/auth/api.ts deleted file mode 100644 index 875404d..0000000 --- a/frontend/src/pages/app/auth/api.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Headers } from "../core/types"; - -/** - * Generates authentication headers for API requests - * @param {boolean} json - Whether to include JSON content type header - * @returns {Headers} Headers object with authentication and content type - */ -export function getAuthHeaders(token: string | null, json: boolean = true): Headers { - const headers: Headers = {}; - - if (json) { - headers["Content-Type"] = "application/json"; - } - - if (token) { - headers['Authorization'] = `Bearer ${token}`; - } - return headers; -} \ No newline at end of file diff --git a/frontend/src/pages/app/resources/css/_panelchat.scss b/frontend/src/pages/app/resources/css/_panelchat.scss deleted file mode 100644 index 9c17533..0000000 --- a/frontend/src/pages/app/resources/css/_panelchat.scss +++ /dev/null @@ -1,150 +0,0 @@ -@use "common/colors" as *; -@use "common/material" as *; - - -// контейнер чата и панели с чатами -.all-container { - display: flex; - flex-direction: row; - width: 100%; - height: 100%; -} - -#profile { - display: none; - flex-direction: column; - z-index: 2000; - top: 0; - left: 0; - position: fixed; - height: 100vh; - width: 27%; - background-color: $color-dark-surface-container; - position: relative; - - .profileheader { - display: flex; - gap: 200px; - - p { - color: white; - } - - a { - text-decoration: none; - color: white; - border: solid 2px $color-dark-on-surface-variant; - padding: 5px; - border-radius: 10px; - - &:hover { - background-color: rgba(255, 255, 255, 0.241); - } - } - } -} - -#chat-list { - display: flex; - flex-direction: column; - flex-grow: 0; - width: 40%; - background-color: $color-dark-surface-container; - height: 100%; - z-index: 1000; - min-height: 0; // allow children to manage their own scrolling - - .chat-header-left { - display: flex; - color: white; - font-size: 25px; - background-color: $color-dark-surface-container; - width: 100%; - justify-content: center; - align-items: center; - padding: 16px; - overflow: hidden; - - .product-name { - flex-grow: 1; - } - - .profile { - font-size: 24px; - display: flex; - justify-content: start; - flex-shrink: 0; - flex-grow: 0; - - #closeprofile a { - text-decoration: none; - color: white; - border: solid 2px $color-dark-on-surface-variant; - padding: 5px; - border-radius: 10px; - - &:hover { - background-color: rgba(255, 255, 255, 0.241); - } - } - - img { - $size: 45px; - display: flex; - border-radius: 50%; - width: $size; - height: $size; - } - } - } - - .chat-tabs { - margin-top: 5px; - width: 100%; - height: calc(100% - 80px); - display: flex; - flex-direction: column; - min-height: 0; // prevent flex collapse when inner overflows - --mdui-color-surface: $color-dark-surface-container; - --mdui-color-surface-variant: transparent; - - img { - width: 45px; - height: 45px; - border-radius: 20%; - object-fit: cover; - margin-right: 1rem; - } - - mdui-tabs { - height: 100%; - display: flex; - flex-direction: column; - min-height: 0; // enable inner panel to scroll - } - - mdui-tab-panel[active] { - flex: 1; - display: flex; - flex-direction: column; - min-height: 0; // critical to avoid collapsing - overflow: hidden; - } - - mdui-list { - flex: 1; - min-height: 0; // allow scroll area to size correctly - overflow-y: auto; - padding: 0; - margin: 0; - } - } - - mdui-bottom-app-bar { - position: relative; - width: 100%; - padding-left: 16px; - padding-right: 16px; - margin-top: auto; - } -} \ No newline at end of file diff --git a/frontend/src/pages/app/resources/css/_profile.scss b/frontend/src/pages/app/resources/css/_profile.scss deleted file mode 100644 index f3f60a4..0000000 --- a/frontend/src/pages/app/resources/css/_profile.scss +++ /dev/null @@ -1,249 +0,0 @@ -@use "common/colors" as *; -@use "common/material" as *; - -#profile-dialog .content { - display: flex; - flex-direction: column; - gap: 24px; - min-width: 400px; - - .header-top { - display: flex; - flex-direction: row; - align-items: center; - gap: 16px; - position: relative; - padding-bottom: 16px; - border-bottom: 1px solid $color-dark-outline; - - .profile-picture-container { - position: relative; - $size: 70px; - width: $size; - height: $size; - flex-shrink: 0; - - #profile-picture { - width: $size; - height: $size; - border-radius: 50%; - object-fit: cover; - } - - .upload-overlay { - position: absolute; - bottom: 0; - right: 0; - width: 28px; - height: 28px; - cursor: pointer; - } - } - - mdui-text-field { - flex: 1; - } - } - - #profile-form { - display: flex; - flex-direction: column; - gap: 16px; - - mdui-text-field { - width: 100%; - } - - .dialog-actions { - display: flex; - gap: 12px; - padding-top: 16px; - border-top: 1px solid $color-dark-outline; - - > * { - flex: 1; - } - } - } -} - -// User profile dialog content styles -#user-profile-dialog .content { - display: flex; - gap: 1.5rem; - - .profile-picture-section { - flex-shrink: 0; - - .profile-picture { - width: 80px; - height: 80px; - border-radius: 50%; - object-fit: cover; - border: 2px solid $color-dark-outline; - } - } - - .profile-info { - flex: 1; - display: flex; - flex-direction: column; - gap: 1rem; - - .username-section { - display: flex; - align-items: center; - gap: 0.75rem; - - .username { - margin: 0; - color: $color-dark-on-surface; - font-size: 1.1rem; - font-weight: 600; - } - - .online-status { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.85rem; - padding: 0.25rem 0.5rem; - border-radius: 12px; - font-weight: 500; - - &.online { - color: $success; - background-color: rgba(76, 175, 80, 0.1); - - .online-indicator { - width: 8px; - height: 8px; - border-radius: 50%; - background-color: $success; - } - } - - &.offline { - color: $color-dark-on-surface-variant; - background-color: rgba(255, 255, 255, 0.05); - - .offline-indicator { - width: 8px; - height: 8px; - border-radius: 50%; - background-color: $color-dark-on-surface-variant; - } - } - } - } - - .bio-section { - label { - display: block; - color: $color-dark-on-surface-variant; - font-size: 0.85rem; - font-weight: 500; - margin-bottom: 0.5rem; - } - - .bio-display { - color: $color-dark-on-surface; - font-size: 0.9rem; - line-height: 1.4; - padding: 0.75rem; - background-color: $color-dark-surface; - border-radius: 8px; - border: 1px solid $color-dark-outline; - min-height: 60px; - } - - .bio-actions { - display: flex; - gap: 0.5rem; - margin-top: 0.5rem; - } - } - - .profile-stats { - display: flex; - flex-direction: column; - gap: 0.5rem; - - .stat { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.5rem 0; - - .stat-label { - color: $color-dark-on-surface-variant; - font-size: 0.85rem; - } - - .stat-value { - color: $color-dark-on-surface; - font-size: 0.85rem; - font-weight: 500; - } - } - } - - .profile-actions { - display: flex; - gap: 0.75rem; - margin-top: 0.5rem; - - mdui-button { - flex: 1; - } - } - } -} - -// Cropper Dialog Styles -#cropper-dialog { - .cropper-dialog-content { - display: flex; - flex-direction: column; - gap: 16px; - min-width: 500px; - max-width: 600px; - } - - .cropper-header { - display: flex; - justify-content: space-between; - align-items: center; - padding-bottom: 16px; - border-bottom: 1px solid $color-dark-outline; - - h3 { - margin: 0; - color: $color-dark-on-surface; - } - } - - .cropper-container { - display: flex; - justify-content: center; - align-items: center; - min-height: 400px; - background: $color-dark-surface-container; - border-radius: 8px; - overflow: hidden; - - #cropper-area { - width: 100%; - height: 100%; - min-height: 400px; - } - } - - .cropper-actions { - display: flex; - gap: 12px; - justify-content: flex-end; - padding-top: 16px; - border-top: 1px solid $color-dark-outline; - } -} \ No newline at end of file diff --git a/frontend/src/pages/app/resources/css/_reactions.scss b/frontend/src/pages/app/resources/css/_reactions.scss deleted file mode 100644 index f64402d..0000000 --- a/frontend/src/pages/app/resources/css/_reactions.scss +++ /dev/null @@ -1,389 +0,0 @@ -@use "common/material" as *; -@use "sass:color"; - -// Reaction styles -.message-reactions { - display: flex; - flex-wrap: wrap; - gap: 4px; - margin-top: 8px; - margin-left: 10px; - margin-right: 10px; - animation: messageReactionsFadeIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); -} - -.reaction-button { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - border: none; - border-radius: 16px; - background-color: $color-dark-surface-container; - cursor: pointer; - transition: transform 0.2s ease, background-color 0.2s ease; - font-size: 1px; - min-height: 28px; - animation: reactionFadeIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); - - &.removing { - animation: reactionFadeOut 0.2s ease forwards; - } - - &:hover { - background-color: $color-dark-surface-container-high; - transform: scale(1.05); - } - - &.reacted { - background-color: $color-dark-primary-container; - border-color: $color-dark-primary; - color: $color-dark-on-primary-container; - - &:hover { - background-color: color.adjust($color-dark-primary-container, $lightness: 20%); - } - } -} - -.reaction-emoji { - font-size: 17px; - line-height: 1; -} - -.reaction-count { - font-size: 12px; - font-weight: 500; - line-height: 1; -} - -// Reaction bar styles (standalone) -.reaction-bar { - background: $color-dark-surface-container; - border: 1px solid $color-dark-outline; - border-radius: 24px; - padding: 8px; - opacity: 1; - transition: all 0.15s ease; - backdrop-filter: blur(8px); - transform: translateY(0); - - &.closing { - opacity: 0; - transform: scale(0.8); - } -} - -// Emoji menu wrapper inside reaction bar -.emoji-menu-wrapper { - width: 320px; - height: 400px; - display: flex; - align-items: center; - justify-content: center; -} - -// Context menu wrapper with animations -.context-menu-wrapper { - position: relative; - display: block; - - // Animation states - &.entering { - opacity: 0; - transform: scale(0.8); - animation: contextMenuEnter 0.2s ease forwards; - } - - &.entering-left { - opacity: 0; - transform: translateX(-20px) scale(0.8); - animation: contextMenuEnterLeft 0.2s ease forwards; - } - - &.entering-up { - opacity: 0; - transform: translateY(20px) scale(0.8); - animation: contextMenuEnterUp 0.2s ease forwards; - } - - &.entering-up-left { - opacity: 0; - transform: translateX(-20px) translateY(20px) scale(0.8); - animation: contextMenuEnterUpLeft 0.2s ease forwards; - } - - &.closing { - opacity: 1; - transform: scale(1); - animation: contextMenuClose 0.2s ease forwards; - } - - &.closing-left { - opacity: 1; - transform: translateX(0) scale(1); - animation: contextMenuCloseLeft 0.2s ease forwards; - } - - &.closing-up { - opacity: 1; - transform: translateY(0) scale(1); - animation: contextMenuCloseUp 0.2s ease forwards; - } - - &.closing-up-left { - opacity: 1; - transform: translateX(0) translateY(0) scale(1); - animation: contextMenuCloseUpLeft 0.2s ease forwards; - } -} - -// Reaction bar inside context menu wrapper -.context-menu-reaction-bar { - display: flex; - align-items: center; - gap: 4px; - padding: 8px 12px; - background: $color-dark-surface-container; - border: 1px solid $color-dark-outline; - border-radius: 16px; - position: absolute; - bottom: 100%; - justify-content: center; - margin-bottom: 10px; - transition: width 0.3s ease-out, height 0.3s ease-out; - - &.left { - left: 0; - transform: translateX(0); - } - - &.right { - right: 0; - transform: translateX(0); - } - - &.expanded { - padding: 0; - overflow: hidden; - width: 320px; - height: 400px; - border-radius: 16px; - - // Default: expand downward from the reaction bar's bottom edge - position: absolute; - bottom: auto; - top: 0; - left: 0; - transform: translateY(0); - - &.expand-upward { - // Expand upward from the reaction bar's top edge - bottom: 100%; - top: auto; - margin-bottom: 10px; - margin-top: 0; - transform: translateY(0); - } - - .emoji-menu-wrapper { - animation: emojiMenuEnter 0.5s ease; - } - } -} - -@keyframes emojiMenuEnter { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} - -.reaction-bar-content { - display: flex; - align-items: center; - gap: 4px; - transition: opacity 0.3s ease-out; - - &.faded { - opacity: 0; - } -} - -.reaction-emoji-button { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: none; - border-radius: 16px; - background: transparent; - cursor: pointer; - transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); - font-size: 18px; - - &:hover { - background: var(--mdui-color-surface-container-high); - transform: scale(1.3); - box-shadow: var(--mdui-elevation-1); - } - - &:active { - transform: scale(0.95); - transition: transform 0.1s ease; - } -} - -.reaction-expand-button { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: 1px solid var(--mdui-color-outline); - border-radius: 16px; - background: var(--mdui-color-surface); - cursor: pointer; - transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); - - &:hover { - background: var(--mdui-color-surface-container-high); - border-color: var(--mdui-color-primary); - transform: scale(1.1); - box-shadow: var(--mdui-elevation-1); - } - - &:active { - transform: scale(0.95); - transition: transform 0.1s ease; - } - - .material-symbols { - font-size: 18px; - color: var(--mdui-color-on-surface); - transition: transform 0.2s ease; - } - - &:hover .material-symbols { - transform: rotate(90deg); - } -} - -// Animation for reactions appearing/disappearing -@keyframes messageReactionsFadeIn { - from { - opacity: 0; - transform: translateY(-10px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes reactionFadeIn { - from { - opacity: 0; - transform: scale(0.8); - } - - to { - opacity: 1; - transform: scale(1); - } -} - -@keyframes reactionFadeOut { - from { - opacity: 1; - transform: scale(1); - } - - to { - opacity: 0; - transform: scale(0.8); - } -} - -// Context menu wrapper animations -@keyframes contextMenuEnter { - to { - opacity: 1; - transform: scale(1); - } -} - -@keyframes contextMenuEnterLeft { - to { - opacity: 1; - transform: translateX(0) scale(1); - } -} - -@keyframes contextMenuEnterUp { - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -@keyframes contextMenuEnterUpLeft { - to { - opacity: 1; - transform: translateX(0) translateY(0) scale(1); - } -} - -@keyframes contextMenuClose { - to { - opacity: 0; - transform: scale(0.8); - } -} - -@keyframes contextMenuCloseLeft { - to { - opacity: 0; - transform: translateX(-20px) scale(0.8); - } -} - -@keyframes contextMenuCloseUp { - to { - opacity: 0; - transform: translateY(20px) scale(0.8); - } -} - -@keyframes contextMenuCloseUpLeft { - to { - opacity: 0; - transform: translateX(-20px) translateY(20px) scale(0.8); - } -} - -// Mobile responsive -@media (max-width: 768px) { - .reaction-bar { - padding: 6px; - } - - .reaction-emoji-button, - .reaction-expand-button { - width: 28px; - height: 28px; - } - - .reaction-emoji-button { - font-size: 16px; - } - - .reaction-expand-button .material-symbols { - font-size: 16px; - } -} diff --git a/frontend/src/pages/app/resources/css/_settings.scss b/frontend/src/pages/app/resources/css/_settings.scss deleted file mode 100644 index 4607769..0000000 --- a/frontend/src/pages/app/resources/css/_settings.scss +++ /dev/null @@ -1,99 +0,0 @@ -@use "common/colors" as *; -@use "common/material" as *; - -#settings-dialog { - .fullscreen-wrapper { - display: flex; - align-items: center; - justify-content: center; - height: calc(100vh - (1.5rem * 2)); - width: 100%; - position: relative; - - #settings-dialog-inner { - max-width: 1200px; - max-height: 1000px; - width: 100%; - height: 100%; - - .header { - display: flex; - flex-direction: row; - gap: 10px; - margin-bottom: 16px; - } - - #settings-menu { - display: flex; - flex-direction: row; - gap: 16px; - - mdui-list { - max-width: 280px; - padding-right: 16px; - overflow-y: auto; - } - - .screen { - flex: 1; - overflow-y: auto; - position: relative; - - .settings-panel { - display: flex; - flex-direction: column; - gap: 16px; - opacity: 0; - visibility: hidden; - transform: translateY(20px); - transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s ease; - position: absolute; - top: 0; - left: 0; - width: 100%; - - &.active { - opacity: 1; - visibility: visible; - transform: translateY(0); - position: relative; - } - - h3 { - margin: 0 0 16px 0; - color: $color-dark-on-surface; - } - - mdui-text-field, - mdui-select, - mdui-switch, - mdui-button { - margin-bottom: 8px; - } - - mdui-switch { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 0; - border-bottom: 1px solid $color-dark-outline; - - &:last-child { - border-bottom: none; - } - } - - p { - margin: 8px 0; - color: $color-dark-on-surface-variant; - } - - mdui-linear-progress { - margin: 16px 0; - } - } - } - } - } - } -} \ No newline at end of file diff --git a/frontend/src/pages/app/resources/css/dialogs/_reply.scss b/frontend/src/pages/app/resources/css/dialogs/_reply.scss deleted file mode 100644 index 420d389..0000000 --- a/frontend/src/pages/app/resources/css/dialogs/_reply.scss +++ /dev/null @@ -1,31 +0,0 @@ -@use "../common/material" as *; - -.reply-dialog .dialog-content { - width: 300px; - overflow-x:hidden; - - .reply-preview-dialog { - margin-bottom: 1rem; - padding: 16px; - background-color: $color-dark-surface-container; - border-radius: 16px; - - .reply-content { - display: flex; - flex-direction: column; - gap: 0.25rem; - - .reply-username { - font-weight: 600; - color: $color-dark-on-surface; - font-size: 0.85rem; - } - - .reply-text { - color: $color-dark-on-surface-variant; - font-size: 0.9rem; - line-height: 1.4; - } - } - } -} \ No newline at end of file diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/auth/LoginPage.tsx similarity index 92% rename from frontend/src/pages/LoginPage.tsx rename to frontend/src/pages/auth/LoginPage.tsx index 0fdb5b4..4bbb165 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/auth/LoginPage.tsx @@ -1,16 +1,17 @@ import { useImmer } from "use-immer"; -import { AlertsContainer, type Alert, type AlertType } from "./app/ui/components/Alerts"; -import { AuthContainer, AuthHeader } from "./app/ui/components/Auth"; -import type { ErrorResponse, LoginRequest, LoginResponse } from "./app/core/types"; -import { ensureKeysOnLogin } from "./app/auth/crypto"; -import { API_BASE_URL } from "./app/core/config"; +import { AlertsContainer, type Alert, type AlertType } from "../chat/ui/components/Alerts"; +import { AuthContainer, AuthHeader } from "../chat/ui/components/Auth"; +import type { ErrorResponse, LoginRequest, LoginResponse } from "../chat/core/types"; +import { ensureKeysOnLogin } from "../../api/authApi"; +import { API_BASE_URL } from "../chat/core/config"; import { useRef } from "react"; import type { TextField } from "mdui/components/text-field"; -import { useAppState } from "./app/ui/state"; -import { MaterialTextField } from "./app/ui/components/core/TextField"; -import { initialize, isSupported, startElectronReceiver, subscribe } from "./app/utils/push-notifications"; -import { isElectron } from "./app/electron/electron"; +import { useAppState } from "../chat/ui/state"; +import { MaterialTextField } from "../chat/ui/components/core/TextField"; +import { initialize, isSupported, startElectronReceiver, subscribe } from "../chat/utils/push-notifications"; +import { isElectron } from "../chat/electron/electron"; import { useNavigate } from "react-router-dom"; +import "./auth.scss"; export default function LoginPage() { const [alerts, updateAlerts] = useImmer([]); diff --git a/frontend/src/pages/RegisterPage.tsx b/frontend/src/pages/auth/RegisterPage.tsx similarity index 93% rename from frontend/src/pages/RegisterPage.tsx rename to frontend/src/pages/auth/RegisterPage.tsx index 2e437e1..ec2b906 100644 --- a/frontend/src/pages/RegisterPage.tsx +++ b/frontend/src/pages/auth/RegisterPage.tsx @@ -1,14 +1,15 @@ import { useImmer } from "use-immer"; -import { AuthContainer, AuthHeader } from "./app/ui/components/Auth"; -import { AlertsContainer, type Alert, type AlertType } from "./app/ui/components/Alerts"; +import { AuthContainer, AuthHeader } from "../chat/ui/components/Auth"; +import { AlertsContainer, type Alert, type AlertType } from "../chat/ui/components/Alerts"; import { useRef } from "react"; import { TextField } from "mdui/components/text-field"; -import type { ErrorResponse, RegisterRequest, LoginResponse } from "./app/core/types"; -import { API_BASE_URL } from "./app/core/config"; -import { useAppState } from "./app/ui/state"; -import { MaterialTextField } from "./app/ui/components/core/TextField"; -import { ensureKeysOnLogin } from "./app/auth/crypto"; +import type { ErrorResponse, RegisterRequest, LoginResponse } from "../chat/core/types"; +import { API_BASE_URL } from "../chat/core/config"; +import { useAppState } from "../chat/ui/state"; +import { MaterialTextField } from "../chat/ui/components/core/TextField"; +import { ensureKeysOnLogin } from "../../api/authApi"; import { useNavigate } from "react-router-dom"; +import "./auth.scss"; export default function RegisterPage() { const [alerts, updateAlerts] = useImmer([]); diff --git a/frontend/src/pages/app/resources/css/_auth.scss b/frontend/src/pages/auth/auth.scss similarity index 93% rename from frontend/src/pages/app/resources/css/_auth.scss rename to frontend/src/pages/auth/auth.scss index 59ad690..aea00ca 100644 --- a/frontend/src/pages/app/resources/css/_auth.scss +++ b/frontend/src/pages/auth/auth.scss @@ -1,5 +1,5 @@ -@use "common/colors" as *; -@use "common/material" as *; +@use "../../css/common/colors" as *; +@use "../../css/common/material" as *; .auth-container { display: flex; diff --git a/frontend/src/pages/ChatPage.tsx b/frontend/src/pages/chat/ChatPage.tsx similarity index 61% rename from frontend/src/pages/ChatPage.tsx rename to frontend/src/pages/chat/ChatPage.tsx index 0989a42..bbf547d 100644 --- a/frontend/src/pages/ChatPage.tsx +++ b/frontend/src/pages/chat/ChatPage.tsx @@ -1,5 +1,6 @@ -import { LeftPanel } from "./app/ui/components/chat/LeftPanel"; -import { RightPanel } from "./app/ui/components/chat/RightPanel"; +import { LeftPanel } from "./ui/components/chat/LeftPanel"; +import { RightPanel } from "./ui/components/chat/RightPanel"; +import "./chat.scss"; export default function ChatPage() { return ( diff --git a/frontend/src/pages/app/resources/css/_chat.scss b/frontend/src/pages/chat/chat.scss similarity index 59% rename from frontend/src/pages/app/resources/css/_chat.scss rename to frontend/src/pages/chat/chat.scss index a333156..f17af69 100644 --- a/frontend/src/pages/app/resources/css/_chat.scss +++ b/frontend/src/pages/chat/chat.scss @@ -1,5 +1,5 @@ -@use "common/colors" as *; -@use "common/material" as *; +@use "../../css/common/colors" as *; +@use "../../css/common/material" as *; @use "sass:color"; #chat-interface { @@ -982,3 +982,915 @@ overflow: visible; } } + +// Panel chat styles +// контейнер чата и панели с чатами +.all-container { + display: flex; + flex-direction: row; + width: 100%; + height: 100%; +} + +#profile { + display: none; + flex-direction: column; + z-index: 2000; + top: 0; + left: 0; + position: fixed; + height: 100vh; + width: 27%; + background-color: $color-dark-surface-container; + position: relative; + + .profileheader { + display: flex; + gap: 200px; + + p { + color: white; + } + + a { + text-decoration: none; + color: white; + border: solid 2px $color-dark-on-surface-variant; + padding: 5px; + border-radius: 10px; + + &:hover { + background-color: rgba(255, 255, 255, 0.241); + } + } + } +} + +#chat-list { + display: flex; + flex-direction: column; + flex-grow: 0; + width: 40%; + background-color: $color-dark-surface-container; + height: 100%; + z-index: 1000; + min-height: 0; // allow children to manage their own scrolling + + .chat-header-left { + display: flex; + color: white; + font-size: 25px; + background-color: $color-dark-surface-container; + width: 100%; + justify-content: center; + align-items: center; + padding: 16px; + overflow: hidden; + + .product-name { + flex-grow: 1; + } + + .profile { + font-size: 24px; + display: flex; + justify-content: start; + flex-shrink: 0; + flex-grow: 0; + + #closeprofile a { + text-decoration: none; + color: white; + border: solid 2px $color-dark-on-surface-variant; + padding: 5px; + border-radius: 10px; + + &:hover { + background-color: rgba(255, 255, 255, 0.241); + } + } + + img { + $size: 45px; + display: flex; + border-radius: 50%; + width: $size; + height: $size; + } + } + } + + .chat-tabs { + margin-top: 5px; + width: 100%; + height: calc(100% - 80px); + display: flex; + flex-direction: column; + min-height: 0; // prevent flex collapse when inner overflows + --mdui-color-surface: $color-dark-surface-container; + --mdui-color-surface-variant: transparent; + + img { + width: 45px; + height: 45px; + border-radius: 20%; + object-fit: cover; + margin-right: 1rem; + } + + mdui-tabs { + height: 100%; + display: flex; + flex-direction: column; + min-height: 0; // enable inner panel to scroll + } + + mdui-tab-panel[active] { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; // critical to avoid collapsing + overflow: hidden; + } + + mdui-list { + flex: 1; + min-height: 0; // allow scroll area to size correctly + overflow-y: auto; + padding: 0; + margin: 0; + } + } + + mdui-bottom-app-bar { + position: relative; + width: 100%; + padding-left: 16px; + padding-right: 16px; + margin-top: auto; + } +} + +// Profile styles +#profile-dialog .content { + display: flex; + flex-direction: column; + gap: 24px; + min-width: 400px; + + .header-top { + display: flex; + flex-direction: row; + align-items: center; + gap: 16px; + position: relative; + padding-bottom: 16px; + border-bottom: 1px solid $color-dark-outline; + + .profile-picture-container { + position: relative; + $size: 70px; + width: $size; + height: $size; + flex-shrink: 0; + + #profile-picture { + width: $size; + height: $size; + border-radius: 50%; + object-fit: cover; + } + + .upload-overlay { + position: absolute; + bottom: 0; + right: 0; + width: 28px; + height: 28px; + cursor: pointer; + } + } + + mdui-text-field { + flex: 1; + } + } + + #profile-form { + display: flex; + flex-direction: column; + gap: 16px; + + mdui-text-field { + width: 100%; + } + + .dialog-actions { + display: flex; + gap: 12px; + padding-top: 16px; + border-top: 1px solid $color-dark-outline; + + > * { + flex: 1; + } + } + } +} + +// User profile dialog content styles +#user-profile-dialog .content { + display: flex; + gap: 1.5rem; + + .profile-picture-section { + flex-shrink: 0; + + .profile-picture { + width: 80px; + height: 80px; + border-radius: 50%; + object-fit: cover; + border: 2px solid $color-dark-outline; + } + } + + .profile-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 1rem; + + .username-section { + display: flex; + align-items: center; + gap: 0.75rem; + + .username { + margin: 0; + color: $color-dark-on-surface; + font-size: 1.1rem; + font-weight: 600; + } + + .online-status { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.85rem; + padding: 0.25rem 0.5rem; + border-radius: 12px; + font-weight: 500; + + &.online { + color: $success; + background-color: rgba(76, 175, 80, 0.1); + + .online-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: $success; + } + } + + &.offline { + color: $color-dark-on-surface-variant; + background-color: rgba(255, 255, 255, 0.05); + + .offline-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: $color-dark-on-surface-variant; + } + } + } + } + + .bio-section { + label { + display: block; + color: $color-dark-on-surface-variant; + font-size: 0.85rem; + font-weight: 500; + margin-bottom: 0.5rem; + } + + .bio-display { + color: $color-dark-on-surface; + font-size: 0.9rem; + line-height: 1.4; + padding: 0.75rem; + background-color: $color-dark-surface; + border-radius: 8px; + border: 1px solid $color-dark-outline; + min-height: 60px; + } + + .bio-actions { + display: flex; + gap: 0.5rem; + margin-top: 0.5rem; + } + } + + .profile-stats { + display: flex; + flex-direction: column; + gap: 0.5rem; + + .stat { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0; + + .stat-label { + color: $color-dark-on-surface-variant; + font-size: 0.85rem; + } + + .stat-value { + color: $color-dark-on-surface; + font-size: 0.85rem; + font-weight: 500; + } + } + } + + .profile-actions { + display: flex; + gap: 0.75rem; + margin-top: 0.5rem; + + mdui-button { + flex: 1; + } + } + } +} + +// Cropper Dialog Styles +#cropper-dialog { + .cropper-dialog-content { + display: flex; + flex-direction: column; + gap: 16px; + min-width: 500px; + max-width: 600px; + } + + .cropper-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 16px; + border-bottom: 1px solid $color-dark-outline; + + h3 { + margin: 0; + color: $color-dark-on-surface; + } + } + + .cropper-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 400px; + background: $color-dark-surface-container; + border-radius: 8px; + overflow: hidden; + + #cropper-area { + width: 100%; + height: 100%; + min-height: 400px; + } + } + + .cropper-actions { + display: flex; + gap: 12px; + justify-content: flex-end; + padding-top: 16px; + border-top: 1px solid $color-dark-outline; + } +} + +// Reaction styles +.message-reactions { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 8px; + margin-left: 10px; + margin-right: 10px; + animation: messageReactionsFadeIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.reaction-button { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border: none; + border-radius: 16px; + background-color: $color-dark-surface-container; + cursor: pointer; + transition: transform 0.2s ease, background-color 0.2s ease; + font-size: 1px; + min-height: 28px; + animation: reactionFadeIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + + &.removing { + animation: reactionFadeOut 0.2s ease forwards; + } + + &:hover { + background-color: $color-dark-surface-container-high; + transform: scale(1.05); + } + + &.reacted { + background-color: $color-dark-primary-container; + border-color: $color-dark-primary; + color: $color-dark-on-primary-container; + + &:hover { + background-color: color.adjust($color-dark-primary-container, $lightness: 20%); + } + } +} + +.reaction-emoji { + font-size: 17px; + line-height: 1; +} + +.reaction-count { + font-size: 12px; + font-weight: 500; + line-height: 1; +} + +// Reaction bar styles (standalone) +.reaction-bar { + background: $color-dark-surface-container; + border: 1px solid $color-dark-outline; + border-radius: 24px; + padding: 8px; + opacity: 1; + transition: all 0.15s ease; + backdrop-filter: blur(8px); + transform: translateY(0); + + &.closing { + opacity: 0; + transform: scale(0.8); + } +} + +// Emoji menu wrapper inside reaction bar +.emoji-menu-wrapper { + width: 320px; + height: 400px; + display: flex; + align-items: center; + justify-content: center; +} + +// Context menu wrapper with animations +.context-menu-wrapper { + position: relative; + display: block; + + // Animation states + &.entering { + opacity: 0; + transform: scale(0.8); + animation: contextMenuEnter 0.2s ease forwards; + } + + &.entering-left { + opacity: 0; + transform: translateX(-20px) scale(0.8); + animation: contextMenuEnterLeft 0.2s ease forwards; + } + + &.entering-up { + opacity: 0; + transform: translateY(20px) scale(0.8); + animation: contextMenuEnterUp 0.2s ease forwards; + } + + &.entering-up-left { + opacity: 0; + transform: translateX(-20px) translateY(20px) scale(0.8); + animation: contextMenuEnterUpLeft 0.2s ease forwards; + } + + &.closing { + opacity: 1; + transform: scale(1); + animation: contextMenuClose 0.2s ease forwards; + } + + &.closing-left { + opacity: 1; + transform: translateX(0) scale(1); + animation: contextMenuCloseLeft 0.2s ease forwards; + } + + &.closing-up { + opacity: 1; + transform: translateY(0) scale(1); + animation: contextMenuCloseUp 0.2s ease forwards; + } + + &.closing-up-left { + opacity: 1; + transform: translateX(0) translateY(0) scale(1); + animation: contextMenuCloseUpLeft 0.2s ease forwards; + } +} + +// Reaction bar inside context menu wrapper +.context-menu-reaction-bar { + display: flex; + align-items: center; + gap: 4px; + padding: 8px 12px; + background: $color-dark-surface-container; + border: 1px solid $color-dark-outline; + border-radius: 16px; + position: absolute; + bottom: 100%; + justify-content: center; + margin-bottom: 10px; + transition: width 0.3s ease-out, height 0.3s ease-out; + + &.left { + left: 0; + transform: translateX(0); + } + + &.right { + right: 0; + transform: translateX(0); + } + + &.expanded { + padding: 0; + overflow: hidden; + width: 320px; + height: 400px; + border-radius: 16px; + + // Default: expand downward from the reaction bar's bottom edge + position: absolute; + bottom: auto; + top: 0; + left: 0; + transform: translateY(0); + + &.expand-upward { + // Expand upward from the reaction bar's top edge + bottom: 100%; + top: auto; + margin-bottom: 10px; + margin-top: 0; + transform: translateY(0); + } + + .emoji-menu-wrapper { + animation: emojiMenuEnter 0.5s ease; + } + } +} + +@keyframes emojiMenuEnter { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +.reaction-bar-content { + display: flex; + align-items: center; + gap: 4px; + transition: opacity 0.3s ease-out; + + &.faded { + opacity: 0; + } +} + +.reaction-emoji-button { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: none; + border-radius: 16px; + background: transparent; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); + font-size: 18px; + + &:hover { + background: var(--mdui-color-surface-container-high); + transform: scale(1.3); + box-shadow: var(--mdui-elevation-1); + } + + &:active { + transform: scale(0.95); + transition: transform 0.1s ease; + } +} + +.reaction-expand-button { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: 1px solid var(--mdui-color-outline); + border-radius: 16px; + background: var(--mdui-color-surface); + cursor: pointer; + transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); + + &:hover { + background: var(--mdui-color-surface-container-high); + border-color: var(--mdui-color-primary); + transform: scale(1.1); + box-shadow: var(--mdui-elevation-1); + } + + &:active { + transform: scale(0.95); + transition: transform 0.1s ease; + } + + .material-symbols { + font-size: 18px; + color: var(--mdui-color-on-surface); + transition: transform 0.2s ease; + } + + &:hover .material-symbols { + transform: rotate(90deg); + } +} + +// Animation for reactions appearing/disappearing +@keyframes messageReactionsFadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes reactionFadeIn { + from { + opacity: 0; + transform: scale(0.8); + } + + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes reactionFadeOut { + from { + opacity: 1; + transform: scale(1); + } + + to { + opacity: 0; + transform: scale(0.8); + } +} + +// Context menu wrapper animations +@keyframes contextMenuEnter { + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes contextMenuEnterLeft { + to { + opacity: 1; + transform: translateX(0) scale(1); + } +} + +@keyframes contextMenuEnterUp { + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes contextMenuEnterUpLeft { + to { + opacity: 1; + transform: translateX(0) translateY(0) scale(1); + } +} + +@keyframes contextMenuClose { + to { + opacity: 0; + transform: scale(0.8); + } +} + +@keyframes contextMenuCloseLeft { + to { + opacity: 0; + transform: translateX(-20px) scale(0.8); + } +} + +@keyframes contextMenuCloseUp { + to { + opacity: 0; + transform: translateY(20px) scale(0.8); + } +} + +@keyframes contextMenuCloseUpLeft { + to { + opacity: 0; + transform: translateX(-20px) translateY(20px) scale(0.8); + } +} + +// Mobile responsive +@media (max-width: 768px) { + .reaction-bar { + padding: 6px; + } + + .reaction-emoji-button, + .reaction-expand-button { + width: 28px; + height: 28px; + } + + .reaction-emoji-button { + font-size: 16px; + } + + .reaction-expand-button .material-symbols { + font-size: 16px; + } +} + +// Settings styles +#settings-dialog { + .fullscreen-wrapper { + display: flex; + align-items: center; + justify-content: center; + height: calc(100vh - (1.5rem * 2)); + width: 100%; + position: relative; + + #settings-dialog-inner { + max-width: 1200px; + max-height: 1000px; + width: 100%; + height: 100%; + + .header { + display: flex; + flex-direction: row; + gap: 10px; + margin-bottom: 16px; + } + + #settings-menu { + display: flex; + flex-direction: row; + gap: 16px; + + mdui-list { + max-width: 280px; + padding-right: 16px; + overflow-y: auto; + } + + .screen { + flex: 1; + overflow-y: auto; + position: relative; + + .settings-panel { + display: flex; + flex-direction: column; + gap: 16px; + opacity: 0; + visibility: hidden; + transform: translateY(20px); + transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s ease; + position: absolute; + top: 0; + left: 0; + width: 100%; + + &.active { + opacity: 1; + visibility: visible; + transform: translateY(0); + position: relative; + } + + h3 { + margin: 0 0 16px 0; + color: $color-dark-on-surface; + } + + mdui-text-field, + mdui-select, + mdui-switch, + mdui-button { + margin-bottom: 8px; + } + + mdui-switch { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 0; + border-bottom: 1px solid $color-dark-outline; + + &:last-child { + border-bottom: none; + } + } + + p { + margin: 8px 0; + color: $color-dark-on-surface-variant; + } + + mdui-linear-progress { + margin: 16px 0; + } + } + } + } + } + } +} + +// Reply dialog styles +.reply-dialog .dialog-content { + width: 300px; + overflow-x:hidden; + + .reply-preview-dialog { + margin-bottom: 1rem; + padding: 16px; + background-color: $color-dark-surface-container; + border-radius: 16px; + + .reply-content { + display: flex; + flex-direction: column; + gap: 0.25rem; + + .reply-username { + font-weight: 600; + color: $color-dark-on-surface; + font-size: 0.85rem; + } + + .reply-text { + color: $color-dark-on-surface-variant; + font-size: 0.9rem; + line-height: 1.4; + } + } + } +} diff --git a/frontend/src/pages/app/core/config.ts b/frontend/src/pages/chat/core/config.ts similarity index 100% rename from frontend/src/pages/app/core/config.ts rename to frontend/src/pages/chat/core/config.ts diff --git a/frontend/src/pages/app/core/init.ts b/frontend/src/pages/chat/core/init.ts similarity index 100% rename from frontend/src/pages/app/core/init.ts rename to frontend/src/pages/chat/core/init.ts diff --git a/frontend/src/pages/app/core/types.d.ts b/frontend/src/pages/chat/core/types.d.ts similarity index 100% rename from frontend/src/pages/app/core/types.d.ts rename to frontend/src/pages/chat/core/types.d.ts diff --git a/frontend/src/pages/app/core/websocket.ts b/frontend/src/pages/chat/core/websocket.ts similarity index 100% rename from frontend/src/pages/app/core/websocket.ts rename to frontend/src/pages/chat/core/websocket.ts diff --git a/frontend/src/pages/app/resources/css/_electron.scss b/frontend/src/pages/chat/electron/electron.scss similarity index 94% rename from frontend/src/pages/app/resources/css/_electron.scss rename to frontend/src/pages/chat/electron/electron.scss index 69aa4ed..3d5f8e9 100644 --- a/frontend/src/pages/app/resources/css/_electron.scss +++ b/frontend/src/pages/chat/electron/electron.scss @@ -1,4 +1,4 @@ -@use "common/material" as *; +@use "../../../css/common/material" as *; #electron-title-bar { display: none; diff --git a/frontend/src/pages/app/electron/electron.ts b/frontend/src/pages/chat/electron/electron.ts similarity index 94% rename from frontend/src/pages/app/electron/electron.ts rename to frontend/src/pages/chat/electron/electron.ts index 0eb9ab1..c69cce2 100644 --- a/frontend/src/pages/app/electron/electron.ts +++ b/frontend/src/pages/chat/electron/electron.ts @@ -5,6 +5,8 @@ * @version 1.0.0 */ +import "./electron.scss"; + export const isElectron = import.meta.env.VITE_ELECTRON && window.electronInterface != undefined; if (isElectron) { diff --git a/frontend/src/pages/app/service-worker/service-worker.ts b/frontend/src/pages/chat/service-worker/service-worker.ts similarity index 100% rename from frontend/src/pages/app/service-worker/service-worker.ts rename to frontend/src/pages/chat/service-worker/service-worker.ts diff --git a/frontend/src/pages/app/ui/components/Alerts.tsx b/frontend/src/pages/chat/ui/components/Alerts.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/Alerts.tsx rename to frontend/src/pages/chat/ui/components/Alerts.tsx diff --git a/frontend/src/pages/app/ui/components/Auth.tsx b/frontend/src/pages/chat/ui/components/Auth.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/Auth.tsx rename to frontend/src/pages/chat/ui/components/Auth.tsx diff --git a/frontend/src/pages/app/ui/components/Electron.tsx b/frontend/src/pages/chat/ui/components/Electron.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/Electron.tsx rename to frontend/src/pages/chat/ui/components/Electron.tsx diff --git a/frontend/src/pages/app/ui/components/chat/BottomAppBar.tsx b/frontend/src/pages/chat/ui/components/chat/BottomAppBar.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/chat/BottomAppBar.tsx rename to frontend/src/pages/chat/ui/components/chat/BottomAppBar.tsx diff --git a/frontend/src/pages/app/ui/components/chat/ChatHeader.tsx b/frontend/src/pages/chat/ui/components/chat/ChatHeader.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/chat/ChatHeader.tsx rename to frontend/src/pages/chat/ui/components/chat/ChatHeader.tsx diff --git a/frontend/src/pages/app/ui/components/chat/ChatInputWrapper.tsx b/frontend/src/pages/chat/ui/components/chat/ChatInputWrapper.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/chat/ChatInputWrapper.tsx rename to frontend/src/pages/chat/ui/components/chat/ChatInputWrapper.tsx diff --git a/frontend/src/pages/app/ui/components/chat/ChatMainHeader.tsx b/frontend/src/pages/chat/ui/components/chat/ChatMainHeader.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/chat/ChatMainHeader.tsx rename to frontend/src/pages/chat/ui/components/chat/ChatMainHeader.tsx diff --git a/frontend/src/pages/app/ui/components/chat/ChatMessages.tsx b/frontend/src/pages/chat/ui/components/chat/ChatMessages.tsx similarity index 99% rename from frontend/src/pages/app/ui/components/chat/ChatMessages.tsx rename to frontend/src/pages/chat/ui/components/chat/ChatMessages.tsx index efdf97c..43923a9 100644 --- a/frontend/src/pages/app/ui/components/chat/ChatMessages.tsx +++ b/frontend/src/pages/chat/ui/components/chat/ChatMessages.tsx @@ -4,7 +4,7 @@ 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 "../../../../../api/profileApi"; import { useEffect, useState, type ReactNode } from "react"; import { delay } from "../../../utils/utils"; import { MaterialDialog } from "../core/Dialog"; diff --git a/frontend/src/pages/app/ui/components/chat/ChatTabs.tsx b/frontend/src/pages/chat/ui/components/chat/ChatTabs.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/chat/ChatTabs.tsx rename to frontend/src/pages/chat/ui/components/chat/ChatTabs.tsx diff --git a/frontend/src/pages/app/ui/components/chat/DMUsersList.tsx b/frontend/src/pages/chat/ui/components/chat/DMUsersList.tsx similarity index 96% rename from frontend/src/pages/app/ui/components/chat/DMUsersList.tsx rename to frontend/src/pages/chat/ui/components/chat/DMUsersList.tsx index c09bc25..7d20b78 100644 --- a/frontend/src/pages/app/ui/components/chat/DMUsersList.tsx +++ b/frontend/src/pages/chat/ui/components/chat/DMUsersList.tsx @@ -1,8 +1,8 @@ import { useEffect } from "react"; import { useDM, type DMUser } from "../../hooks/useDM"; import { useAppState } from "../../state"; -import { fetchUserPublicKey } from "../../../api/dmApi"; -import defaultAvatar from "../../../resources/images/default-avatar.png"; +import { fetchUserPublicKey } from "../../../../../api/dmApi"; +import defaultAvatar from "../../../../../images/default-avatar.png"; export function DMUsersList() { const { dmUsers, isLoadingUsers, loadUsers } = useDM(); diff --git a/frontend/src/pages/app/ui/components/chat/EmojiMenu.tsx b/frontend/src/pages/chat/ui/components/chat/EmojiMenu.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/chat/EmojiMenu.tsx rename to frontend/src/pages/chat/ui/components/chat/EmojiMenu.tsx diff --git a/frontend/src/pages/app/ui/components/chat/LeftPanel.tsx b/frontend/src/pages/chat/ui/components/chat/LeftPanel.tsx similarity index 98% rename from frontend/src/pages/app/ui/components/chat/LeftPanel.tsx rename to frontend/src/pages/chat/ui/components/chat/LeftPanel.tsx index 1377632..6c30c39 100644 --- a/frontend/src/pages/app/ui/components/chat/LeftPanel.tsx +++ b/frontend/src/pages/chat/ui/components/chat/LeftPanel.tsx @@ -1,6 +1,6 @@ import { PRODUCT_NAME } from "../../../core/config"; import { useAppState } from "../../state"; -import defaultAvatar from "../../../resources/images/default-avatar.png"; +import defaultAvatar from "../../../../../images/default-avatar.png"; import { useState, type FormEvent } from "react"; import { ProfileDialog } from "../profile/ProfileDialog"; import { SettingsDialog } from "../settings/SettingsDialog"; diff --git a/frontend/src/pages/app/ui/components/chat/Message.tsx b/frontend/src/pages/chat/ui/components/chat/Message.tsx similarity index 99% rename from frontend/src/pages/app/ui/components/chat/Message.tsx rename to frontend/src/pages/chat/ui/components/chat/Message.tsx index 5e88677..1894390 100644 --- a/frontend/src/pages/app/ui/components/chat/Message.tsx +++ b/frontend/src/pages/chat/ui/components/chat/Message.tsx @@ -1,14 +1,14 @@ import { formatTime, id } from "../../../utils/utils"; import type { Attachment, Message as MessageType } from "../../../core/types"; -import defaultAvatar from "../../../resources/images/default-avatar.png"; +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 "../../../auth/crypto"; +import { getCurrentKeys } from "../../../../../api/authApi"; import { ecdhSharedSecret, deriveWrappingKey } from "../../../utils/crypto/asymmetric"; import { importAesGcmKey, aesGcmDecrypt } from "../../../utils/crypto/symmetric"; -import { getAuthHeaders } from "../../../auth/api"; +import { getAuthHeaders } from "../../../../../api/authApi"; import { useAppState } from "../../state"; import { ub64 } from "../../../utils/utils"; import { useImmer } from "use-immer"; diff --git a/frontend/src/pages/app/ui/components/chat/MessageContextMenu.tsx b/frontend/src/pages/chat/ui/components/chat/MessageContextMenu.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/chat/MessageContextMenu.tsx rename to frontend/src/pages/chat/ui/components/chat/MessageContextMenu.tsx diff --git a/frontend/src/pages/app/ui/components/chat/MessagePanelRenderer.tsx b/frontend/src/pages/chat/ui/components/chat/MessagePanelRenderer.tsx similarity index 99% rename from frontend/src/pages/app/ui/components/chat/MessagePanelRenderer.tsx rename to frontend/src/pages/chat/ui/components/chat/MessagePanelRenderer.tsx index da64a2f..96795af 100644 --- a/frontend/src/pages/app/ui/components/chat/MessagePanelRenderer.tsx +++ b/frontend/src/pages/chat/ui/components/chat/MessagePanelRenderer.tsx @@ -5,7 +5,7 @@ import { ChatMessages } from "./ChatMessages"; import { ChatInputWrapper } from "./ChatInputWrapper"; import { setGlobalMessageHandler } from "../../../core/websocket"; import type { Message, WebSocketMessage } from "../../../core/types"; -import defaultAvatar from "../../../resources/images/default-avatar.png"; +import defaultAvatar from "../../../../../images/default-avatar.png"; import AnimatedOpacity from "../core/animations/AnimatedOpacity"; import type { DMPanel } from "../../panels/DMPanel"; diff --git a/frontend/src/pages/app/ui/components/chat/MessageReactions.tsx b/frontend/src/pages/chat/ui/components/chat/MessageReactions.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/chat/MessageReactions.tsx rename to frontend/src/pages/chat/ui/components/chat/MessageReactions.tsx diff --git a/frontend/src/pages/app/ui/components/chat/ReplyMessageDialog.tsx b/frontend/src/pages/chat/ui/components/chat/ReplyMessageDialog.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/chat/ReplyMessageDialog.tsx rename to frontend/src/pages/chat/ui/components/chat/ReplyMessageDialog.tsx diff --git a/frontend/src/pages/app/ui/components/chat/RightPanel.tsx b/frontend/src/pages/chat/ui/components/chat/RightPanel.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/chat/RightPanel.tsx rename to frontend/src/pages/chat/ui/components/chat/RightPanel.tsx diff --git a/frontend/src/pages/app/ui/components/chat/UserProfileDialog.tsx b/frontend/src/pages/chat/ui/components/chat/UserProfileDialog.tsx similarity index 97% rename from frontend/src/pages/app/ui/components/chat/UserProfileDialog.tsx rename to frontend/src/pages/chat/ui/components/chat/UserProfileDialog.tsx index 1f3b8eb..ee77e28 100644 --- a/frontend/src/pages/app/ui/components/chat/UserProfileDialog.tsx +++ b/frontend/src/pages/chat/ui/components/chat/UserProfileDialog.tsx @@ -2,7 +2,7 @@ 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 "../../../resources/images/default-avatar.png"; +import defaultAvatar from "../../../../../images/default-avatar.png"; interface UserProfileDialogProps extends DialogProps { userProfile: UserProfile | null; diff --git a/frontend/src/pages/app/ui/components/chat/emojiData.ts b/frontend/src/pages/chat/ui/components/chat/emojiData.ts similarity index 100% rename from frontend/src/pages/app/ui/components/chat/emojiData.ts rename to frontend/src/pages/chat/ui/components/chat/emojiData.ts diff --git a/frontend/src/pages/app/ui/components/core/Dialog.tsx b/frontend/src/pages/chat/ui/components/core/Dialog.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/core/Dialog.tsx rename to frontend/src/pages/chat/ui/components/core/Dialog.tsx diff --git a/frontend/src/pages/app/ui/components/core/Quote.tsx b/frontend/src/pages/chat/ui/components/core/Quote.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/core/Quote.tsx rename to frontend/src/pages/chat/ui/components/core/Quote.tsx diff --git a/frontend/src/pages/app/ui/components/core/RichTextArea.tsx b/frontend/src/pages/chat/ui/components/core/RichTextArea.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/core/RichTextArea.tsx rename to frontend/src/pages/chat/ui/components/core/RichTextArea.tsx diff --git a/frontend/src/pages/app/ui/components/core/TextField.tsx b/frontend/src/pages/chat/ui/components/core/TextField.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/core/TextField.tsx rename to frontend/src/pages/chat/ui/components/core/TextField.tsx diff --git a/frontend/src/pages/app/ui/components/core/animations/AnimatedHeight.tsx b/frontend/src/pages/chat/ui/components/core/animations/AnimatedHeight.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/core/animations/AnimatedHeight.tsx rename to frontend/src/pages/chat/ui/components/core/animations/AnimatedHeight.tsx diff --git a/frontend/src/pages/app/ui/components/core/animations/AnimatedOpacity.tsx b/frontend/src/pages/chat/ui/components/core/animations/AnimatedOpacity.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/core/animations/AnimatedOpacity.tsx rename to frontend/src/pages/chat/ui/components/core/animations/AnimatedOpacity.tsx diff --git a/frontend/src/pages/app/ui/components/core/animations/types.d.ts b/frontend/src/pages/chat/ui/components/core/animations/types.d.ts similarity index 100% rename from frontend/src/pages/app/ui/components/core/animations/types.d.ts rename to frontend/src/pages/chat/ui/components/core/animations/types.d.ts diff --git a/frontend/src/pages/app/ui/components/profile/CropperDialog.tsx b/frontend/src/pages/chat/ui/components/profile/CropperDialog.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/profile/CropperDialog.tsx rename to frontend/src/pages/chat/ui/components/profile/CropperDialog.tsx diff --git a/frontend/src/pages/app/ui/components/profile/ImageCropper.tsx b/frontend/src/pages/chat/ui/components/profile/ImageCropper.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/profile/ImageCropper.tsx rename to frontend/src/pages/chat/ui/components/profile/ImageCropper.tsx diff --git a/frontend/src/pages/app/ui/components/profile/ProfileDialog.tsx b/frontend/src/pages/chat/ui/components/profile/ProfileDialog.tsx similarity index 99% rename from frontend/src/pages/app/ui/components/profile/ProfileDialog.tsx rename to frontend/src/pages/chat/ui/components/profile/ProfileDialog.tsx index 1bb65ce..8a2b359 100644 --- a/frontend/src/pages/app/ui/components/profile/ProfileDialog.tsx +++ b/frontend/src/pages/chat/ui/components/profile/ProfileDialog.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useRef, type FormEvent } from "react"; -import defaultAvatar from "../../../resources/images/default-avatar.png"; +import defaultAvatar from "../../../../../images/default-avatar.png"; import type { TextField } from "mdui/components/text-field"; import type { DialogProps } from "../../../core/types"; import { MaterialDialog } from "../core/Dialog"; diff --git a/frontend/src/pages/app/ui/components/settings/SettingsDialog.tsx b/frontend/src/pages/chat/ui/components/settings/SettingsDialog.tsx similarity index 99% rename from frontend/src/pages/app/ui/components/settings/SettingsDialog.tsx rename to frontend/src/pages/chat/ui/components/settings/SettingsDialog.tsx index 14b1ad5..9a1ead0 100644 --- a/frontend/src/pages/app/ui/components/settings/SettingsDialog.tsx +++ b/frontend/src/pages/chat/ui/components/settings/SettingsDialog.tsx @@ -6,7 +6,7 @@ import { initialize, isSupported, startElectronReceiver, stopElectronReceiver, s import { isElectron } from "../../../electron/electron"; import { useAppState } from "../../state"; import type { Switch } from "mdui/components/switch"; -import { getAuthHeaders } from "../../../auth/api"; +import { getAuthHeaders } from "../../../../../api/authApi"; export function SettingsDialog({ isOpen, onOpenChange }: DialogProps) { const [activePanel, setActivePanel] = useState("notifications-settings"); diff --git a/frontend/src/pages/app/ui/hooks/useCombinedRefs.ts b/frontend/src/pages/chat/ui/hooks/useCombinedRefs.ts similarity index 100% rename from frontend/src/pages/app/ui/hooks/useCombinedRefs.ts rename to frontend/src/pages/chat/ui/hooks/useCombinedRefs.ts diff --git a/frontend/src/pages/app/ui/hooks/useDM.ts b/frontend/src/pages/chat/ui/hooks/useDM.ts similarity index 99% rename from frontend/src/pages/app/ui/hooks/useDM.ts rename to frontend/src/pages/chat/ui/hooks/useDM.ts index 9e55858..53ae1bd 100644 --- a/frontend/src/pages/app/ui/hooks/useDM.ts +++ b/frontend/src/pages/chat/ui/hooks/useDM.ts @@ -6,7 +6,7 @@ import { fetchDMHistory, decryptDm, sendDMViaWebSocket -} from "../../api/dmApi"; +} from "../../../../api/dmApi"; import type { User, Message, DmEncryptedJSON } from "../../core/types"; import { websocket } from "../../core/websocket"; diff --git a/frontend/src/pages/app/ui/hooks/useProfile.ts b/frontend/src/pages/chat/ui/hooks/useProfile.ts similarity index 98% rename from frontend/src/pages/app/ui/hooks/useProfile.ts rename to frontend/src/pages/chat/ui/hooks/useProfile.ts index e256f78..9cad0a2 100644 --- a/frontend/src/pages/app/ui/hooks/useProfile.ts +++ b/frontend/src/pages/chat/ui/hooks/useProfile.ts @@ -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 "../../../../api/profileApi"; import { showSuccess, showError } from "../../utils/notification"; export default function useProfile() { diff --git a/frontend/src/pages/app/ui/hooks/useWindowSize.ts b/frontend/src/pages/chat/ui/hooks/useWindowSize.ts similarity index 100% rename from frontend/src/pages/app/ui/hooks/useWindowSize.ts rename to frontend/src/pages/chat/ui/hooks/useWindowSize.ts diff --git a/frontend/src/pages/app/ui/panels/DMPanel.ts b/frontend/src/pages/chat/ui/panels/DMPanel.ts similarity index 99% rename from frontend/src/pages/app/ui/panels/DMPanel.ts rename to frontend/src/pages/chat/ui/panels/DMPanel.ts index b528138..eb6e8c0 100644 --- a/frontend/src/pages/app/ui/panels/DMPanel.ts +++ b/frontend/src/pages/chat/ui/panels/DMPanel.ts @@ -6,7 +6,7 @@ import { sendDmWithFiles, editDmEnvelope, deleteDmEnvelope -} from "../../api/dmApi"; +} from "../../../../api/dmApi"; import type { DmEncryptedJSON, DmEnvelope, DMWebSocketMessage, EncryptedMessageJson, Message } from "../../core/types"; import type { UserState } from "../state"; diff --git a/frontend/src/pages/app/ui/panels/MessagePanel.ts b/frontend/src/pages/chat/ui/panels/MessagePanel.ts similarity index 100% rename from frontend/src/pages/app/ui/panels/MessagePanel.ts rename to frontend/src/pages/chat/ui/panels/MessagePanel.ts diff --git a/frontend/src/pages/app/ui/panels/PublicChatPanel.ts b/frontend/src/pages/chat/ui/panels/PublicChatPanel.ts similarity index 99% rename from frontend/src/pages/app/ui/panels/PublicChatPanel.ts rename to frontend/src/pages/chat/ui/panels/PublicChatPanel.ts index 9d3d29a..38372cf 100644 --- a/frontend/src/pages/app/ui/panels/PublicChatPanel.ts +++ b/frontend/src/pages/chat/ui/panels/PublicChatPanel.ts @@ -1,6 +1,6 @@ import { MessagePanel } from "./MessagePanel"; import { API_BASE_URL } from "../../core/config"; -import { getAuthHeaders } from "../../auth/api"; +import { getAuthHeaders } from "../../../../api/authApi"; import { request } from "../../core/websocket"; import type { ChatWebSocketMessage, Message, SendMessageRequest, ReactionUpdateWebSocketMessage } from "../../core/types"; import type { UserState } from "../state"; diff --git a/frontend/src/pages/app/ui/screen/ChatScreen.tsx b/frontend/src/pages/chat/ui/screen/ChatScreen.tsx similarity index 100% rename from frontend/src/pages/app/ui/screen/ChatScreen.tsx rename to frontend/src/pages/chat/ui/screen/ChatScreen.tsx diff --git a/frontend/src/pages/app/ui/screen/DownloadAppScreen.tsx b/frontend/src/pages/chat/ui/screen/DownloadAppScreen.tsx similarity index 100% rename from frontend/src/pages/app/ui/screen/DownloadAppScreen.tsx rename to frontend/src/pages/chat/ui/screen/DownloadAppScreen.tsx diff --git a/frontend/src/pages/app/ui/screen/LoginScreen.tsx b/frontend/src/pages/chat/ui/screen/LoginScreen.tsx similarity index 99% rename from frontend/src/pages/app/ui/screen/LoginScreen.tsx rename to frontend/src/pages/chat/ui/screen/LoginScreen.tsx index 168b0d8..c6e5656 100644 --- a/frontend/src/pages/app/ui/screen/LoginScreen.tsx +++ b/frontend/src/pages/chat/ui/screen/LoginScreen.tsx @@ -2,7 +2,7 @@ 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 "../../auth/crypto"; +import { ensureKeysOnLogin } from "../../../../api/authApi"; import { API_BASE_URL } from "../../core/config"; import { useRef } from "react"; import type { TextField } from "mdui/components/text-field"; diff --git a/frontend/src/pages/app/ui/screen/RegisterScreen.tsx b/frontend/src/pages/chat/ui/screen/RegisterScreen.tsx similarity index 99% rename from frontend/src/pages/app/ui/screen/RegisterScreen.tsx rename to frontend/src/pages/chat/ui/screen/RegisterScreen.tsx index ec16814..9fd2a6a 100644 --- a/frontend/src/pages/app/ui/screen/RegisterScreen.tsx +++ b/frontend/src/pages/chat/ui/screen/RegisterScreen.tsx @@ -9,7 +9,7 @@ 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 "../../auth/crypto"; +import { ensureKeysOnLogin } from "../../../../api/authApi"; export default function RegisterScreen() { const [alerts, updateAlerts] = useImmer([]); diff --git a/frontend/src/pages/app/ui/state.ts b/frontend/src/pages/chat/ui/state.ts similarity index 99% rename from frontend/src/pages/app/ui/state.ts rename to frontend/src/pages/chat/ui/state.ts index 14899e0..c9b9e79 100644 --- a/frontend/src/pages/app/ui/state.ts +++ b/frontend/src/pages/chat/ui/state.ts @@ -4,8 +4,8 @@ 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 "../auth/api"; -import { restoreKeys } from "../auth/crypto"; +import { getAuthHeaders } from "../../../api/authApi"; +import { restoreKeys } from "../../../api/authApi"; import { API_BASE_URL } from "../core/config"; import { initialize, subscribe, startElectronReceiver, isSupported } from "../utils/push-notifications"; import { isElectron } from "../electron/electron"; diff --git a/frontend/src/pages/app/utils/crypto/asymmetric.ts b/frontend/src/pages/chat/utils/crypto/asymmetric.ts similarity index 100% rename from frontend/src/pages/app/utils/crypto/asymmetric.ts rename to frontend/src/pages/chat/utils/crypto/asymmetric.ts diff --git a/frontend/src/pages/app/utils/crypto/backup.ts b/frontend/src/pages/chat/utils/crypto/backup.ts similarity index 100% rename from frontend/src/pages/app/utils/crypto/backup.ts rename to frontend/src/pages/chat/utils/crypto/backup.ts diff --git a/frontend/src/pages/app/utils/crypto/kdf.ts b/frontend/src/pages/chat/utils/crypto/kdf.ts similarity index 100% rename from frontend/src/pages/app/utils/crypto/kdf.ts rename to frontend/src/pages/chat/utils/crypto/kdf.ts diff --git a/frontend/src/pages/app/utils/crypto/symmetric.ts b/frontend/src/pages/chat/utils/crypto/symmetric.ts similarity index 100% rename from frontend/src/pages/app/utils/crypto/symmetric.ts rename to frontend/src/pages/chat/utils/crypto/symmetric.ts diff --git a/frontend/src/pages/app/utils/material.ts b/frontend/src/pages/chat/utils/material.ts similarity index 100% rename from frontend/src/pages/app/utils/material.ts rename to frontend/src/pages/chat/utils/material.ts diff --git a/frontend/src/pages/app/utils/notification.ts b/frontend/src/pages/chat/utils/notification.ts similarity index 100% rename from frontend/src/pages/app/utils/notification.ts rename to frontend/src/pages/chat/utils/notification.ts diff --git a/frontend/src/pages/app/utils/push-notifications.ts b/frontend/src/pages/chat/utils/push-notifications.ts similarity index 100% rename from frontend/src/pages/app/utils/push-notifications.ts rename to frontend/src/pages/chat/utils/push-notifications.ts diff --git a/frontend/src/pages/app/utils/utils.ts b/frontend/src/pages/chat/utils/utils.ts similarity index 100% rename from frontend/src/pages/app/utils/utils.ts rename to frontend/src/pages/chat/utils/utils.ts diff --git a/frontend/src/pages/DownloadAppPage.tsx b/frontend/src/pages/download-app/DownloadAppPage.tsx similarity index 97% rename from frontend/src/pages/DownloadAppPage.tsx rename to frontend/src/pages/download-app/DownloadAppPage.tsx index 3e8ec2e..95f2cd3 100644 --- a/frontend/src/pages/DownloadAppPage.tsx +++ b/frontend/src/pages/download-app/DownloadAppPage.tsx @@ -1,3 +1,5 @@ +import "./download-app.scss"; + export default function DownloadAppPage() { return (
diff --git a/frontend/src/pages/app/resources/css/_download-app.scss b/frontend/src/pages/download-app/download-app.scss similarity index 100% rename from frontend/src/pages/app/resources/css/_download-app.scss rename to frontend/src/pages/download-app/download-app.scss diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/home/HomePage.tsx similarity index 99% rename from frontend/src/pages/HomePage.tsx rename to frontend/src/pages/home/HomePage.tsx index 76632df..19578ba 100644 --- a/frontend/src/pages/HomePage.tsx +++ b/frontend/src/pages/home/HomePage.tsx @@ -1,6 +1,7 @@ import { Navigate, useNavigate } from "react-router-dom"; -import { useAppState } from "./app/ui/state"; -import { isElectron } from "./app/electron/electron"; +import { useAppState } from "../chat/ui/state"; +import { isElectron } from "../chat/electron/electron"; +import "./home.scss"; function GitHubLink({ children }: { children: React.ReactNode }) { return ( diff --git a/frontend/src/pages/app/resources/css/_homepage.scss b/frontend/src/pages/home/home.scss similarity index 99% rename from frontend/src/pages/app/resources/css/_homepage.scss rename to frontend/src/pages/home/home.scss index 15aee92..1dcb789 100644 --- a/frontend/src/pages/app/resources/css/_homepage.scss +++ b/frontend/src/pages/home/home.scss @@ -1,4 +1,4 @@ -@use "common/material" as *; +@use "../../css/common/material" as *; .homepage { min-height: 100vh; diff --git a/frontend/src/pages/NotFoundPage.tsx b/frontend/src/pages/not-found/NotFoundPage.tsx similarity index 98% rename from frontend/src/pages/NotFoundPage.tsx rename to frontend/src/pages/not-found/NotFoundPage.tsx index 907119f..1b06839 100644 --- a/frontend/src/pages/NotFoundPage.tsx +++ b/frontend/src/pages/not-found/NotFoundPage.tsx @@ -1,4 +1,5 @@ import { useNavigate } from "react-router-dom"; +import "./not-found.scss"; export default function NotFoundPage() { const navigate = useNavigate(); diff --git a/frontend/src/pages/app/resources/css/_404.scss b/frontend/src/pages/not-found/not-found.scss similarity index 100% rename from frontend/src/pages/app/resources/css/_404.scss rename to frontend/src/pages/not-found/not-found.scss From 4d0e96946169fdd4442fe378924d1c3d2fa19576 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Wed, 8 Oct 2025 18:41:47 +0300 Subject: [PATCH 02/10] Modularize chat styles --- frontend/src/pages/chat/ChatPage.tsx | 2 +- frontend/src/pages/chat/chat.scss | 1896 ----------------- frontend/src/pages/chat/css/_animations.scss | 107 + frontend/src/pages/chat/css/_chat-input.scss | 318 +++ .../src/pages/chat/css/_context-menu.scss | 292 +++ frontend/src/pages/chat/css/_layout.scss | 49 + frontend/src/pages/chat/css/_left-panel.scss | 234 ++ .../pages/chat/css/_message-reactions.scss | 59 + frontend/src/pages/chat/css/_message.scss | 352 +++ .../src/pages/chat/css/_profile-dialog.scss | 251 +++ frontend/src/pages/chat/css/_right-panel.scss | 131 ++ .../src/pages/chat/css/_settings-dialog.scss | 101 + frontend/src/pages/chat/css/chat.scss | 11 + .../ui/components/chat/MessageContextMenu.tsx | 2 +- 14 files changed, 1907 insertions(+), 1898 deletions(-) delete mode 100644 frontend/src/pages/chat/chat.scss create mode 100644 frontend/src/pages/chat/css/_animations.scss create mode 100644 frontend/src/pages/chat/css/_chat-input.scss create mode 100644 frontend/src/pages/chat/css/_context-menu.scss create mode 100644 frontend/src/pages/chat/css/_layout.scss create mode 100644 frontend/src/pages/chat/css/_left-panel.scss create mode 100644 frontend/src/pages/chat/css/_message-reactions.scss create mode 100644 frontend/src/pages/chat/css/_message.scss create mode 100644 frontend/src/pages/chat/css/_profile-dialog.scss create mode 100644 frontend/src/pages/chat/css/_right-panel.scss create mode 100644 frontend/src/pages/chat/css/_settings-dialog.scss create mode 100644 frontend/src/pages/chat/css/chat.scss diff --git a/frontend/src/pages/chat/ChatPage.tsx b/frontend/src/pages/chat/ChatPage.tsx index bbf547d..19cb5a2 100644 --- a/frontend/src/pages/chat/ChatPage.tsx +++ b/frontend/src/pages/chat/ChatPage.tsx @@ -1,6 +1,6 @@ import { LeftPanel } from "./ui/components/chat/LeftPanel"; import { RightPanel } from "./ui/components/chat/RightPanel"; -import "./chat.scss"; +import "./css/chat.scss"; export default function ChatPage() { return ( diff --git a/frontend/src/pages/chat/chat.scss b/frontend/src/pages/chat/chat.scss deleted file mode 100644 index f17af69..0000000 --- a/frontend/src/pages/chat/chat.scss +++ /dev/null @@ -1,1896 +0,0 @@ -@use "../../css/common/colors" as *; -@use "../../css/common/material" as *; -@use "sass:color"; - -#chat-interface { - height: 100%; - background: linear-gradient(135deg, $color-dark-background 0%, $color-dark-surface-container 70%, rgba($color-dark-primary-container, 0.3) 100%); - position: relative; - - &::before { - content: ''; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: - radial-gradient(circle at 20% 80%, rgba($color-dark-primary, 0.15) 0%, transparent 50%), - radial-gradient(circle at 80% 20%, rgba($color-dark-tertiary, 0.15) 0%, transparent 50%), - radial-gradient(circle at 40% 40%, rgba($color-dark-secondary, 0.1) 0%, transparent 50%); - pointer-events: none; - z-index: 0; - } - - .header { - display: flex; - background-color: $color-dark-surface-container; - color: white; - padding: 16px 16px; - justify-content: end; - width: fit-content; - z-index: 1000; - position: absolute; - top: 0; - right: 0; - - .header-content { - display: flex; - justify-content: space-between; - align-items: center; - - .logo { - font-size: 1.8rem; - font-weight: 700; - display: flex; - align-items: center; - } - - #logouts { - display: none; - list-style: none; - gap: 10px; - - li { - a { - color: white; - text-decoration: none; - font-weight: 500; - transition: all 0.3s ease; - padding: 10px; - border-radius: 10px; - display: flex; - flex-direction: row; - align-items: center; - gap: 10px; - - &:hover { - background-color: rgba(255, 255, 255, 0.2); - } - } - } - } - } - } - - .chat-container { - display: flex; - width: 100%; - flex-direction: column; - overflow: hidden; - - .chat-main { - flex-grow: 1; - display: flex; - flex-direction: column; - height: 100%; - position: relative; - overflow: hidden; - - .chat-header { - padding: 16px; - background: rgba($color-dark-surface-container, 0.8); - backdrop-filter: blur(20px); - display: flex; - align-items: center; - box-shadow: 0 4px 20px rgba($color-dark-primary, 0.1); - position: relative; - z-index: 1; - - .chat-header-avatar { - width: 45px; - height: 45px; - border-radius: 20%; - object-fit: cover; - margin-right: 1rem; - } - - .chat-header-info { - display: flex; - - .info-chat { - display: flex; - flex-direction: column; - - h4 { - font-size: 1.1rem; - margin: 0 0 0.2rem; - } - - p { - margin: 0; - font-size: 0.8rem; - color: #718096; - } - } - - .online-status { - display: inline-block; - width: 10px; - height: 10px; - border-radius: 50%; - background-color: $success; - margin-right: 5px; - } - - a { - display: flex; - flex-direction: row; - text-decoration: none; - color: white; - justify-content: end; - padding: 0; - margin: 0; - position: absolute; - right: 2%; - top: 2%; - - &:hover { - border: none; - } - } - } - } - - .quote.contextual-content > .quote-inner { - display: flex; - flex-direction: column; - gap: 4px; - - .reply-username { - font-weight: 600; - color: $color-dark-on-surface; - font-size: 0.85rem; - } - - .reply-text { - overflow: hidden; - text-overflow: ellipsis; - } - } - - .chat-messages { - flex: 1; - padding: 10px 20px; - overflow-y: auto; - position: relative; - z-index: 1; - - &::-webkit-scrollbar { - width: 7px; - } - - &::-webkit-scrollbar-track { - background: transparent; - } - - &::-webkit-scrollbar-thumb { - background-color: $color-dark-surface-container-high; - border-radius: 20px; - } - - .message { - margin-bottom: 1rem; - max-width: 70%; - position: relative; - width: fit-content; - display: flex; - align-items: flex-end; - gap: 8px; - - .message-inner { - border-radius: 12px; - position: relative; - word-wrap: break-word; - overflow-wrap: anywhere; - word-break: break-word; - width: fit-content; - max-width: 100%; - display: inline-block; - - .message-profile-pic { - width: 32px; - height: 32px; - flex-shrink: 0; - margin-bottom: 4px; - margin: 10px; - - img { - width: 100%; - height: 100%; - border-radius: 50%; - object-fit: cover; - transition: transform 0.2s ease, box-shadow 0.2s ease; - - &:hover { - transform: scale(1.1); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); - } - } - } - - .message-username { - font-weight: 600; - margin-bottom: 0.3rem; - font-size: 0.9rem; - transition: color 0.2s ease; - margin: 10px; - - &:hover { - color: $color-dark-primary; - text-decoration: underline; - } - } - - .message-content { - word-wrap: break-word; - margin: 10px 10px 0 10px; - white-space: pre-wrap; - - > p:first-child { - margin-block-start: 0; - } - - > p:last-child { - margin-block-end: 0; - } - } - - .quote.reply-preview { - user-select: none; - margin: 10px; - } - - .message-attachments { - padding: 5px 0 0 0; - overflow: hidden; - - .attachment { - a { - text-decoration: none; - } - - .attachement-image { - max-width: 200px; - border-radius: 8px; - cursor: pointer; - margin-left: 3px; - margin-right: 3px; - margin-bottom: 3px; - - &:last-child { - margin-bottom: 0; - } - - &.loading { - filter: blur(10px); - transition: filter 200ms ease; - } - } - - .attachement-image.placeholder { - background: $color-dark-surface-container-highest; - pointer-events: none; - } - - .image-wrapper { - position: relative; - display: inline-block; - } - - .loading-overlay { - position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - background: rgba(0, 0, 0, 0.08); - backdrop-filter: blur(6px); - border-radius: 8px; - } - - .preload-image { - position: absolute; - width: 0; - height: 0; - opacity: 0; - pointer-events: none; - } - - .with-icon-gap { - display: inline-flex; - align-items: center; - gap: 8px; - } - } - } - - .message-time { - font-size: 0.7rem; - color: $color-dark-on-surface-variant; - margin-top: 0.3rem; - text-align: right; - user-select: none; - margin: 4px 8px 8px 8px; - display: flex; - align-items: center; - justify-content: flex-end; - gap: 4px; - - .message-status-indicator { - display: flex; - align-items: center; - width: 16px; - height: 16px; - - .error-icon { - color: #f44336; - font-size: 16px; - } - - .success-icon { - color: #4caf50; - font-size: 16px; - } - - mdui-circular-progress { - width: 16px; - height: 16px; - } - } - } - } - - &.received .message-inner { - background: $color-dark-surface-container; - color: $color-dark-on-surface; - border-top-left-radius: 5px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - border: 1px solid rgba($color-dark-outline-variant, 0.4); - position: relative; - overflow: hidden; - - &::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient(135deg, rgba($color-dark-primary, 0.05), rgba($color-dark-tertiary, 0.03)); - pointer-events: none; - z-index: 0; - } - - > * { - position: relative; - z-index: 1; - } - } - - &.received .message-time { - color: $color-dark-on-surface-variant; - font-weight: 500; - - .message-status-indicator { - .success-icon { - color: $color-dark-on-surface-variant; - filter: brightness(1.1); - } - - .error-icon { - color: #ff6b6b; - filter: brightness(1.1); - } - } - } - - &.sent { - margin-left: auto; - flex-direction: row-reverse; - - .message-inner { - background: linear-gradient(135deg, color.adjust($color-dark-primary, $lightness: 8%), color.adjust($color-dark-primary-container, $lightness: 5%)); - color: $color-dark-on-primary; - border-top-right-radius: 5px; - box-shadow: 0 0 20px rgba($color-dark-primary, 0.4); - border: 1px solid rgba($color-dark-primary, 0.5); - position: relative; - overflow: hidden; - - &::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: linear-gradient(135deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.08)); - pointer-events: none; - z-index: 0; - } - - > * { - position: relative; - z-index: 1; - } - } - - .message-time { - color: $color-dark-on-primary; - font-weight: 500; - - .message-status-indicator { - .success-icon { - color: $color-dark-on-primary; - filter: brightness(1.2); - } - - .error-icon { - color: #ff6b6b; - filter: brightness(1.2); - } - } - } - } - } - } - - .file-overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - - background: rgba(0, 0, 0, 0.5); - - z-index: 100; - - backdrop-filter: blur(20px); - - .file-overlay-wrapper { - border-radius: 30px; - outline: 3px dashed $color-dark-primary; - outline-offset: -20px; - height: 100%; - width: 100%; - display: flex; - align-items: center; - justify-content: center; - - .file-overlay-inner { - display: flex; - gap: 12px; - align-items: center; - padding: 12px 16px; - background: rgba(18, 18, 18, 0.8); - border: 1px solid $color-dark-surface-container-high; - border-radius: 12px; - color: $color-dark-on-surface; - - mdui-icon { - color: $color-dark-primary; - } - } - } - } - - .chat-input-wrapper { - position: relative; - margin: 0 10px 10px 10px; - - .input-group { - display: flex; - background: $color-dark-surface-container; - border-radius: 30px; - flex-direction: column; - border: 1px solid rgba($color-dark-outline-variant, 0.4); - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); - - .contextual-preview { - padding: 12px 16px 0 16px; - display: flex; - align-items: flex-start; - gap: 16px; - - mdui-icon { - align-self: center; - box-sizing: content-box; - } - - .reply-cancel { - margin-left: auto; - } - } - - .attachments-preview { - align-items: center; - - .attachments-chips { - display: flex; - flex-wrap: wrap; - gap: 8px; - } - } - - .chat-input { - flex: 1; - display: flex; - flex-direction: row; - align-items: center; - - .buttons, .left-buttons { - display: flex; - flex-direction: row; - align-items: center; - } - - .left-buttons { - .emoji-btn { - margin: 10px; - color: $color-dark-on-surface-variant; - transition: color 0.2s ease; - flex-shrink: 0; - align-self: flex-end; - - &:hover { - color: $color-dark-primary; - } - } - } - - .message-input { - flex: 1; - padding: 20px 0; - border: none; - border-radius: 25px; - font-size: 1rem; - outline: none; - caret-color: $color-dark-primary; - color: $color-dark-on-surface; - background: transparent; - resize: none; - font: inherit; - font-size: 13pt; - height: 100%; - width: 100%; - - &::placeholder { - color: $color-dark-on-surface-variant; - opacity: 0.7; - } - } - - .buttons { - .send-btn { - margin: 10px; - width: 50px; - height: 50px; - border-radius: 50%; - background: linear-gradient(135deg, color.adjust($color-dark-primary, $lightness: 8%), color.adjust($color-dark-primary-container, $lightness: 5%)); - color: $color-dark-on-primary; - border: 1px solid rgba($color-dark-primary, 0.5); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: all 0.25s ease; - align-self: flex-end; - box-shadow: 0 0 20px rgba($color-dark-primary, 0.4); - - &:hover { - transform: translateY(-2px); - box-shadow: 0 0 30px rgba($color-dark-primary, 0.6); - } - } - } - } - } - } - } - } -} - -// ChatHeader component styles -.chat-header-left { - .product-name { - font-size: 1.8rem; - font-weight: 700; - background: linear-gradient(45deg, $color-dark-primary, $color-dark-tertiary); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - text-shadow: 0 0 20px rgba($color-dark-primary, 0.5); - } - - .profile { - a { - display: flex; - align-items: center; - text-decoration: none; - transition: transform 0.3s ease; - - &:hover { - transform: scale(1.05); - } - - img { - width: 40px; - height: 40px; - border-radius: 50%; - object-fit: cover; - border: 2px solid rgba($color-dark-primary, 0.4); - box-shadow: 0 0 15px rgba($color-dark-primary, 0.3); - transition: all 0.3s ease; - - &:hover { - box-shadow: 0 0 25px rgba($color-dark-primary, 0.5); - border-color: rgba($color-dark-primary, 0.6); - } - } - } - } -} - -.message-profile-pic { - img { - width: 40px; - height: 40px; - border-radius: 50%; - object-fit: cover; - border: 2px solid $color-dark-outline; - - &.loading { - opacity: 0.6; - cursor: default; - } - } -} - -.message-username { - &.loading { - opacity: 0.6; - cursor: default; - } -} - -.context-menu { - position: fixed; - background: $color-dark-surface; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - padding: 0.5rem 0; - min-width: 160px; - z-index: 1000; - - &.entering { - animation: fadeInDown 0.2s ease forwards; - } - - &.entering-left { - animation: fadeInLeft 0.2s ease forwards; - } - - &.entering-up { - animation: fadeInUp 0.2s ease forwards; - } - - &.entering-up-left { - animation: fadeInUpLeft 0.2s ease forwards; - } - - &.closing { - animation: fadeOutUp 0.2s ease forwards; - } - - &.closing-left { - animation: fadeOutRight 0.2s ease forwards; - } - - &.closing-up { - animation: fadeOutDown 0.2s ease forwards; - } - - &.closing-up-left { - animation: fadeOutDownRight 0.2s ease forwards; - } - - .context-menu-item { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem 1rem; - cursor: pointer; - color: $color-dark-on-surface; - font-size: 0.9rem; - transition: background-color 0.2s ease; - - &:hover { - background-color: $color-dark-surface-container; - } - - .material-symbols { - font-size: 1.1rem; - color: $color-dark-on-surface-variant; - } - } -} - -// Fullscreen Image Viewer -.fullscreen-image-overlay { - position: fixed; - inset: 0; - width: 100vw; - height: 100vh; - background: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(20px); - z-index: 9999; - opacity: 1; - transition: opacity 0.3s ease; - - &.closing { - opacity: 0; - } - - .fullscreen-animated-image { - position: absolute; - object-fit: contain; - border-radius: 12px; - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4); - transition: left 0.3s ease, top 0.3s ease, width 0.3s ease, height 0.3s ease; - } - - .fullscreen-controls { - position: absolute; - display: flex; - gap: 8px; - - &.top-right { - top: 12px; - right: 12px; - } - } - - .progress-wrapper { - width: 40px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - } -} - -// Emoji Menu Styles -.emoji-menu { - $transition: cubic-bezier(0.4, 0, 0.2, 1); - - background: $color-dark-surface-container; - border: 1px solid rgba($color-dark-outline-variant, 0.4); - border-radius: 16px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); - backdrop-filter: blur(20px); - width: 320px; - height: 400px; - overflow: hidden; - display: flex; - flex-direction: column; - transform-origin: bottom left; - opacity: 0; - transform: translateY(30px); - transition: transform 0.25s $transition, opacity 0.25s $transition; - user-select: none; - - &.open { - opacity: 1; - transform: translateY(0); - } - - .emoji-menu-header { - background: $color-dark-surface-container-high; - border-bottom: 1px solid rgba($color-dark-outline-variant, 0.2); - position: sticky; - top: 0; - z-index: 1; - - .emoji-category-tabs { - display: flex; - gap: 4px; - overflow-x: auto; - overflow-y: hidden; - scroll-behavior: smooth; - padding: 8px; - - &::-webkit-scrollbar { - height: 4px; - } - - &::-webkit-scrollbar-track { - background: transparent; - } - - &::-webkit-scrollbar-thumb { - background-color: $color-dark-surface-container; - border-radius: 2px; - } - - &::-webkit-scrollbar-thumb:hover { - background-color: $color-dark-surface-container-high; - } - - .emoji-category-tab { - background: transparent; - border: none; - border-radius: 10px; - padding: 8px; - cursor: pointer; - transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - font-size: 1.2rem; - min-width: 40px; - height: 40px; - display: flex; - align-items: center; - justify-content: center; - position: relative; - overflow: hidden; - - &:hover { - background: $color-dark-surface-container; - } - - &.active { - background: $color-dark-primary-container; - color: $color-dark-on-primary-container; - transform: scale(1.05); - } - - &::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: $color-dark-primary-container; - opacity: 0; - transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1); - border-radius: 8px; - } - - &.active::before { - opacity: 1; - } - - span { - position: relative; - z-index: 1; - } - } - } - } - - .emoji-grid { - display: flex; - flex-direction: column; - flex: 1; - overflow-y: auto; - scroll-behavior: smooth; - - &::-webkit-scrollbar { - width: 6px; - } - - &::-webkit-scrollbar-track { - background: transparent; - } - - &::-webkit-scrollbar-thumb { - background-color: $color-dark-surface-container-high; - border-radius: 3px; - } - - .emoji-category-section { - .emoji-category-title { - position: sticky; - top: 0; - padding-top: 5px; - padding-bottom: 5px; - padding-left: 12px; - padding-right: 12px; - font-size: 0.85rem; - font-weight: 600; - color: $color-dark-on-surface-variant; - z-index: 2; - margin: 0; - backdrop-filter: blur(10px); - } - - .emoji-category-grid { - display: flex; - flex-direction: row; - flex-wrap: wrap; - gap: 2px; - padding: 8px; - } - } - - .emoji-item { - $size: 30px; - - background: transparent; - border: none; - border-radius: 6px; - padding: 5px; - cursor: pointer; - transition: all 0.15s ease; - font-size: $size; - width: $size; - height: $size; - box-sizing: content-box; - display: flex; - align-items: center; - justify-content: center; - - &:hover { - background: $color-dark-surface-container-high; - transform: scale(1.1); - } - - &:active { - transform: scale(0.95); - } - } - } - - .emoji-empty-state { - padding: 20px; - text-align: center; - color: $color-dark-on-surface-variant; - font-size: 0.9rem; - } - - // Integrated mode styles (inside reaction bar) - &.integrated { - position: relative !important; - width: 320px !important; - height: 400px !important; - transform: none !important; - opacity: 1 !important; - box-shadow: none; - border: none; - background: $color-dark-surface-container; - overflow: visible; - } -} - -// Panel chat styles -// контейнер чата и панели с чатами -.all-container { - display: flex; - flex-direction: row; - width: 100%; - height: 100%; -} - -#profile { - display: none; - flex-direction: column; - z-index: 2000; - top: 0; - left: 0; - position: fixed; - height: 100vh; - width: 27%; - background-color: $color-dark-surface-container; - position: relative; - - .profileheader { - display: flex; - gap: 200px; - - p { - color: white; - } - - a { - text-decoration: none; - color: white; - border: solid 2px $color-dark-on-surface-variant; - padding: 5px; - border-radius: 10px; - - &:hover { - background-color: rgba(255, 255, 255, 0.241); - } - } - } -} - -#chat-list { - display: flex; - flex-direction: column; - flex-grow: 0; - width: 40%; - background-color: $color-dark-surface-container; - height: 100%; - z-index: 1000; - min-height: 0; // allow children to manage their own scrolling - - .chat-header-left { - display: flex; - color: white; - font-size: 25px; - background-color: $color-dark-surface-container; - width: 100%; - justify-content: center; - align-items: center; - padding: 16px; - overflow: hidden; - - .product-name { - flex-grow: 1; - } - - .profile { - font-size: 24px; - display: flex; - justify-content: start; - flex-shrink: 0; - flex-grow: 0; - - #closeprofile a { - text-decoration: none; - color: white; - border: solid 2px $color-dark-on-surface-variant; - padding: 5px; - border-radius: 10px; - - &:hover { - background-color: rgba(255, 255, 255, 0.241); - } - } - - img { - $size: 45px; - display: flex; - border-radius: 50%; - width: $size; - height: $size; - } - } - } - - .chat-tabs { - margin-top: 5px; - width: 100%; - height: calc(100% - 80px); - display: flex; - flex-direction: column; - min-height: 0; // prevent flex collapse when inner overflows - --mdui-color-surface: $color-dark-surface-container; - --mdui-color-surface-variant: transparent; - - img { - width: 45px; - height: 45px; - border-radius: 20%; - object-fit: cover; - margin-right: 1rem; - } - - mdui-tabs { - height: 100%; - display: flex; - flex-direction: column; - min-height: 0; // enable inner panel to scroll - } - - mdui-tab-panel[active] { - flex: 1; - display: flex; - flex-direction: column; - min-height: 0; // critical to avoid collapsing - overflow: hidden; - } - - mdui-list { - flex: 1; - min-height: 0; // allow scroll area to size correctly - overflow-y: auto; - padding: 0; - margin: 0; - } - } - - mdui-bottom-app-bar { - position: relative; - width: 100%; - padding-left: 16px; - padding-right: 16px; - margin-top: auto; - } -} - -// Profile styles -#profile-dialog .content { - display: flex; - flex-direction: column; - gap: 24px; - min-width: 400px; - - .header-top { - display: flex; - flex-direction: row; - align-items: center; - gap: 16px; - position: relative; - padding-bottom: 16px; - border-bottom: 1px solid $color-dark-outline; - - .profile-picture-container { - position: relative; - $size: 70px; - width: $size; - height: $size; - flex-shrink: 0; - - #profile-picture { - width: $size; - height: $size; - border-radius: 50%; - object-fit: cover; - } - - .upload-overlay { - position: absolute; - bottom: 0; - right: 0; - width: 28px; - height: 28px; - cursor: pointer; - } - } - - mdui-text-field { - flex: 1; - } - } - - #profile-form { - display: flex; - flex-direction: column; - gap: 16px; - - mdui-text-field { - width: 100%; - } - - .dialog-actions { - display: flex; - gap: 12px; - padding-top: 16px; - border-top: 1px solid $color-dark-outline; - - > * { - flex: 1; - } - } - } -} - -// User profile dialog content styles -#user-profile-dialog .content { - display: flex; - gap: 1.5rem; - - .profile-picture-section { - flex-shrink: 0; - - .profile-picture { - width: 80px; - height: 80px; - border-radius: 50%; - object-fit: cover; - border: 2px solid $color-dark-outline; - } - } - - .profile-info { - flex: 1; - display: flex; - flex-direction: column; - gap: 1rem; - - .username-section { - display: flex; - align-items: center; - gap: 0.75rem; - - .username { - margin: 0; - color: $color-dark-on-surface; - font-size: 1.1rem; - font-weight: 600; - } - - .online-status { - display: flex; - align-items: center; - gap: 0.5rem; - font-size: 0.85rem; - padding: 0.25rem 0.5rem; - border-radius: 12px; - font-weight: 500; - - &.online { - color: $success; - background-color: rgba(76, 175, 80, 0.1); - - .online-indicator { - width: 8px; - height: 8px; - border-radius: 50%; - background-color: $success; - } - } - - &.offline { - color: $color-dark-on-surface-variant; - background-color: rgba(255, 255, 255, 0.05); - - .offline-indicator { - width: 8px; - height: 8px; - border-radius: 50%; - background-color: $color-dark-on-surface-variant; - } - } - } - } - - .bio-section { - label { - display: block; - color: $color-dark-on-surface-variant; - font-size: 0.85rem; - font-weight: 500; - margin-bottom: 0.5rem; - } - - .bio-display { - color: $color-dark-on-surface; - font-size: 0.9rem; - line-height: 1.4; - padding: 0.75rem; - background-color: $color-dark-surface; - border-radius: 8px; - border: 1px solid $color-dark-outline; - min-height: 60px; - } - - .bio-actions { - display: flex; - gap: 0.5rem; - margin-top: 0.5rem; - } - } - - .profile-stats { - display: flex; - flex-direction: column; - gap: 0.5rem; - - .stat { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.5rem 0; - - .stat-label { - color: $color-dark-on-surface-variant; - font-size: 0.85rem; - } - - .stat-value { - color: $color-dark-on-surface; - font-size: 0.85rem; - font-weight: 500; - } - } - } - - .profile-actions { - display: flex; - gap: 0.75rem; - margin-top: 0.5rem; - - mdui-button { - flex: 1; - } - } - } -} - -// Cropper Dialog Styles -#cropper-dialog { - .cropper-dialog-content { - display: flex; - flex-direction: column; - gap: 16px; - min-width: 500px; - max-width: 600px; - } - - .cropper-header { - display: flex; - justify-content: space-between; - align-items: center; - padding-bottom: 16px; - border-bottom: 1px solid $color-dark-outline; - - h3 { - margin: 0; - color: $color-dark-on-surface; - } - } - - .cropper-container { - display: flex; - justify-content: center; - align-items: center; - min-height: 400px; - background: $color-dark-surface-container; - border-radius: 8px; - overflow: hidden; - - #cropper-area { - width: 100%; - height: 100%; - min-height: 400px; - } - } - - .cropper-actions { - display: flex; - gap: 12px; - justify-content: flex-end; - padding-top: 16px; - border-top: 1px solid $color-dark-outline; - } -} - -// Reaction styles -.message-reactions { - display: flex; - flex-wrap: wrap; - gap: 4px; - margin-top: 8px; - margin-left: 10px; - margin-right: 10px; - animation: messageReactionsFadeIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); -} - -.reaction-button { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - border: none; - border-radius: 16px; - background-color: $color-dark-surface-container; - cursor: pointer; - transition: transform 0.2s ease, background-color 0.2s ease; - font-size: 1px; - min-height: 28px; - animation: reactionFadeIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); - - &.removing { - animation: reactionFadeOut 0.2s ease forwards; - } - - &:hover { - background-color: $color-dark-surface-container-high; - transform: scale(1.05); - } - - &.reacted { - background-color: $color-dark-primary-container; - border-color: $color-dark-primary; - color: $color-dark-on-primary-container; - - &:hover { - background-color: color.adjust($color-dark-primary-container, $lightness: 20%); - } - } -} - -.reaction-emoji { - font-size: 17px; - line-height: 1; -} - -.reaction-count { - font-size: 12px; - font-weight: 500; - line-height: 1; -} - -// Reaction bar styles (standalone) -.reaction-bar { - background: $color-dark-surface-container; - border: 1px solid $color-dark-outline; - border-radius: 24px; - padding: 8px; - opacity: 1; - transition: all 0.15s ease; - backdrop-filter: blur(8px); - transform: translateY(0); - - &.closing { - opacity: 0; - transform: scale(0.8); - } -} - -// Emoji menu wrapper inside reaction bar -.emoji-menu-wrapper { - width: 320px; - height: 400px; - display: flex; - align-items: center; - justify-content: center; -} - -// Context menu wrapper with animations -.context-menu-wrapper { - position: relative; - display: block; - - // Animation states - &.entering { - opacity: 0; - transform: scale(0.8); - animation: contextMenuEnter 0.2s ease forwards; - } - - &.entering-left { - opacity: 0; - transform: translateX(-20px) scale(0.8); - animation: contextMenuEnterLeft 0.2s ease forwards; - } - - &.entering-up { - opacity: 0; - transform: translateY(20px) scale(0.8); - animation: contextMenuEnterUp 0.2s ease forwards; - } - - &.entering-up-left { - opacity: 0; - transform: translateX(-20px) translateY(20px) scale(0.8); - animation: contextMenuEnterUpLeft 0.2s ease forwards; - } - - &.closing { - opacity: 1; - transform: scale(1); - animation: contextMenuClose 0.2s ease forwards; - } - - &.closing-left { - opacity: 1; - transform: translateX(0) scale(1); - animation: contextMenuCloseLeft 0.2s ease forwards; - } - - &.closing-up { - opacity: 1; - transform: translateY(0) scale(1); - animation: contextMenuCloseUp 0.2s ease forwards; - } - - &.closing-up-left { - opacity: 1; - transform: translateX(0) translateY(0) scale(1); - animation: contextMenuCloseUpLeft 0.2s ease forwards; - } -} - -// Reaction bar inside context menu wrapper -.context-menu-reaction-bar { - display: flex; - align-items: center; - gap: 4px; - padding: 8px 12px; - background: $color-dark-surface-container; - border: 1px solid $color-dark-outline; - border-radius: 16px; - position: absolute; - bottom: 100%; - justify-content: center; - margin-bottom: 10px; - transition: width 0.3s ease-out, height 0.3s ease-out; - - &.left { - left: 0; - transform: translateX(0); - } - - &.right { - right: 0; - transform: translateX(0); - } - - &.expanded { - padding: 0; - overflow: hidden; - width: 320px; - height: 400px; - border-radius: 16px; - - // Default: expand downward from the reaction bar's bottom edge - position: absolute; - bottom: auto; - top: 0; - left: 0; - transform: translateY(0); - - &.expand-upward { - // Expand upward from the reaction bar's top edge - bottom: 100%; - top: auto; - margin-bottom: 10px; - margin-top: 0; - transform: translateY(0); - } - - .emoji-menu-wrapper { - animation: emojiMenuEnter 0.5s ease; - } - } -} - -@keyframes emojiMenuEnter { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} - -.reaction-bar-content { - display: flex; - align-items: center; - gap: 4px; - transition: opacity 0.3s ease-out; - - &.faded { - opacity: 0; - } -} - -.reaction-emoji-button { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: none; - border-radius: 16px; - background: transparent; - cursor: pointer; - transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); - font-size: 18px; - - &:hover { - background: var(--mdui-color-surface-container-high); - transform: scale(1.3); - box-shadow: var(--mdui-elevation-1); - } - - &:active { - transform: scale(0.95); - transition: transform 0.1s ease; - } -} - -.reaction-expand-button { - display: flex; - align-items: center; - justify-content: center; - width: 32px; - height: 32px; - border: 1px solid var(--mdui-color-outline); - border-radius: 16px; - background: var(--mdui-color-surface); - cursor: pointer; - transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); - - &:hover { - background: var(--mdui-color-surface-container-high); - border-color: var(--mdui-color-primary); - transform: scale(1.1); - box-shadow: var(--mdui-elevation-1); - } - - &:active { - transform: scale(0.95); - transition: transform 0.1s ease; - } - - .material-symbols { - font-size: 18px; - color: var(--mdui-color-on-surface); - transition: transform 0.2s ease; - } - - &:hover .material-symbols { - transform: rotate(90deg); - } -} - -// Animation for reactions appearing/disappearing -@keyframes messageReactionsFadeIn { - from { - opacity: 0; - transform: translateY(-10px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes reactionFadeIn { - from { - opacity: 0; - transform: scale(0.8); - } - - to { - opacity: 1; - transform: scale(1); - } -} - -@keyframes reactionFadeOut { - from { - opacity: 1; - transform: scale(1); - } - - to { - opacity: 0; - transform: scale(0.8); - } -} - -// Context menu wrapper animations -@keyframes contextMenuEnter { - to { - opacity: 1; - transform: scale(1); - } -} - -@keyframes contextMenuEnterLeft { - to { - opacity: 1; - transform: translateX(0) scale(1); - } -} - -@keyframes contextMenuEnterUp { - to { - opacity: 1; - transform: translateY(0) scale(1); - } -} - -@keyframes contextMenuEnterUpLeft { - to { - opacity: 1; - transform: translateX(0) translateY(0) scale(1); - } -} - -@keyframes contextMenuClose { - to { - opacity: 0; - transform: scale(0.8); - } -} - -@keyframes contextMenuCloseLeft { - to { - opacity: 0; - transform: translateX(-20px) scale(0.8); - } -} - -@keyframes contextMenuCloseUp { - to { - opacity: 0; - transform: translateY(20px) scale(0.8); - } -} - -@keyframes contextMenuCloseUpLeft { - to { - opacity: 0; - transform: translateX(-20px) translateY(20px) scale(0.8); - } -} - -// Mobile responsive -@media (max-width: 768px) { - .reaction-bar { - padding: 6px; - } - - .reaction-emoji-button, - .reaction-expand-button { - width: 28px; - height: 28px; - } - - .reaction-emoji-button { - font-size: 16px; - } - - .reaction-expand-button .material-symbols { - font-size: 16px; - } -} - -// Settings styles -#settings-dialog { - .fullscreen-wrapper { - display: flex; - align-items: center; - justify-content: center; - height: calc(100vh - (1.5rem * 2)); - width: 100%; - position: relative; - - #settings-dialog-inner { - max-width: 1200px; - max-height: 1000px; - width: 100%; - height: 100%; - - .header { - display: flex; - flex-direction: row; - gap: 10px; - margin-bottom: 16px; - } - - #settings-menu { - display: flex; - flex-direction: row; - gap: 16px; - - mdui-list { - max-width: 280px; - padding-right: 16px; - overflow-y: auto; - } - - .screen { - flex: 1; - overflow-y: auto; - position: relative; - - .settings-panel { - display: flex; - flex-direction: column; - gap: 16px; - opacity: 0; - visibility: hidden; - transform: translateY(20px); - transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s ease; - position: absolute; - top: 0; - left: 0; - width: 100%; - - &.active { - opacity: 1; - visibility: visible; - transform: translateY(0); - position: relative; - } - - h3 { - margin: 0 0 16px 0; - color: $color-dark-on-surface; - } - - mdui-text-field, - mdui-select, - mdui-switch, - mdui-button { - margin-bottom: 8px; - } - - mdui-switch { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 0; - border-bottom: 1px solid $color-dark-outline; - - &:last-child { - border-bottom: none; - } - } - - p { - margin: 8px 0; - color: $color-dark-on-surface-variant; - } - - mdui-linear-progress { - margin: 16px 0; - } - } - } - } - } - } -} - -// Reply dialog styles -.reply-dialog .dialog-content { - width: 300px; - overflow-x:hidden; - - .reply-preview-dialog { - margin-bottom: 1rem; - padding: 16px; - background-color: $color-dark-surface-container; - border-radius: 16px; - - .reply-content { - display: flex; - flex-direction: column; - gap: 0.25rem; - - .reply-username { - font-weight: 600; - color: $color-dark-on-surface; - font-size: 0.85rem; - } - - .reply-text { - color: $color-dark-on-surface-variant; - font-size: 0.9rem; - line-height: 1.4; - } - } - } -} diff --git a/frontend/src/pages/chat/css/_animations.scss b/frontend/src/pages/chat/css/_animations.scss new file mode 100644 index 0000000..14b01c6 --- /dev/null +++ b/frontend/src/pages/chat/css/_animations.scss @@ -0,0 +1,107 @@ +@use "../../../css/common/colors" as *; +@use "../../../css/common/material" as *; +@use "sass:color"; + +// Animation for reactions appearing/disappearing +@keyframes messageReactionsFadeIn { + from { + opacity: 0; + transform: translateY(-10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes reactionFadeIn { + from { + opacity: 0; + transform: scale(0.8); + } + + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes reactionFadeOut { + from { + opacity: 1; + transform: scale(1); + } + + to { + opacity: 0; + transform: scale(0.8); + } +} + +// Context menu wrapper animations +@keyframes contextMenuEnter { + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes contextMenuEnterLeft { + to { + opacity: 1; + transform: translateX(0) scale(1); + } +} + +@keyframes contextMenuEnterUp { + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes contextMenuEnterUpLeft { + to { + opacity: 1; + transform: translateX(0) translateY(0) scale(1); + } +} + +@keyframes contextMenuClose { + to { + opacity: 0; + transform: scale(0.8); + } +} + +@keyframes contextMenuCloseLeft { + to { + opacity: 0; + transform: translateX(-20px) scale(0.8); + } +} + +@keyframes contextMenuCloseUp { + to { + opacity: 0; + transform: translateY(20px) scale(0.8); + } +} + +@keyframes contextMenuCloseUpLeft { + to { + opacity: 0; + transform: translateX(-20px) translateY(20px) scale(0.8); + } +} + +@keyframes emojiMenuEnter { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} diff --git a/frontend/src/pages/chat/css/_chat-input.scss b/frontend/src/pages/chat/css/_chat-input.scss new file mode 100644 index 0000000..bea62bf --- /dev/null +++ b/frontend/src/pages/chat/css/_chat-input.scss @@ -0,0 +1,318 @@ +@use "../../../css/common/colors" as *; +@use "../../../css/common/material" as *; +@use "sass:color"; + +.chat-input-wrapper { + position: relative; + margin: 0 10px 10px 10px; + + .input-group { + display: flex; + background: $color-dark-surface-container; + border-radius: 30px; + flex-direction: column; + border: 1px solid rgba($color-dark-outline-variant, 0.4); + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); + + .contextual-preview { + padding: 12px 16px 0 16px; + display: flex; + align-items: flex-start; + gap: 16px; + + mdui-icon { + align-self: center; + box-sizing: content-box; + } + + .reply-cancel { + margin-left: auto; + } + } + + .attachments-preview { + align-items: center; + + .attachments-chips { + display: flex; + flex-wrap: wrap; + gap: 8px; + } + } + + .chat-input { + flex: 1; + display: flex; + flex-direction: row; + align-items: center; + + .buttons, .left-buttons { + display: flex; + flex-direction: row; + align-items: center; + } + + .left-buttons { + .emoji-btn { + margin: 10px; + color: $color-dark-on-surface-variant; + transition: color 0.2s ease; + flex-shrink: 0; + align-self: flex-end; + + &:hover { + color: $color-dark-primary; + } + } + } + + .message-input { + flex: 1; + padding: 20px 0; + border: none; + border-radius: 25px; + font-size: 1rem; + outline: none; + caret-color: $color-dark-primary; + color: $color-dark-on-surface; + background: transparent; + resize: none; + font: inherit; + font-size: 13pt; + height: 100%; + width: 100%; + + &::placeholder { + color: $color-dark-on-surface-variant; + opacity: 0.7; + } + } + + .buttons { + .send-btn { + margin: 10px; + width: 50px; + height: 50px; + border-radius: 50%; + background: linear-gradient(135deg, color.adjust($color-dark-primary, $lightness: 8%), color.adjust($color-dark-primary-container, $lightness: 5%)); + color: $color-dark-on-primary; + border: 1px solid rgba($color-dark-primary, 0.5); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.25s ease; + align-self: flex-end; + box-shadow: 0 0 20px rgba($color-dark-primary, 0.4); + + &:hover { + transform: translateY(-2px); + box-shadow: 0 0 30px rgba($color-dark-primary, 0.6); + } + } + } + } + } +} + +// Emoji Menu Styles +.emoji-menu { + $transition: cubic-bezier(0.4, 0, 0.2, 1); + + background: $color-dark-surface-container; + border: 1px solid rgba($color-dark-outline-variant, 0.4); + border-radius: 16px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); + backdrop-filter: blur(20px); + width: 320px; + height: 400px; + overflow: hidden; + display: flex; + flex-direction: column; + transform-origin: bottom left; + opacity: 0; + transform: translateY(30px); + transition: transform 0.25s $transition, opacity 0.25s $transition; + user-select: none; + + &.open { + opacity: 1; + transform: translateY(0); + } + + .emoji-menu-header { + background: $color-dark-surface-container-high; + border-bottom: 1px solid rgba($color-dark-outline-variant, 0.2); + position: sticky; + top: 0; + z-index: 1; + + .emoji-category-tabs { + display: flex; + gap: 4px; + overflow-x: auto; + overflow-y: hidden; + scroll-behavior: smooth; + padding: 8px; + + &::-webkit-scrollbar { + height: 4px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: $color-dark-surface-container; + border-radius: 2px; + } + + &::-webkit-scrollbar-thumb:hover { + background-color: $color-dark-surface-container-high; + } + + .emoji-category-tab { + background: transparent; + border: none; + border-radius: 10px; + padding: 8px; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + font-size: 1.2rem; + min-width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; + + &:hover { + background: $color-dark-surface-container; + } + + &.active { + background: $color-dark-primary-container; + color: $color-dark-on-primary-container; + transform: scale(1.05); + } + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: $color-dark-primary-container; + opacity: 0; + transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1); + border-radius: 8px; + } + + &.active::before { + opacity: 1; + } + + span { + position: relative; + z-index: 1; + } + } + } + } + + .emoji-grid { + display: flex; + flex-direction: column; + flex: 1; + overflow-y: auto; + scroll-behavior: smooth; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: $color-dark-surface-container-high; + border-radius: 3px; + } + + .emoji-category-section { + .emoji-category-title { + position: sticky; + top: 0; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 12px; + padding-right: 12px; + font-size: 0.85rem; + font-weight: 600; + color: $color-dark-on-surface-variant; + z-index: 2; + margin: 0; + backdrop-filter: blur(10px); + } + + .emoji-category-grid { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 2px; + padding: 8px; + } + } + + .emoji-item { + $size: 30px; + + background: transparent; + border: none; + border-radius: 6px; + padding: 5px; + cursor: pointer; + transition: all 0.15s ease; + font-size: $size; + width: $size; + height: $size; + box-sizing: content-box; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background: $color-dark-surface-container-high; + transform: scale(1.1); + } + + &:active { + transform: scale(0.95); + } + } + } + + .emoji-empty-state { + padding: 20px; + text-align: center; + color: $color-dark-on-surface-variant; + font-size: 0.9rem; + } + + // Integrated mode styles (inside reaction bar) + &.integrated { + position: relative !important; + width: 320px !important; + height: 400px !important; + transform: none !important; + opacity: 1 !important; + box-shadow: none; + border: none; + background: $color-dark-surface-container; + overflow: visible; + } +} diff --git a/frontend/src/pages/chat/css/_context-menu.scss b/frontend/src/pages/chat/css/_context-menu.scss new file mode 100644 index 0000000..60b4bba --- /dev/null +++ b/frontend/src/pages/chat/css/_context-menu.scss @@ -0,0 +1,292 @@ +@use "../../../css/common/colors" as *; +@use "../../../css/common/material" as *; +@use "sass:color"; + +// Reaction bar styles (standalone) +.reaction-bar { + background: $color-dark-surface-container; + border: 1px solid $color-dark-outline; + border-radius: 24px; + padding: 8px; + opacity: 1; + transition: all 0.15s ease; + backdrop-filter: blur(8px); + transform: translateY(0); + + &.closing { + opacity: 0; + transform: scale(0.8); + } +} + +// Emoji menu wrapper inside reaction bar +.emoji-menu-wrapper { + width: 320px; + height: 400px; + display: flex; + align-items: center; + justify-content: center; +} + +// Context menu wrapper with animations +.context-menu-wrapper { + position: relative; + display: block; + + // Animation states + &.entering { + opacity: 0; + transform: scale(0.8); + animation: contextMenuEnter 0.2s ease forwards; + } + + &.entering-left { + opacity: 0; + transform: translateX(-20px) scale(0.8); + animation: contextMenuEnterLeft 0.2s ease forwards; + } + + &.entering-up { + opacity: 0; + transform: translateY(20px) scale(0.8); + animation: contextMenuEnterUp 0.2s ease forwards; + } + + &.entering-up-left { + opacity: 0; + transform: translateX(-20px) translateY(20px) scale(0.8); + animation: contextMenuEnterUpLeft 0.2s ease forwards; + } + + &.closing { + opacity: 1; + transform: scale(1); + animation: contextMenuClose 0.2s ease forwards; + } + + &.closing-left { + opacity: 1; + transform: translateX(0) scale(1); + animation: contextMenuCloseLeft 0.2s ease forwards; + } + + &.closing-up { + opacity: 1; + transform: translateY(0) scale(1); + animation: contextMenuCloseUp 0.2s ease forwards; + } + + &.closing-up-left { + opacity: 1; + transform: translateX(0) translateY(0) scale(1); + animation: contextMenuCloseUpLeft 0.2s ease forwards; + } +} + +.context-menu { + position: fixed; + background: $color-dark-surface; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + padding: 0.5rem 0; + min-width: 160px; + z-index: 1000; + + &.entering { + animation: fadeInDown 0.2s ease forwards; + } + + &.entering-left { + animation: fadeInLeft 0.2s ease forwards; + } + + &.entering-up { + animation: fadeInUp 0.2s ease forwards; + } + + &.entering-up-left { + animation: fadeInUpLeft 0.2s ease forwards; + } + + &.closing { + animation: fadeOutUp 0.2s ease forwards; + } + + &.closing-left { + animation: fadeOutRight 0.2s ease forwards; + } + + &.closing-up { + animation: fadeOutDown 0.2s ease forwards; + } + + &.closing-up-left { + animation: fadeOutDownRight 0.2s ease forwards; + } + + .context-menu-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + cursor: pointer; + color: $color-dark-on-surface; + font-size: 0.9rem; + transition: background-color 0.2s ease; + + &:hover { + background-color: $color-dark-surface-container; + } + + .material-symbols { + font-size: 1.1rem; + color: $color-dark-on-surface-variant; + } + } +} + +// Reaction bar inside context menu wrapper +.context-menu-reaction-bar { + display: flex; + align-items: center; + gap: 4px; + padding: 8px 12px; + background: $color-dark-surface-container; + border: 1px solid $color-dark-outline; + border-radius: 16px; + position: absolute; + bottom: 100%; + justify-content: center; + margin-bottom: 10px; + transition: width 0.3s ease-out, height 0.3s ease-out; + + &.left { + left: 0; + transform: translateX(0); + } + + &.right { + right: 0; + transform: translateX(0); + } + + &.expanded { + padding: 0; + overflow: hidden; + width: 320px; + height: 400px; + border-radius: 16px; + + // Default: expand downward from the reaction bar's bottom edge + position: absolute; + bottom: auto; + top: 0; + left: 0; + transform: translateY(0); + + &.expand-upward { + // Expand upward from the reaction bar's top edge + bottom: 100%; + top: auto; + margin-bottom: 10px; + margin-top: 0; + transform: translateY(0); + } + + .emoji-menu-wrapper { + animation: emojiMenuEnter 0.5s ease; + } + } +} + +.reaction-bar-content { + display: flex; + align-items: center; + gap: 4px; + transition: opacity 0.3s ease-out; + + &.faded { + opacity: 0; + } +} + +.reaction-emoji-button { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: none; + border-radius: 16px; + background: transparent; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); + font-size: 18px; + + &:hover { + background: var(--mdui-color-surface-container-high); + transform: scale(1.3); + box-shadow: var(--mdui-elevation-1); + } + + &:active { + transform: scale(0.95); + transition: transform 0.1s ease; + } +} + +.reaction-expand-button { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: 1px solid var(--mdui-color-outline); + border-radius: 16px; + background: var(--mdui-color-surface); + cursor: pointer; + transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); + + &:hover { + background: var(--mdui-color-surface-container-high); + border-color: var(--mdui-color-primary); + transform: scale(1.1); + box-shadow: var(--mdui-elevation-1); + } + + &:active { + transform: scale(0.95); + transition: transform 0.1s ease; + } + + .material-symbols { + font-size: 18px; + color: var(--mdui-color-on-surface); + transition: transform 0.2s ease; + } + + &:hover .material-symbols { + transform: rotate(90deg); + } +} + +// Mobile responsive +@media (max-width: 768px) { + .reaction-bar { + padding: 6px; + } + + .reaction-emoji-button, + .reaction-expand-button { + width: 28px; + height: 28px; + } + + .reaction-emoji-button { + font-size: 16px; + } + + .reaction-expand-button .material-symbols { + font-size: 16px; + } +} diff --git a/frontend/src/pages/chat/css/_layout.scss b/frontend/src/pages/chat/css/_layout.scss new file mode 100644 index 0000000..720a0d4 --- /dev/null +++ b/frontend/src/pages/chat/css/_layout.scss @@ -0,0 +1,49 @@ +@use "../../../css/common/colors" as *; +@use "../../../css/common/material" as *; +@use "sass:color"; + +#chat-interface { + height: 100%; + background: linear-gradient(135deg, $color-dark-background 0%, $color-dark-surface-container 70%, rgba($color-dark-primary-container, 0.3) 100%); + position: relative; + + &::before { + content: ''; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + radial-gradient(circle at 20% 80%, rgba($color-dark-primary, 0.15) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba($color-dark-tertiary, 0.15) 0%, transparent 50%), + radial-gradient(circle at 40% 40%, rgba($color-dark-secondary, 0.1) 0%, transparent 50%); + pointer-events: none; + z-index: 0; + } + + .chat-container { + display: flex; + width: 100%; + flex-direction: column; + overflow: hidden; + + .chat-main { + flex-grow: 1; + display: flex; + flex-direction: column; + height: 100%; + position: relative; + overflow: hidden; + } + } +} + +// Panel chat styles +// контейнер чата и панели с чатами +.all-container { + display: flex; + flex-direction: row; + width: 100%; + height: 100%; +} diff --git a/frontend/src/pages/chat/css/_left-panel.scss b/frontend/src/pages/chat/css/_left-panel.scss new file mode 100644 index 0000000..d2f59cb --- /dev/null +++ b/frontend/src/pages/chat/css/_left-panel.scss @@ -0,0 +1,234 @@ +@use "../../../css/common/colors" as *; +@use "../../../css/common/material" as *; +@use "sass:color"; + +.header { + display: flex; + background-color: $color-dark-surface-container; + color: white; + padding: 16px 16px; + justify-content: end; + width: fit-content; + z-index: 1000; + position: absolute; + top: 0; + right: 0; + + .header-content { + display: flex; + justify-content: space-between; + align-items: center; + + .logo { + font-size: 1.8rem; + font-weight: 700; + display: flex; + align-items: center; + } + + #logouts { + display: none; + list-style: none; + gap: 10px; + + li { + a { + color: white; + text-decoration: none; + font-weight: 500; + transition: all 0.3s ease; + padding: 10px; + border-radius: 10px; + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; + + &:hover { + background-color: rgba(255, 255, 255, 0.2); + } + } + } + } + } +} + +#profile { + display: none; + flex-direction: column; + z-index: 2000; + top: 0; + left: 0; + position: fixed; + height: 100vh; + width: 27%; + background-color: $color-dark-surface-container; + position: relative; + + .profileheader { + display: flex; + gap: 200px; + + p { + color: white; + } + + a { + text-decoration: none; + color: white; + border: solid 2px $color-dark-on-surface-variant; + padding: 5px; + border-radius: 10px; + + &:hover { + background-color: rgba(255, 255, 255, 0.241); + } + } + } +} + +#chat-list { + display: flex; + flex-direction: column; + flex-grow: 0; + width: 40%; + background-color: $color-dark-surface-container; + height: 100%; + z-index: 1000; + min-height: 0; // allow children to manage their own scrolling + + .chat-header-left { + display: flex; + color: white; + font-size: 25px; + background-color: $color-dark-surface-container; + width: 100%; + justify-content: center; + align-items: center; + padding: 16px; + overflow: hidden; + + .product-name { + flex-grow: 1; + } + + .profile { + font-size: 24px; + display: flex; + justify-content: start; + flex-shrink: 0; + flex-grow: 0; + + #closeprofile a { + text-decoration: none; + color: white; + border: solid 2px $color-dark-on-surface-variant; + padding: 5px; + border-radius: 10px; + + &:hover { + background-color: rgba(255, 255, 255, 0.241); + } + } + + img { + $size: 45px; + display: flex; + border-radius: 50%; + width: $size; + height: $size; + } + } + } + + .chat-tabs { + margin-top: 5px; + width: 100%; + height: calc(100% - 80px); + display: flex; + flex-direction: column; + min-height: 0; // prevent flex collapse when inner overflows + --mdui-color-surface: $color-dark-surface-container; + --mdui-color-surface-variant: transparent; + + img { + width: 45px; + height: 45px; + border-radius: 20%; + object-fit: cover; + margin-right: 1rem; + } + + mdui-tabs { + height: 100%; + display: flex; + flex-direction: column; + min-height: 0; // enable inner panel to scroll + } + + mdui-tab-panel[active] { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; // critical to avoid collapsing + overflow: hidden; + } + + mdui-list { + flex: 1; + min-height: 0; // allow scroll area to size correctly + overflow-y: auto; + padding: 0; + margin: 0; + } + } + + mdui-bottom-app-bar { + position: relative; + width: 100%; + padding-left: 16px; + padding-right: 16px; + margin-top: auto; + } +} + +// ChatHeader component styles +.chat-header-left { + .product-name { + font-size: 1.8rem; + font-weight: 700; + background: linear-gradient(45deg, $color-dark-primary, $color-dark-tertiary); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + text-shadow: 0 0 20px rgba($color-dark-primary, 0.5); + } + + .profile { + a { + display: flex; + align-items: center; + text-decoration: none; + transition: transform 0.3s ease; + + &:hover { + transform: scale(1.05); + } + + img { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; + border: 2px solid rgba($color-dark-primary, 0.4); + box-shadow: 0 0 15px rgba($color-dark-primary, 0.3); + transition: all 0.3s ease; + + &:hover { + box-shadow: 0 0 25px rgba($color-dark-primary, 0.5); + border-color: rgba($color-dark-primary, 0.6); + } + } + } + } +} diff --git a/frontend/src/pages/chat/css/_message-reactions.scss b/frontend/src/pages/chat/css/_message-reactions.scss new file mode 100644 index 0000000..5e67b9a --- /dev/null +++ b/frontend/src/pages/chat/css/_message-reactions.scss @@ -0,0 +1,59 @@ +@use "../../../css/common/colors" as *; +@use "../../../css/common/material" as *; +@use "sass:color"; + +// Reaction styles +.message-reactions { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-top: 8px; + margin-left: 10px; + margin-right: 10px; + animation: messageReactionsFadeIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.reaction-button { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border: none; + border-radius: 16px; + background-color: $color-dark-surface-container; + cursor: pointer; + transition: transform 0.2s ease, background-color 0.2s ease; + font-size: 1px; + min-height: 28px; + animation: reactionFadeIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + + &.removing { + animation: reactionFadeOut 0.2s ease forwards; + } + + &:hover { + background-color: $color-dark-surface-container-high; + transform: scale(1.05); + } + + &.reacted { + background-color: $color-dark-primary-container; + border-color: $color-dark-primary; + color: $color-dark-on-primary-container; + + &:hover { + background-color: color.adjust($color-dark-primary-container, $lightness: 20%); + } + } +} + +.reaction-emoji { + font-size: 17px; + line-height: 1; +} + +.reaction-count { + font-size: 12px; + font-weight: 500; + line-height: 1; +} \ No newline at end of file diff --git a/frontend/src/pages/chat/css/_message.scss b/frontend/src/pages/chat/css/_message.scss new file mode 100644 index 0000000..7bb0ecc --- /dev/null +++ b/frontend/src/pages/chat/css/_message.scss @@ -0,0 +1,352 @@ +@use "../../../css/common/colors" as *; +@use "../../../css/common/material" as *; +@use "sass:color"; + +.quote.contextual-content > .quote-inner { + display: flex; + flex-direction: column; + gap: 4px; + + .reply-username { + font-weight: 600; + color: $color-dark-on-surface; + font-size: 0.85rem; + } + + .reply-text { + overflow: hidden; + text-overflow: ellipsis; + } +} + +.message { + margin-bottom: 1rem; + max-width: 70%; + position: relative; + width: fit-content; + display: flex; + align-items: flex-end; + gap: 8px; + + .message-inner { + border-radius: 12px; + position: relative; + word-wrap: break-word; + overflow-wrap: anywhere; + word-break: break-word; + width: fit-content; + max-width: 100%; + display: inline-block; + + .message-profile-pic { + width: 32px; + height: 32px; + flex-shrink: 0; + margin-bottom: 4px; + margin: 10px; + + img { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; + transition: transform 0.2s ease, box-shadow 0.2s ease; + + &:hover { + transform: scale(1.1); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + } + } + } + + .message-username { + font-weight: 600; + margin-bottom: 0.3rem; + font-size: 0.9rem; + transition: color 0.2s ease; + margin: 10px; + + &:hover { + color: $color-dark-primary; + text-decoration: underline; + } + } + + .message-content { + word-wrap: break-word; + margin: 10px 10px 0 10px; + white-space: pre-wrap; + + > p:first-child { + margin-block-start: 0; + } + + > p:last-child { + margin-block-end: 0; + } + } + + .quote.reply-preview { + user-select: none; + margin: 10px; + } + + .message-attachments { + padding: 5px 0 0 0; + overflow: hidden; + + .attachment { + a { + text-decoration: none; + } + + .attachement-image { + max-width: 200px; + border-radius: 8px; + cursor: pointer; + margin-left: 3px; + margin-right: 3px; + margin-bottom: 3px; + + &:last-child { + margin-bottom: 0; + } + + &.loading { + filter: blur(10px); + transition: filter 200ms ease; + } + } + + .attachement-image.placeholder { + background: $color-dark-surface-container-highest; + pointer-events: none; + } + + .image-wrapper { + position: relative; + display: inline-block; + } + + .loading-overlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.08); + backdrop-filter: blur(6px); + border-radius: 8px; + } + + .preload-image { + position: absolute; + width: 0; + height: 0; + opacity: 0; + pointer-events: none; + } + + .with-icon-gap { + display: inline-flex; + align-items: center; + gap: 8px; + } + } + } + + .message-time { + font-size: 0.7rem; + color: $color-dark-on-surface-variant; + margin-top: 0.3rem; + text-align: right; + user-select: none; + margin: 4px 8px 8px 8px; + display: flex; + align-items: center; + justify-content: flex-end; + gap: 4px; + + .message-status-indicator { + display: flex; + align-items: center; + width: 16px; + height: 16px; + + .error-icon { + color: #f44336; + font-size: 16px; + } + + .success-icon { + color: #4caf50; + font-size: 16px; + } + + mdui-circular-progress { + width: 16px; + height: 16px; + } + } + } + } + + &.received .message-inner { + background: $color-dark-surface-container; + color: $color-dark-on-surface; + border-top-left-radius: 5px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + border: 1px solid rgba($color-dark-outline-variant, 0.4); + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba($color-dark-primary, 0.05), rgba($color-dark-tertiary, 0.03)); + pointer-events: none; + z-index: 0; + } + + > * { + position: relative; + z-index: 1; + } + } + + &.received .message-time { + color: $color-dark-on-surface-variant; + font-weight: 500; + + .message-status-indicator { + .success-icon { + color: $color-dark-on-surface-variant; + filter: brightness(1.1); + } + + .error-icon { + color: #ff6b6b; + filter: brightness(1.1); + } + } + } + + &.sent { + margin-left: auto; + flex-direction: row-reverse; + + .message-inner { + background: linear-gradient(135deg, color.adjust($color-dark-primary, $lightness: 8%), color.adjust($color-dark-primary-container, $lightness: 5%)); + color: $color-dark-on-primary; + border-top-right-radius: 5px; + box-shadow: 0 0 20px rgba($color-dark-primary, 0.4); + border: 1px solid rgba($color-dark-primary, 0.5); + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0.08)); + pointer-events: none; + z-index: 0; + } + + > * { + position: relative; + z-index: 1; + } + } + + .message-time { + color: $color-dark-on-primary; + font-weight: 500; + + .message-status-indicator { + .success-icon { + color: $color-dark-on-primary; + filter: brightness(1.2); + } + + .error-icon { + color: #ff6b6b; + filter: brightness(1.2); + } + } + } + } +} + +.message-profile-pic { + img { + width: 40px; + height: 40px; + border-radius: 50%; + object-fit: cover; + border: 2px solid $color-dark-outline; + + &.loading { + opacity: 0.6; + cursor: default; + } + } +} + +.message-username { + &.loading { + opacity: 0.6; + cursor: default; + } +} + +// Fullscreen Image Viewer +.fullscreen-image-overlay { + position: fixed; + inset: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(20px); + z-index: 9999; + opacity: 1; + transition: opacity 0.3s ease; + + &.closing { + opacity: 0; + } + + .fullscreen-animated-image { + position: absolute; + object-fit: contain; + border-radius: 12px; + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4); + transition: left 0.3s ease, top 0.3s ease, width 0.3s ease, height 0.3s ease; + } + + .fullscreen-controls { + position: absolute; + display: flex; + gap: 8px; + + &.top-right { + top: 12px; + right: 12px; + } + } + + .progress-wrapper { + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + } +} diff --git a/frontend/src/pages/chat/css/_profile-dialog.scss b/frontend/src/pages/chat/css/_profile-dialog.scss new file mode 100644 index 0000000..594ed54 --- /dev/null +++ b/frontend/src/pages/chat/css/_profile-dialog.scss @@ -0,0 +1,251 @@ +@use "../../../css/common/colors" as *; +@use "../../../css/common/material" as *; +@use "sass:color"; + +// Profile styles +#profile-dialog .content { + display: flex; + flex-direction: column; + gap: 24px; + min-width: 400px; + + .header-top { + display: flex; + flex-direction: row; + align-items: center; + gap: 16px; + position: relative; + padding-bottom: 16px; + border-bottom: 1px solid $color-dark-outline; + + .profile-picture-container { + position: relative; + $size: 70px; + width: $size; + height: $size; + flex-shrink: 0; + + #profile-picture { + width: $size; + height: $size; + border-radius: 50%; + object-fit: cover; + } + + .upload-overlay { + position: absolute; + bottom: 0; + right: 0; + width: 28px; + height: 28px; + cursor: pointer; + } + } + + mdui-text-field { + flex: 1; + } + } + + #profile-form { + display: flex; + flex-direction: column; + gap: 16px; + + mdui-text-field { + width: 100%; + } + + .dialog-actions { + display: flex; + gap: 12px; + padding-top: 16px; + border-top: 1px solid $color-dark-outline; + + > * { + flex: 1; + } + } + } +} + +// User profile dialog content styles +#user-profile-dialog .content { + display: flex; + gap: 1.5rem; + + .profile-picture-section { + flex-shrink: 0; + + .profile-picture { + width: 80px; + height: 80px; + border-radius: 50%; + object-fit: cover; + border: 2px solid $color-dark-outline; + } + } + + .profile-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 1rem; + + .username-section { + display: flex; + align-items: center; + gap: 0.75rem; + + .username { + margin: 0; + color: $color-dark-on-surface; + font-size: 1.1rem; + font-weight: 600; + } + + .online-status { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.85rem; + padding: 0.25rem 0.5rem; + border-radius: 12px; + font-weight: 500; + + &.online { + color: $success; + background-color: rgba(76, 175, 80, 0.1); + + .online-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: $success; + } + } + + &.offline { + color: $color-dark-on-surface-variant; + background-color: rgba(255, 255, 255, 0.05); + + .offline-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + background-color: $color-dark-on-surface-variant; + } + } + } + } + + .bio-section { + label { + display: block; + color: $color-dark-on-surface-variant; + font-size: 0.85rem; + font-weight: 500; + margin-bottom: 0.5rem; + } + + .bio-display { + color: $color-dark-on-surface; + font-size: 0.9rem; + line-height: 1.4; + padding: 0.75rem; + background-color: $color-dark-surface; + border-radius: 8px; + border: 1px solid $color-dark-outline; + min-height: 60px; + } + + .bio-actions { + display: flex; + gap: 0.5rem; + margin-top: 0.5rem; + } + } + + .profile-stats { + display: flex; + flex-direction: column; + gap: 0.5rem; + + .stat { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0; + + .stat-label { + color: $color-dark-on-surface-variant; + font-size: 0.85rem; + } + + .stat-value { + color: $color-dark-on-surface; + font-size: 0.85rem; + font-weight: 500; + } + } + } + + .profile-actions { + display: flex; + gap: 0.75rem; + margin-top: 0.5rem; + + mdui-button { + flex: 1; + } + } + } +} + +// Cropper Dialog Styles +#cropper-dialog { + .cropper-dialog-content { + display: flex; + flex-direction: column; + gap: 16px; + min-width: 500px; + max-width: 600px; + } + + .cropper-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 16px; + border-bottom: 1px solid $color-dark-outline; + + h3 { + margin: 0; + color: $color-dark-on-surface; + } + } + + .cropper-container { + display: flex; + justify-content: center; + align-items: center; + min-height: 400px; + background: $color-dark-surface-container; + border-radius: 8px; + overflow: hidden; + + #cropper-area { + width: 100%; + height: 100%; + min-height: 400px; + } + } + + .cropper-actions { + display: flex; + gap: 12px; + justify-content: flex-end; + padding-top: 16px; + border-top: 1px solid $color-dark-outline; + } +} diff --git a/frontend/src/pages/chat/css/_right-panel.scss b/frontend/src/pages/chat/css/_right-panel.scss new file mode 100644 index 0000000..23a5503 --- /dev/null +++ b/frontend/src/pages/chat/css/_right-panel.scss @@ -0,0 +1,131 @@ +@use "../../../css/common/colors" as *; +@use "../../../css/common/material" as *; +@use "sass:color"; + +.chat-main { + .chat-header { + padding: 16px; + background: rgba($color-dark-surface-container, 0.8); + backdrop-filter: blur(20px); + display: flex; + align-items: center; + box-shadow: 0 4px 20px rgba($color-dark-primary, 0.1); + position: relative; + z-index: 1; + + .chat-header-avatar { + width: 45px; + height: 45px; + border-radius: 20%; + object-fit: cover; + margin-right: 1rem; + } + + .chat-header-info { + display: flex; + + .info-chat { + display: flex; + flex-direction: column; + + h4 { + font-size: 1.1rem; + margin: 0 0 0.2rem; + } + + p { + margin: 0; + font-size: 0.8rem; + color: #718096; + } + } + + .online-status { + display: inline-block; + width: 10px; + height: 10px; + border-radius: 50%; + background-color: $success; + margin-right: 5px; + } + + a { + display: flex; + flex-direction: row; + text-decoration: none; + color: white; + justify-content: end; + padding: 0; + margin: 0; + position: absolute; + right: 2%; + top: 2%; + + &:hover { + border: none; + } + } + } + } + + .chat-messages { + flex: 1; + padding: 10px 20px; + overflow-y: auto; + position: relative; + z-index: 1; + + &::-webkit-scrollbar { + width: 7px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background-color: $color-dark-surface-container-high; + border-radius: 20px; + } + } + + .file-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + + background: rgba(0, 0, 0, 0.5); + + z-index: 100; + + backdrop-filter: blur(20px); + + .file-overlay-wrapper { + border-radius: 30px; + outline: 3px dashed $color-dark-primary; + outline-offset: -20px; + height: 100%; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + + .file-overlay-inner { + display: flex; + gap: 12px; + align-items: center; + padding: 12px 16px; + background: rgba(18, 18, 18, 0.8); + border: 1px solid $color-dark-surface-container-high; + border-radius: 12px; + color: $color-dark-on-surface; + + mdui-icon { + color: $color-dark-primary; + } + } + } + } +} diff --git a/frontend/src/pages/chat/css/_settings-dialog.scss b/frontend/src/pages/chat/css/_settings-dialog.scss new file mode 100644 index 0000000..a05cf50 --- /dev/null +++ b/frontend/src/pages/chat/css/_settings-dialog.scss @@ -0,0 +1,101 @@ +@use "../../../css/common/colors" as *; +@use "../../../css/common/material" as *; +@use "sass:color"; + +// Settings styles +#settings-dialog { + .fullscreen-wrapper { + display: flex; + align-items: center; + justify-content: center; + height: calc(100vh - (1.5rem * 2)); + width: 100%; + position: relative; + + #settings-dialog-inner { + max-width: 1200px; + max-height: 1000px; + width: 100%; + height: 100%; + + .header { + display: flex; + flex-direction: row; + gap: 10px; + margin-bottom: 16px; + } + + #settings-menu { + display: flex; + flex-direction: row; + gap: 16px; + + mdui-list { + max-width: 280px; + padding-right: 16px; + overflow-y: auto; + } + + .screen { + flex: 1; + overflow-y: auto; + position: relative; + + .settings-panel { + display: flex; + flex-direction: column; + gap: 16px; + opacity: 0; + visibility: hidden; + transform: translateY(20px); + transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s ease; + position: absolute; + top: 0; + left: 0; + width: 100%; + + &.active { + opacity: 1; + visibility: visible; + transform: translateY(0); + position: relative; + } + + h3 { + margin: 0 0 16px 0; + color: $color-dark-on-surface; + } + + mdui-text-field, + mdui-select, + mdui-switch, + mdui-button { + margin-bottom: 8px; + } + + mdui-switch { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 0; + border-bottom: 1px solid $color-dark-outline; + + &:last-child { + border-bottom: none; + } + } + + p { + margin: 8px 0; + color: $color-dark-on-surface-variant; + } + + mdui-linear-progress { + margin: 16px 0; + } + } + } + } + } + } +} diff --git a/frontend/src/pages/chat/css/chat.scss b/frontend/src/pages/chat/css/chat.scss new file mode 100644 index 0000000..fb38357 --- /dev/null +++ b/frontend/src/pages/chat/css/chat.scss @@ -0,0 +1,11 @@ +// Import all component-specific styles +@use "layout"; +@use "left-panel"; +@use "right-panel"; +@use "message"; +@use "chat-input"; +@use "message-reactions"; +@use "context-menu"; +@use "profile-dialog"; +@use "settings-dialog"; +@use "animations"; \ No newline at end of file diff --git a/frontend/src/pages/chat/ui/components/chat/MessageContextMenu.tsx b/frontend/src/pages/chat/ui/components/chat/MessageContextMenu.tsx index 81fe39a..6ccf740 100644 --- a/frontend/src/pages/chat/ui/components/chat/MessageContextMenu.tsx +++ b/frontend/src/pages/chat/ui/components/chat/MessageContextMenu.tsx @@ -274,7 +274,7 @@ export function MessageContextMenu({ className={`context-menu-reaction-bar ${reactionBarPosition} ${isEmojiMenuExpanded ? "expanded" : ""} ${expandUpward ? "expand-upward" : ""}`} style={isEmojiMenuExpanded && !expandUpward ? { position: 'fixed', - top: `${(-(contextMenuHeight || 0) + 5)}px`, + top: `${(-(contextMenuHeight || 0) + 95)}px`, width: '320px', height: '400px', zIndex: 1001 From 546c2523c2b74a6e2af79e5a1df62087b33d01a3 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Thu, 9 Oct 2025 22:16:21 +0300 Subject: [PATCH 03/10] Fix logo location and move service worker --- frontend/index.html | 2 +- frontend/src/pages/chat/utils/push-notifications.ts | 2 +- frontend/src/{pages/chat/service-worker => }/service-worker.ts | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename frontend/src/{pages/chat/service-worker => }/service-worker.ts (100%) diff --git a/frontend/index.html b/frontend/index.html index b52328c..9038e1e 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ Loading... - +
diff --git a/frontend/src/pages/chat/utils/push-notifications.ts b/frontend/src/pages/chat/utils/push-notifications.ts index 2696d30..869d7e5 100644 --- a/frontend/src/pages/chat/utils/push-notifications.ts +++ b/frontend/src/pages/chat/utils/push-notifications.ts @@ -2,7 +2,7 @@ import { API_BASE_URL } from "../core/config"; import { isElectron } from "../electron/electron"; import { websocket } from "../core/websocket"; import type { NewMessageWebSocketMessage, WebSocketMessage } from "../core/types"; -import serviceWorker from "../service-worker/service-worker.ts?worker&url"; +import serviceWorker from "../../../service-worker?worker&url"; export interface PushSubscriptionData { endpoint: string; diff --git a/frontend/src/pages/chat/service-worker/service-worker.ts b/frontend/src/service-worker.ts similarity index 100% rename from frontend/src/pages/chat/service-worker/service-worker.ts rename to frontend/src/service-worker.ts From 2312569455896be444c6739e170092574f75146f Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Thu, 9 Oct 2025 22:19:06 +0300 Subject: [PATCH 04/10] Move utils to root --- frontend/src/api/authApi.ts | 6 +++--- frontend/src/api/dmApi.ts | 8 ++++---- frontend/src/main.tsx | 2 +- frontend/src/pages/auth/LoginPage.tsx | 2 +- frontend/src/pages/chat/core/websocket.ts | 2 +- frontend/src/pages/chat/{utils => }/push-notifications.ts | 8 ++++---- .../src/pages/chat/ui/components/chat/ChatMessages.tsx | 2 +- frontend/src/pages/chat/ui/components/chat/Message.tsx | 8 ++++---- .../pages/chat/ui/components/chat/UserProfileDialog.tsx | 2 +- frontend/src/pages/chat/ui/components/core/Dialog.tsx | 2 +- .../pages/chat/ui/components/settings/SettingsDialog.tsx | 2 +- frontend/src/pages/chat/ui/hooks/useProfile.ts | 2 +- frontend/src/pages/chat/ui/screen/LoginScreen.tsx | 2 +- frontend/src/pages/chat/ui/state.ts | 2 +- frontend/src/{pages/chat => }/utils/crypto/asymmetric.ts | 0 frontend/src/{pages/chat => }/utils/crypto/backup.ts | 0 frontend/src/{pages/chat => }/utils/crypto/kdf.ts | 0 frontend/src/{pages/chat => }/utils/crypto/symmetric.ts | 0 frontend/src/{pages/chat => }/utils/material.ts | 0 frontend/src/{pages/chat => }/utils/notification.ts | 0 frontend/src/{pages/chat => }/utils/utils.ts | 0 21 files changed, 25 insertions(+), 25 deletions(-) rename frontend/src/pages/chat/{utils => }/push-notifications.ts (98%) rename frontend/src/{pages/chat => }/utils/crypto/asymmetric.ts (100%) rename frontend/src/{pages/chat => }/utils/crypto/backup.ts (100%) rename frontend/src/{pages/chat => }/utils/crypto/kdf.ts (100%) rename frontend/src/{pages/chat => }/utils/crypto/symmetric.ts (100%) rename frontend/src/{pages/chat => }/utils/material.ts (100%) rename frontend/src/{pages/chat => }/utils/notification.ts (100%) rename frontend/src/{pages/chat => }/utils/utils.ts (100%) diff --git a/frontend/src/api/authApi.ts b/frontend/src/api/authApi.ts index 86fab4e..d95d6bd 100644 --- a/frontend/src/api/authApi.ts +++ b/frontend/src/api/authApi.ts @@ -1,7 +1,7 @@ import type { Headers, UploadPublicKeyRequest, BackupBlob } from "../pages/chat/core/types"; -import { generateX25519KeyPair } from "../pages/chat/utils/crypto/asymmetric"; -import { encodeBlob, encryptBackupWithPassword, decryptBackupWithPassword, decodeBlob } from "../pages/chat/utils/crypto/backup"; -import { b64, ub64 } from "../pages/chat/utils/utils"; +import { generateX25519KeyPair } from "../utils/crypto/asymmetric"; +import { encodeBlob, encryptBackupWithPassword, decryptBackupWithPassword, decodeBlob } from "../utils/crypto/backup"; +import { b64, ub64 } from "../utils/utils"; import { API_BASE_URL } from "../pages/chat/core/config"; /** diff --git a/frontend/src/api/dmApi.ts b/frontend/src/api/dmApi.ts index 7c195af..e1d906e 100644 --- a/frontend/src/api/dmApi.ts +++ b/frontend/src/api/dmApi.ts @@ -1,12 +1,12 @@ import { API_BASE_URL } from "../pages/chat/core/config"; import { getAuthHeaders } from "./authApi"; -import { ecdhSharedSecret, deriveWrappingKey } from "../pages/chat/utils/crypto/asymmetric"; -import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../pages/chat/utils/crypto/symmetric"; -import { randomBytes } from "../pages/chat/utils/crypto/kdf"; +import { ecdhSharedSecret, deriveWrappingKey } from "../utils/crypto/asymmetric"; +import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../utils/crypto/symmetric"; +import { randomBytes } from "../utils/crypto/kdf"; import { getCurrentKeys } from "./authApi"; import { request } from "../pages/chat/core/websocket"; import type { SendDMRequest, DmEnvelope, User, DMEditRequest, DmEncryptedJSON, BaseDmEnvelope } from "../pages/chat/core/types"; -import { b64, ub64 } from "../pages/chat/utils/utils"; +import { b64, ub64 } from "../utils/utils"; export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string): Promise { const keys = getCurrentKeys(); diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index c7f0e07..9269967 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -8,7 +8,7 @@ import './css/style.scss'; import "mdui/mdui.css"; -import "./pages/chat/utils/material"; +import "./utils/material"; import "./pages/chat/core/init"; import "./pages/chat/electron/electron"; import { createRoot } from 'react-dom/client'; diff --git a/frontend/src/pages/auth/LoginPage.tsx b/frontend/src/pages/auth/LoginPage.tsx index 4bbb165..0a36ecd 100644 --- a/frontend/src/pages/auth/LoginPage.tsx +++ b/frontend/src/pages/auth/LoginPage.tsx @@ -8,7 +8,7 @@ import { useRef } from "react"; import type { TextField } from "mdui/components/text-field"; import { useAppState } from "../chat/ui/state"; import { MaterialTextField } from "../chat/ui/components/core/TextField"; -import { initialize, isSupported, startElectronReceiver, subscribe } from "../chat/utils/push-notifications"; +import { initialize, isSupported, startElectronReceiver, subscribe } from "../chat/push-notifications"; import { isElectron } from "../chat/electron/electron"; import { useNavigate } from "react-router-dom"; import "./auth.scss"; diff --git a/frontend/src/pages/chat/core/websocket.ts b/frontend/src/pages/chat/core/websocket.ts index c881cf3..b08f771 100644 --- a/frontend/src/pages/chat/core/websocket.ts +++ b/frontend/src/pages/chat/core/websocket.ts @@ -7,7 +7,7 @@ import { API_WS_BASE_URL } from "./config"; import type { WebSocketMessage } from "./types"; -import { delay } from "../utils/utils"; +import { delay } from "../../../utils/utils"; /** * Creates a new WebSocket connection to the chat server diff --git a/frontend/src/pages/chat/utils/push-notifications.ts b/frontend/src/pages/chat/push-notifications.ts similarity index 98% rename from frontend/src/pages/chat/utils/push-notifications.ts rename to frontend/src/pages/chat/push-notifications.ts index 869d7e5..c5f7689 100644 --- a/frontend/src/pages/chat/utils/push-notifications.ts +++ b/frontend/src/pages/chat/push-notifications.ts @@ -1,7 +1,7 @@ -import { API_BASE_URL } from "../core/config"; -import { isElectron } from "../electron/electron"; -import { websocket } from "../core/websocket"; -import type { NewMessageWebSocketMessage, WebSocketMessage } from "../core/types"; +import { API_BASE_URL } from "./core/config"; +import { isElectron } from "./electron/electron"; +import { websocket } from "./core/websocket"; +import type { NewMessageWebSocketMessage, WebSocketMessage } from "./core/types"; import serviceWorker from "../../../service-worker?worker&url"; export interface PushSubscriptionData { diff --git a/frontend/src/pages/chat/ui/components/chat/ChatMessages.tsx b/frontend/src/pages/chat/ui/components/chat/ChatMessages.tsx index 43923a9..60df171 100644 --- a/frontend/src/pages/chat/ui/components/chat/ChatMessages.tsx +++ b/frontend/src/pages/chat/ui/components/chat/ChatMessages.tsx @@ -6,7 +6,7 @@ import { UserProfileDialog } from "./UserProfileDialog"; import { MessageContextMenu, type ContextMenuState } from "./MessageContextMenu"; import { fetchUserProfile } from "../../../../../api/profileApi"; import { useEffect, useState, type ReactNode } from "react"; -import { delay } from "../../../utils/utils"; +import { delay } from "../../../../../utils/utils"; import { MaterialDialog } from "../core/Dialog"; import { request } from "../../../core/websocket"; import type { AddReactionRequest, AddDmReactionRequest } from "../../../core/types"; diff --git a/frontend/src/pages/chat/ui/components/chat/Message.tsx b/frontend/src/pages/chat/ui/components/chat/Message.tsx index 1894390..05fe6d0 100644 --- a/frontend/src/pages/chat/ui/components/chat/Message.tsx +++ b/frontend/src/pages/chat/ui/components/chat/Message.tsx @@ -1,4 +1,4 @@ -import { formatTime, id } from "../../../utils/utils"; +import { formatTime, id } from "../../../../../utils/utils"; import type { Attachment, Message as MessageType } from "../../../core/types"; import defaultAvatar from "../../../../../images/default-avatar.png"; import Quote from "../core/Quote"; @@ -6,11 +6,11 @@ import { parse } from "marked"; import DOMPurify from "dompurify"; import { useEffect, useState, useRef } from "react"; import { getCurrentKeys } from "../../../../../api/authApi"; -import { ecdhSharedSecret, deriveWrappingKey } from "../../../utils/crypto/asymmetric"; -import { importAesGcmKey, aesGcmDecrypt } from "../../../utils/crypto/symmetric"; +import { ecdhSharedSecret, deriveWrappingKey } from "../../../../../utils/crypto/asymmetric"; +import { importAesGcmKey, aesGcmDecrypt } from "../../../../../utils/crypto/symmetric"; import { getAuthHeaders } from "../../../../../api/authApi"; import { useAppState } from "../../state"; -import { ub64 } from "../../../utils/utils"; +import { ub64 } from "../../../../../utils/utils"; import { useImmer } from "use-immer"; import { createPortal } from "react-dom"; import { MessageReactions } from "./MessageReactions"; diff --git a/frontend/src/pages/chat/ui/components/chat/UserProfileDialog.tsx b/frontend/src/pages/chat/ui/components/chat/UserProfileDialog.tsx index ee77e28..1861f7c 100644 --- a/frontend/src/pages/chat/ui/components/chat/UserProfileDialog.tsx +++ b/frontend/src/pages/chat/ui/components/chat/UserProfileDialog.tsx @@ -1,7 +1,7 @@ import type { DialogProps } from "../../../core/types"; import type { UserProfile } from "../../../core/types"; import { MaterialDialog } from "../core/Dialog"; -import { formatTime } from "../../../utils/utils"; +import { formatTime } from "../../../../../utils/utils"; import defaultAvatar from "../../../../../images/default-avatar.png"; interface UserProfileDialogProps extends DialogProps { diff --git a/frontend/src/pages/chat/ui/components/core/Dialog.tsx b/frontend/src/pages/chat/ui/components/core/Dialog.tsx index d339d7f..43d1923 100644 --- a/frontend/src/pages/chat/ui/components/core/Dialog.tsx +++ b/frontend/src/pages/chat/ui/components/core/Dialog.tsx @@ -1,7 +1,7 @@ import type { Dialog as MduiDialog } from "mdui/components/dialog"; import { useEffect, type Ref } from "react" import { createPortal } from "react-dom"; -import { id } from "../../../utils/utils"; +import { id } from "../../../../../utils/utils"; import useCombinedRefs from "../../hooks/useCombinedRefs"; export interface BaseDialogProps { diff --git a/frontend/src/pages/chat/ui/components/settings/SettingsDialog.tsx b/frontend/src/pages/chat/ui/components/settings/SettingsDialog.tsx index 9a1ead0..7cf21eb 100644 --- a/frontend/src/pages/chat/ui/components/settings/SettingsDialog.tsx +++ b/frontend/src/pages/chat/ui/components/settings/SettingsDialog.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; 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 "../../../utils/push-notifications"; +import { initialize, isSupported, startElectronReceiver, stopElectronReceiver, subscribe, unsubscribe } from "../../../push-notifications"; import { isElectron } from "../../../electron/electron"; import { useAppState } from "../../state"; import type { Switch } from "mdui/components/switch"; diff --git a/frontend/src/pages/chat/ui/hooks/useProfile.ts b/frontend/src/pages/chat/ui/hooks/useProfile.ts index 9cad0a2..de890e6 100644 --- a/frontend/src/pages/chat/ui/hooks/useProfile.ts +++ b/frontend/src/pages/chat/ui/hooks/useProfile.ts @@ -1,7 +1,7 @@ import { useState, useCallback, useEffect } from "react"; import { useAppState } from "../state"; import { loadProfile, updateProfile, uploadProfilePicture, type ProfileData } from "../../../../api/profileApi"; -import { showSuccess, showError } from "../../utils/notification"; +import { showSuccess, showError } from "../../../../utils/notification"; export default function useProfile() { const { user } = useAppState(); diff --git a/frontend/src/pages/chat/ui/screen/LoginScreen.tsx b/frontend/src/pages/chat/ui/screen/LoginScreen.tsx index c6e5656..85adff9 100644 --- a/frontend/src/pages/chat/ui/screen/LoginScreen.tsx +++ b/frontend/src/pages/chat/ui/screen/LoginScreen.tsx @@ -9,7 +9,7 @@ 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 "../../utils/push-notifications"; +import { initialize, isSupported, startElectronReceiver, subscribe } from "../../push-notifications"; import { isElectron } from "../../electron/electron"; export default function LoginScreen() { diff --git a/frontend/src/pages/chat/ui/state.ts b/frontend/src/pages/chat/ui/state.ts index c9b9e79..2b011af 100644 --- a/frontend/src/pages/chat/ui/state.ts +++ b/frontend/src/pages/chat/ui/state.ts @@ -7,7 +7,7 @@ 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 "../utils/push-notifications"; +import { initialize, subscribe, startElectronReceiver, isSupported } from "../push-notifications"; import { isElectron } from "../electron/electron"; export type ChatTabs = "chats" | "channels" | "contacts" | "dms" diff --git a/frontend/src/pages/chat/utils/crypto/asymmetric.ts b/frontend/src/utils/crypto/asymmetric.ts similarity index 100% rename from frontend/src/pages/chat/utils/crypto/asymmetric.ts rename to frontend/src/utils/crypto/asymmetric.ts diff --git a/frontend/src/pages/chat/utils/crypto/backup.ts b/frontend/src/utils/crypto/backup.ts similarity index 100% rename from frontend/src/pages/chat/utils/crypto/backup.ts rename to frontend/src/utils/crypto/backup.ts diff --git a/frontend/src/pages/chat/utils/crypto/kdf.ts b/frontend/src/utils/crypto/kdf.ts similarity index 100% rename from frontend/src/pages/chat/utils/crypto/kdf.ts rename to frontend/src/utils/crypto/kdf.ts diff --git a/frontend/src/pages/chat/utils/crypto/symmetric.ts b/frontend/src/utils/crypto/symmetric.ts similarity index 100% rename from frontend/src/pages/chat/utils/crypto/symmetric.ts rename to frontend/src/utils/crypto/symmetric.ts diff --git a/frontend/src/pages/chat/utils/material.ts b/frontend/src/utils/material.ts similarity index 100% rename from frontend/src/pages/chat/utils/material.ts rename to frontend/src/utils/material.ts diff --git a/frontend/src/pages/chat/utils/notification.ts b/frontend/src/utils/notification.ts similarity index 100% rename from frontend/src/pages/chat/utils/notification.ts rename to frontend/src/utils/notification.ts diff --git a/frontend/src/pages/chat/utils/utils.ts b/frontend/src/utils/utils.ts similarity index 100% rename from frontend/src/pages/chat/utils/utils.ts rename to frontend/src/utils/utils.ts From 0ccade3a91c76dbc4e6dbe66acf41d8002b7edd5 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Thu, 9 Oct 2025 22:32:00 +0300 Subject: [PATCH 05/10] Restructure CSS --- frontend/src/App.tsx | 4 +- frontend/src/{ => core}/api/authApi.ts | 10 +- frontend/src/{ => core}/api/dmApi.ts | 14 +- frontend/src/{ => core}/api/profileApi.ts | 4 +- frontend/src/{pages/chat => }/core/config.ts | 0 .../chat => core}/electron/electron.scss | 2 +- .../{pages/chat => core}/electron/electron.ts | 0 frontend/src/{pages/chat => }/core/init.ts | 0 .../push-notifications}/push-notifications.ts | 10 +- .../push-notifications}/service-worker.ts | 0 frontend/src/{pages/chat => }/core/types.d.ts | 0 .../src/{pages/chat => }/core/websocket.ts | 2 +- .../src/css/{common => }/_animations.scss | 0 frontend/src/css/{common => }/_colors.scss | 0 .../src/css/{common => }/_components.scss | 71 --------- frontend/src/css/{common => }/_material.scss | 0 .../css/{lib => }/fonts/material-symbols.scss | 0 .../{lib => }/fonts/material-symbols.woff2 | Bin .../src/css/{lib => }/fonts/montserrat.scss | 0 .../JTUSjIg1_i6t8kCHKm459W1hyyTh89ZNpQ.woff2 | Bin .../JTUSjIg1_i6t8kCHKm459WRhyyTh89ZNpQ.woff2 | Bin .../JTUSjIg1_i6t8kCHKm459WZhyyTh89ZNpQ.woff2 | Bin .../JTUSjIg1_i6t8kCHKm459WdhyyTh89ZNpQ.woff2 | Bin .../JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2 | Bin frontend/src/css/style.scss | 12 +- frontend/src/main.tsx | 4 +- frontend/src/pages/auth/LoginPage.tsx | 10 +- frontend/src/pages/auth/RegisterPage.tsx | 6 +- frontend/src/pages/auth/auth.scss | 4 +- frontend/src/pages/chat/css/_animations.scss | 4 +- frontend/src/pages/chat/css/_chat-input.scss | 4 +- .../src/pages/chat/css/_context-menu.scss | 4 +- frontend/src/pages/chat/css/_layout.scss | 4 +- frontend/src/pages/chat/css/_left-panel.scss | 4 +- .../pages/chat/css/_message-reactions.scss | 4 +- frontend/src/pages/chat/css/_message.scss | 4 +- .../src/pages/chat/css/_profile-dialog.scss | 4 +- frontend/src/pages/chat/css/_right-panel.scss | 4 +- .../src/pages/chat/css/_settings-dialog.scss | 4 +- .../src/pages/chat/ui/components/Electron.tsx | 4 +- .../chat/ui/components/chat/ChatHeader.tsx | 2 +- .../ui/components/chat/ChatInputWrapper.tsx | 2 +- .../chat/ui/components/chat/ChatMessages.tsx | 10 +- .../chat/ui/components/chat/DMUsersList.tsx | 2 +- .../chat/ui/components/chat/EmojiMenu.tsx | 2 +- .../chat/ui/components/chat/LeftPanel.tsx | 2 +- .../pages/chat/ui/components/chat/Message.tsx | 6 +- .../ui/components/chat/MessageContextMenu.tsx | 2 +- .../components/chat/MessagePanelRenderer.tsx | 4 +- .../ui/components/chat/MessageReactions.tsx | 2 +- .../ui/components/chat/ReplyMessageDialog.tsx | 2 +- .../ui/components/chat/UserProfileDialog.tsx | 4 +- .../ui/components/profile/ImageCropper.tsx | 2 +- .../ui/components/profile/ProfileDialog.tsx | 2 +- .../ui/components/settings/SettingsDialog.tsx | 10 +- frontend/src/pages/chat/ui/hooks/useDM.ts | 6 +- .../src/pages/chat/ui/hooks/useProfile.ts | 2 +- frontend/src/pages/chat/ui/panels/DMPanel.ts | 4 +- .../src/pages/chat/ui/panels/MessagePanel.ts | 2 +- .../pages/chat/ui/panels/PublicChatPanel.ts | 8 +- .../src/pages/chat/ui/screen/ChatScreen.tsx | 13 -- .../chat/ui/screen/DownloadAppScreen.tsx | 25 --- .../src/pages/chat/ui/screen/LoginScreen.tsx | 142 ----------------- .../pages/chat/ui/screen/RegisterScreen.tsx | 148 ------------------ frontend/src/pages/chat/ui/state.ts | 14 +- frontend/src/pages/home/HomePage.tsx | 2 +- frontend/src/pages/home/home.scss | 2 +- 67 files changed, 110 insertions(+), 509 deletions(-) rename frontend/src/{ => core}/api/authApi.ts (94%) rename frontend/src/{ => core}/api/dmApi.ts (94%) rename frontend/src/{ => core}/api/profileApi.ts (96%) rename frontend/src/{pages/chat => }/core/config.ts (100%) rename frontend/src/{pages/chat => core}/electron/electron.scss (94%) rename frontend/src/{pages/chat => core}/electron/electron.ts (100%) rename frontend/src/{pages/chat => }/core/init.ts (100%) rename frontend/src/{pages/chat => core/push-notifications}/push-notifications.ts (97%) rename frontend/src/{ => core/push-notifications}/service-worker.ts (100%) rename frontend/src/{pages/chat => }/core/types.d.ts (100%) rename frontend/src/{pages/chat => }/core/websocket.ts (98%) rename frontend/src/css/{common => }/_animations.scss (100%) rename frontend/src/css/{common => }/_colors.scss (100%) rename frontend/src/css/{common => }/_components.scss (50%) rename frontend/src/css/{common => }/_material.scss (100%) rename frontend/src/css/{lib => }/fonts/material-symbols.scss (100%) rename frontend/src/css/{lib => }/fonts/material-symbols.woff2 (100%) rename frontend/src/css/{lib => }/fonts/montserrat.scss (100%) rename frontend/src/css/{lib => }/fonts/montserrat/JTUSjIg1_i6t8kCHKm459W1hyyTh89ZNpQ.woff2 (100%) rename frontend/src/css/{lib => }/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WRhyyTh89ZNpQ.woff2 (100%) rename frontend/src/css/{lib => }/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WZhyyTh89ZNpQ.woff2 (100%) rename frontend/src/css/{lib => }/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WdhyyTh89ZNpQ.woff2 (100%) rename frontend/src/css/{lib => }/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2 (100%) delete mode 100644 frontend/src/pages/chat/ui/screen/ChatScreen.tsx delete mode 100644 frontend/src/pages/chat/ui/screen/DownloadAppScreen.tsx delete mode 100644 frontend/src/pages/chat/ui/screen/LoginScreen.tsx delete mode 100644 frontend/src/pages/chat/ui/screen/RegisterScreen.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2e07646..696ed8c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,8 +9,8 @@ import ChatPage from "./pages/chat/ChatPage"; import DownloadAppPage from "./pages/download-app/DownloadAppPage"; import NotFoundPage from "./pages/not-found/NotFoundPage"; import ProtectedRoute from "./pages/ProtectedRoute"; -import { isElectron } from "./pages/chat/electron/electron"; -import { MINIMUM_WIDTH } from "./pages/chat/core/config"; +import { isElectron } from "./core/electron/electron"; +import { MINIMUM_WIDTH } from "./core/config"; import useWindowSize from "./pages/chat/ui/hooks/useWindowSize"; export default function App() { diff --git a/frontend/src/api/authApi.ts b/frontend/src/core/api/authApi.ts similarity index 94% rename from frontend/src/api/authApi.ts rename to frontend/src/core/api/authApi.ts index d95d6bd..a104e4f 100644 --- a/frontend/src/api/authApi.ts +++ b/frontend/src/core/api/authApi.ts @@ -1,8 +1,8 @@ -import type { Headers, UploadPublicKeyRequest, BackupBlob } from "../pages/chat/core/types"; -import { generateX25519KeyPair } from "../utils/crypto/asymmetric"; -import { encodeBlob, encryptBackupWithPassword, decryptBackupWithPassword, decodeBlob } from "../utils/crypto/backup"; -import { b64, ub64 } from "../utils/utils"; -import { API_BASE_URL } from "../pages/chat/core/config"; +import type { Headers, UploadPublicKeyRequest, BackupBlob } from "../types"; +import { generateX25519KeyPair } from "../../utils/crypto/asymmetric"; +import { encodeBlob, encryptBackupWithPassword, decryptBackupWithPassword, decodeBlob } from "../../utils/crypto/backup"; +import { b64, ub64 } from "../../utils/utils"; +import { API_BASE_URL } from "../config"; /** * Generates authentication headers for API requests diff --git a/frontend/src/api/dmApi.ts b/frontend/src/core/api/dmApi.ts similarity index 94% rename from frontend/src/api/dmApi.ts rename to frontend/src/core/api/dmApi.ts index e1d906e..59efea4 100644 --- a/frontend/src/api/dmApi.ts +++ b/frontend/src/core/api/dmApi.ts @@ -1,12 +1,12 @@ -import { API_BASE_URL } from "../pages/chat/core/config"; +import { API_BASE_URL } from "../config"; import { getAuthHeaders } from "./authApi"; -import { ecdhSharedSecret, deriveWrappingKey } from "../utils/crypto/asymmetric"; -import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../utils/crypto/symmetric"; -import { randomBytes } from "../utils/crypto/kdf"; +import { ecdhSharedSecret, deriveWrappingKey } from "../../utils/crypto/asymmetric"; +import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../../utils/crypto/symmetric"; +import { randomBytes } from "../../utils/crypto/kdf"; import { getCurrentKeys } from "./authApi"; -import { request } from "../pages/chat/core/websocket"; -import type { SendDMRequest, DmEnvelope, User, DMEditRequest, DmEncryptedJSON, BaseDmEnvelope } from "../pages/chat/core/types"; -import { b64, ub64 } from "../utils/utils"; +import { request } from "../websocket"; +import type { SendDMRequest, DmEnvelope, User, DMEditRequest, DmEncryptedJSON, BaseDmEnvelope } from "../types"; +import { b64, ub64 } from "../../utils/utils"; export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string): Promise { const keys = getCurrentKeys(); diff --git a/frontend/src/api/profileApi.ts b/frontend/src/core/api/profileApi.ts similarity index 96% rename from frontend/src/api/profileApi.ts rename to frontend/src/core/api/profileApi.ts index 85c382b..9755bdd 100644 --- a/frontend/src/api/profileApi.ts +++ b/frontend/src/core/api/profileApi.ts @@ -1,6 +1,6 @@ import { getAuthHeaders } from "./authApi"; -import { API_BASE_URL } from "../pages/chat/core/config"; -import type { UserProfile } from "../pages/chat/core/types"; +import { API_BASE_URL } from "../config"; +import type { UserProfile } from "../types"; export interface ProfileData { profile_picture?: string; diff --git a/frontend/src/pages/chat/core/config.ts b/frontend/src/core/config.ts similarity index 100% rename from frontend/src/pages/chat/core/config.ts rename to frontend/src/core/config.ts diff --git a/frontend/src/pages/chat/electron/electron.scss b/frontend/src/core/electron/electron.scss similarity index 94% rename from frontend/src/pages/chat/electron/electron.scss rename to frontend/src/core/electron/electron.scss index 3d5f8e9..9f45831 100644 --- a/frontend/src/pages/chat/electron/electron.scss +++ b/frontend/src/core/electron/electron.scss @@ -1,4 +1,4 @@ -@use "../../../css/common/material" as *; +@use "../../css/material" as *; #electron-title-bar { display: none; diff --git a/frontend/src/pages/chat/electron/electron.ts b/frontend/src/core/electron/electron.ts similarity index 100% rename from frontend/src/pages/chat/electron/electron.ts rename to frontend/src/core/electron/electron.ts diff --git a/frontend/src/pages/chat/core/init.ts b/frontend/src/core/init.ts similarity index 100% rename from frontend/src/pages/chat/core/init.ts rename to frontend/src/core/init.ts diff --git a/frontend/src/pages/chat/push-notifications.ts b/frontend/src/core/push-notifications/push-notifications.ts similarity index 97% rename from frontend/src/pages/chat/push-notifications.ts rename to frontend/src/core/push-notifications/push-notifications.ts index c5f7689..e3edd2b 100644 --- a/frontend/src/pages/chat/push-notifications.ts +++ b/frontend/src/core/push-notifications/push-notifications.ts @@ -1,8 +1,8 @@ -import { API_BASE_URL } from "./core/config"; -import { isElectron } from "./electron/electron"; -import { websocket } from "./core/websocket"; -import type { NewMessageWebSocketMessage, WebSocketMessage } from "./core/types"; -import serviceWorker from "../../../service-worker?worker&url"; +import { API_BASE_URL } from "../config"; +import { isElectron } from "../electron/electron"; +import { websocket } from "../websocket"; +import type { NewMessageWebSocketMessage, WebSocketMessage } from "../types"; +import serviceWorker from "./service-worker?worker&url"; export interface PushSubscriptionData { endpoint: string; diff --git a/frontend/src/service-worker.ts b/frontend/src/core/push-notifications/service-worker.ts similarity index 100% rename from frontend/src/service-worker.ts rename to frontend/src/core/push-notifications/service-worker.ts diff --git a/frontend/src/pages/chat/core/types.d.ts b/frontend/src/core/types.d.ts similarity index 100% rename from frontend/src/pages/chat/core/types.d.ts rename to frontend/src/core/types.d.ts diff --git a/frontend/src/pages/chat/core/websocket.ts b/frontend/src/core/websocket.ts similarity index 98% rename from frontend/src/pages/chat/core/websocket.ts rename to frontend/src/core/websocket.ts index b08f771..c881cf3 100644 --- a/frontend/src/pages/chat/core/websocket.ts +++ b/frontend/src/core/websocket.ts @@ -7,7 +7,7 @@ import { API_WS_BASE_URL } from "./config"; import type { WebSocketMessage } from "./types"; -import { delay } from "../../../utils/utils"; +import { delay } from "../utils/utils"; /** * Creates a new WebSocket connection to the chat server diff --git a/frontend/src/css/common/_animations.scss b/frontend/src/css/_animations.scss similarity index 100% rename from frontend/src/css/common/_animations.scss rename to frontend/src/css/_animations.scss diff --git a/frontend/src/css/common/_colors.scss b/frontend/src/css/_colors.scss similarity index 100% rename from frontend/src/css/common/_colors.scss rename to frontend/src/css/_colors.scss diff --git a/frontend/src/css/common/_components.scss b/frontend/src/css/_components.scss similarity index 50% rename from frontend/src/css/common/_components.scss rename to frontend/src/css/_components.scss index 4567775..03bb64c 100644 --- a/frontend/src/css/common/_components.scss +++ b/frontend/src/css/_components.scss @@ -30,77 +30,6 @@ button, input { font: inherit; } -.context-menu { - position: relative; - background-color: $color-dark-surface-container; - border-radius: 8px; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); - padding: 0.5rem 0; - z-index: 1000; - display: block; - min-width: 150px; - max-width: 200px; - white-space: nowrap; - user-select: none; - - .context-menu-item { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.75rem 1rem; - cursor: pointer; - color: $color-dark-on-surface; - transition: background-color 0.2s ease; - font-size: 0.9rem; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - - &:hover { - background-color: rgba(255, 255, 255, 0.1); - } - - .material-symbols { - font-size: 1.1rem; - flex-shrink: 0; - } - } - - &.pos-top-left { - transform-origin: top right; - } - &.pos-top-right { - transform-origin: top left; - } - &.pos-bottom-left { - transform-origin: bottom right; - } - &.pos-bottom-right { - transform-origin: bottom left; - } - - &.open { - animation: context-menu-open 0.25s ease; - } - - &.faded { - opacity: 0; - transition: opacity 0.3s ease-out; - } - - @keyframes context-menu-open { - 0% { - opacity: 0; - transform: scale(0.5); - } - - 100% { - opacity: 1; - transform: scale(1); - } - } -} - // Dialog content styles .dialog-content { h3 { diff --git a/frontend/src/css/common/_material.scss b/frontend/src/css/_material.scss similarity index 100% rename from frontend/src/css/common/_material.scss rename to frontend/src/css/_material.scss diff --git a/frontend/src/css/lib/fonts/material-symbols.scss b/frontend/src/css/fonts/material-symbols.scss similarity index 100% rename from frontend/src/css/lib/fonts/material-symbols.scss rename to frontend/src/css/fonts/material-symbols.scss diff --git a/frontend/src/css/lib/fonts/material-symbols.woff2 b/frontend/src/css/fonts/material-symbols.woff2 similarity index 100% rename from frontend/src/css/lib/fonts/material-symbols.woff2 rename to frontend/src/css/fonts/material-symbols.woff2 diff --git a/frontend/src/css/lib/fonts/montserrat.scss b/frontend/src/css/fonts/montserrat.scss similarity index 100% rename from frontend/src/css/lib/fonts/montserrat.scss rename to frontend/src/css/fonts/montserrat.scss diff --git a/frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459W1hyyTh89ZNpQ.woff2 b/frontend/src/css/fonts/montserrat/JTUSjIg1_i6t8kCHKm459W1hyyTh89ZNpQ.woff2 similarity index 100% rename from frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459W1hyyTh89ZNpQ.woff2 rename to frontend/src/css/fonts/montserrat/JTUSjIg1_i6t8kCHKm459W1hyyTh89ZNpQ.woff2 diff --git a/frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WRhyyTh89ZNpQ.woff2 b/frontend/src/css/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WRhyyTh89ZNpQ.woff2 similarity index 100% rename from frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WRhyyTh89ZNpQ.woff2 rename to frontend/src/css/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WRhyyTh89ZNpQ.woff2 diff --git a/frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WZhyyTh89ZNpQ.woff2 b/frontend/src/css/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WZhyyTh89ZNpQ.woff2 similarity index 100% rename from frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WZhyyTh89ZNpQ.woff2 rename to frontend/src/css/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WZhyyTh89ZNpQ.woff2 diff --git a/frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WdhyyTh89ZNpQ.woff2 b/frontend/src/css/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WdhyyTh89ZNpQ.woff2 similarity index 100% rename from frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WdhyyTh89ZNpQ.woff2 rename to frontend/src/css/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WdhyyTh89ZNpQ.woff2 diff --git a/frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2 b/frontend/src/css/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2 similarity index 100% rename from frontend/src/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2 rename to frontend/src/css/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2 diff --git a/frontend/src/css/style.scss b/frontend/src/css/style.scss index b490119..9ab39fe 100644 --- a/frontend/src/css/style.scss +++ b/frontend/src/css/style.scss @@ -1,10 +1,10 @@ -@use "common/animations"; -@use "common/components"; -@use "common/colors" as *; -@use "common/material" as *; +@use "animations"; +@use "components"; +@use "colors" as *; +@use "material" as *; -@use "lib/fonts/montserrat"; -@use "lib/fonts/material-symbols"; +@use "fonts/montserrat"; +@use "fonts/material-symbols"; * { diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 9269967..d89d1e8 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -9,8 +9,8 @@ import './css/style.scss'; import "mdui/mdui.css"; import "./utils/material"; -import "./pages/chat/core/init"; -import "./pages/chat/electron/electron"; +import "./core/init"; +import "./core/electron/electron"; import { createRoot } from 'react-dom/client'; import App from './App'; import { StrictMode } from 'react'; diff --git a/frontend/src/pages/auth/LoginPage.tsx b/frontend/src/pages/auth/LoginPage.tsx index 0a36ecd..be27b3a 100644 --- a/frontend/src/pages/auth/LoginPage.tsx +++ b/frontend/src/pages/auth/LoginPage.tsx @@ -1,15 +1,15 @@ import { useImmer } from "use-immer"; import { AlertsContainer, type Alert, type AlertType } from "../chat/ui/components/Alerts"; import { AuthContainer, AuthHeader } from "../chat/ui/components/Auth"; -import type { ErrorResponse, LoginRequest, LoginResponse } from "../chat/core/types"; -import { ensureKeysOnLogin } from "../../api/authApi"; -import { API_BASE_URL } from "../chat/core/config"; +import type { ErrorResponse, LoginRequest, LoginResponse } from "../../core/types"; +import { ensureKeysOnLogin } from "../../core/api/authApi"; +import { API_BASE_URL } from "../../core/config"; import { useRef } from "react"; import type { TextField } from "mdui/components/text-field"; import { useAppState } from "../chat/ui/state"; import { MaterialTextField } from "../chat/ui/components/core/TextField"; -import { initialize, isSupported, startElectronReceiver, subscribe } from "../chat/push-notifications"; -import { isElectron } from "../chat/electron/electron"; +import { initialize, isSupported, startElectronReceiver, subscribe } from "../../core/push-notifications/push-notifications"; +import { isElectron } from "../../core/electron/electron"; import { useNavigate } from "react-router-dom"; import "./auth.scss"; diff --git a/frontend/src/pages/auth/RegisterPage.tsx b/frontend/src/pages/auth/RegisterPage.tsx index ec2b906..1bddf1e 100644 --- a/frontend/src/pages/auth/RegisterPage.tsx +++ b/frontend/src/pages/auth/RegisterPage.tsx @@ -3,11 +3,11 @@ import { AuthContainer, AuthHeader } from "../chat/ui/components/Auth"; import { AlertsContainer, type Alert, type AlertType } from "../chat/ui/components/Alerts"; import { useRef } from "react"; import { TextField } from "mdui/components/text-field"; -import type { ErrorResponse, RegisterRequest, LoginResponse } from "../chat/core/types"; -import { API_BASE_URL } from "../chat/core/config"; +import type { ErrorResponse, RegisterRequest, LoginResponse } from "../../core/types"; +import { API_BASE_URL } from "../../core/config"; import { useAppState } from "../chat/ui/state"; import { MaterialTextField } from "../chat/ui/components/core/TextField"; -import { ensureKeysOnLogin } from "../../api/authApi"; +import { ensureKeysOnLogin } from "../../core/api/authApi"; import { useNavigate } from "react-router-dom"; import "./auth.scss"; diff --git a/frontend/src/pages/auth/auth.scss b/frontend/src/pages/auth/auth.scss index aea00ca..2427d3c 100644 --- a/frontend/src/pages/auth/auth.scss +++ b/frontend/src/pages/auth/auth.scss @@ -1,5 +1,5 @@ -@use "../../css/common/colors" as *; -@use "../../css/common/material" as *; +@use "../../css/colors" as *; +@use "../../css/material" as *; .auth-container { display: flex; diff --git a/frontend/src/pages/chat/css/_animations.scss b/frontend/src/pages/chat/css/_animations.scss index 14b01c6..d837774 100644 --- a/frontend/src/pages/chat/css/_animations.scss +++ b/frontend/src/pages/chat/css/_animations.scss @@ -1,5 +1,5 @@ -@use "../../../css/common/colors" as *; -@use "../../../css/common/material" as *; +@use "../../../css/colors" as *; +@use "../../../css/material" as *; @use "sass:color"; // Animation for reactions appearing/disappearing diff --git a/frontend/src/pages/chat/css/_chat-input.scss b/frontend/src/pages/chat/css/_chat-input.scss index bea62bf..9ac5779 100644 --- a/frontend/src/pages/chat/css/_chat-input.scss +++ b/frontend/src/pages/chat/css/_chat-input.scss @@ -1,5 +1,5 @@ -@use "../../../css/common/colors" as *; -@use "../../../css/common/material" as *; +@use "../../../css/colors" as *; +@use "../../../css/material" as *; @use "sass:color"; .chat-input-wrapper { diff --git a/frontend/src/pages/chat/css/_context-menu.scss b/frontend/src/pages/chat/css/_context-menu.scss index 60b4bba..1720e46 100644 --- a/frontend/src/pages/chat/css/_context-menu.scss +++ b/frontend/src/pages/chat/css/_context-menu.scss @@ -1,5 +1,5 @@ -@use "../../../css/common/colors" as *; -@use "../../../css/common/material" as *; +@use "../../../css/colors" as *; +@use "../../../css/material" as *; @use "sass:color"; // Reaction bar styles (standalone) diff --git a/frontend/src/pages/chat/css/_layout.scss b/frontend/src/pages/chat/css/_layout.scss index 720a0d4..b64bea8 100644 --- a/frontend/src/pages/chat/css/_layout.scss +++ b/frontend/src/pages/chat/css/_layout.scss @@ -1,5 +1,5 @@ -@use "../../../css/common/colors" as *; -@use "../../../css/common/material" as *; +@use "../../../css/colors" as *; +@use "../../../css/material" as *; @use "sass:color"; #chat-interface { diff --git a/frontend/src/pages/chat/css/_left-panel.scss b/frontend/src/pages/chat/css/_left-panel.scss index d2f59cb..1c47a74 100644 --- a/frontend/src/pages/chat/css/_left-panel.scss +++ b/frontend/src/pages/chat/css/_left-panel.scss @@ -1,5 +1,5 @@ -@use "../../../css/common/colors" as *; -@use "../../../css/common/material" as *; +@use "../../../css/colors" as *; +@use "../../../css/material" as *; @use "sass:color"; .header { diff --git a/frontend/src/pages/chat/css/_message-reactions.scss b/frontend/src/pages/chat/css/_message-reactions.scss index 5e67b9a..7225cc2 100644 --- a/frontend/src/pages/chat/css/_message-reactions.scss +++ b/frontend/src/pages/chat/css/_message-reactions.scss @@ -1,5 +1,5 @@ -@use "../../../css/common/colors" as *; -@use "../../../css/common/material" as *; +@use "../../../css/colors" as *; +@use "../../../css/material" as *; @use "sass:color"; // Reaction styles diff --git a/frontend/src/pages/chat/css/_message.scss b/frontend/src/pages/chat/css/_message.scss index 7bb0ecc..79556fa 100644 --- a/frontend/src/pages/chat/css/_message.scss +++ b/frontend/src/pages/chat/css/_message.scss @@ -1,5 +1,5 @@ -@use "../../../css/common/colors" as *; -@use "../../../css/common/material" as *; +@use "../../../css/colors" as *; +@use "../../../css/material" as *; @use "sass:color"; .quote.contextual-content > .quote-inner { diff --git a/frontend/src/pages/chat/css/_profile-dialog.scss b/frontend/src/pages/chat/css/_profile-dialog.scss index 594ed54..b5315c9 100644 --- a/frontend/src/pages/chat/css/_profile-dialog.scss +++ b/frontend/src/pages/chat/css/_profile-dialog.scss @@ -1,5 +1,5 @@ -@use "../../../css/common/colors" as *; -@use "../../../css/common/material" as *; +@use "../../../css/colors" as *; +@use "../../../css/material" as *; @use "sass:color"; // Profile styles diff --git a/frontend/src/pages/chat/css/_right-panel.scss b/frontend/src/pages/chat/css/_right-panel.scss index 23a5503..f6ff746 100644 --- a/frontend/src/pages/chat/css/_right-panel.scss +++ b/frontend/src/pages/chat/css/_right-panel.scss @@ -1,5 +1,5 @@ -@use "../../../css/common/colors" as *; -@use "../../../css/common/material" as *; +@use "../../../css/colors" as *; +@use "../../../css/material" as *; @use "sass:color"; .chat-main { diff --git a/frontend/src/pages/chat/css/_settings-dialog.scss b/frontend/src/pages/chat/css/_settings-dialog.scss index a05cf50..fca04b6 100644 --- a/frontend/src/pages/chat/css/_settings-dialog.scss +++ b/frontend/src/pages/chat/css/_settings-dialog.scss @@ -1,5 +1,5 @@ -@use "../../../css/common/colors" as *; -@use "../../../css/common/material" as *; +@use "../../../css/colors" as *; +@use "../../../css/material" as *; @use "sass:color"; // Settings styles diff --git a/frontend/src/pages/chat/ui/components/Electron.tsx b/frontend/src/pages/chat/ui/components/Electron.tsx index 1052454..296d9d1 100644 --- a/frontend/src/pages/chat/ui/components/Electron.tsx +++ b/frontend/src/pages/chat/ui/components/Electron.tsx @@ -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 && ( diff --git a/frontend/src/pages/chat/ui/components/chat/ChatHeader.tsx b/frontend/src/pages/chat/ui/components/chat/ChatHeader.tsx index 2c47b88..0569871 100644 --- a/frontend/src/pages/chat/ui/components/chat/ChatHeader.tsx +++ b/frontend/src/pages/chat/ui/components/chat/ChatHeader.tsx @@ -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"; diff --git a/frontend/src/pages/chat/ui/components/chat/ChatInputWrapper.tsx b/frontend/src/pages/chat/ui/components/chat/ChatInputWrapper.tsx index 89df0f9..7f2d42f 100644 --- a/frontend/src/pages/chat/ui/components/chat/ChatInputWrapper.tsx +++ b/frontend/src/pages/chat/ui/components/chat/ChatInputWrapper.tsx @@ -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"; diff --git a/frontend/src/pages/chat/ui/components/chat/ChatMessages.tsx b/frontend/src/pages/chat/ui/components/chat/ChatMessages.tsx index 60df171..a5cf808 100644 --- a/frontend/src/pages/chat/ui/components/chat/ChatMessages.tsx +++ b/frontend/src/pages/chat/ui/components/chat/ChatMessages.tsx @@ -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[]; diff --git a/frontend/src/pages/chat/ui/components/chat/DMUsersList.tsx b/frontend/src/pages/chat/ui/components/chat/DMUsersList.tsx index 7d20b78..89e820c 100644 --- a/frontend/src/pages/chat/ui/components/chat/DMUsersList.tsx +++ b/frontend/src/pages/chat/ui/components/chat/DMUsersList.tsx @@ -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() { diff --git a/frontend/src/pages/chat/ui/components/chat/EmojiMenu.tsx b/frontend/src/pages/chat/ui/components/chat/EmojiMenu.tsx index c701c4c..a7c0b8d 100644 --- a/frontend/src/pages/chat/ui/components/chat/EmojiMenu.tsx +++ b/frontend/src/pages/chat/ui/components/chat/EmojiMenu.tsx @@ -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; diff --git a/frontend/src/pages/chat/ui/components/chat/LeftPanel.tsx b/frontend/src/pages/chat/ui/components/chat/LeftPanel.tsx index 6c30c39..9d15d35 100644 --- a/frontend/src/pages/chat/ui/components/chat/LeftPanel.tsx +++ b/frontend/src/pages/chat/ui/components/chat/LeftPanel.tsx @@ -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"; diff --git a/frontend/src/pages/chat/ui/components/chat/Message.tsx b/frontend/src/pages/chat/ui/components/chat/Message.tsx index 05fe6d0..058f85a 100644 --- a/frontend/src/pages/chat/ui/components/chat/Message.tsx +++ b/frontend/src/pages/chat/ui/components/chat/Message.tsx @@ -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"; diff --git a/frontend/src/pages/chat/ui/components/chat/MessageContextMenu.tsx b/frontend/src/pages/chat/ui/components/chat/MessageContextMenu.tsx index 6ccf740..6db8a5e 100644 --- a/frontend/src/pages/chat/ui/components/chat/MessageContextMenu.tsx +++ b/frontend/src/pages/chat/ui/components/chat/MessageContextMenu.tsx @@ -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 { diff --git a/frontend/src/pages/chat/ui/components/chat/MessagePanelRenderer.tsx b/frontend/src/pages/chat/ui/components/chat/MessagePanelRenderer.tsx index 96795af..8b6d8de 100644 --- a/frontend/src/pages/chat/ui/components/chat/MessagePanelRenderer.tsx +++ b/frontend/src/pages/chat/ui/components/chat/MessagePanelRenderer.tsx @@ -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"; diff --git a/frontend/src/pages/chat/ui/components/chat/MessageReactions.tsx b/frontend/src/pages/chat/ui/components/chat/MessageReactions.tsx index 808a82e..d811d1a 100644 --- a/frontend/src/pages/chat/ui/components/chat/MessageReactions.tsx +++ b/frontend/src/pages/chat/ui/components/chat/MessageReactions.tsx @@ -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[]; diff --git a/frontend/src/pages/chat/ui/components/chat/ReplyMessageDialog.tsx b/frontend/src/pages/chat/ui/components/chat/ReplyMessageDialog.tsx index f270d1a..1207f28 100644 --- a/frontend/src/pages/chat/ui/components/chat/ReplyMessageDialog.tsx +++ b/frontend/src/pages/chat/ui/components/chat/ReplyMessageDialog.tsx @@ -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"; diff --git a/frontend/src/pages/chat/ui/components/chat/UserProfileDialog.tsx b/frontend/src/pages/chat/ui/components/chat/UserProfileDialog.tsx index 1861f7c..ef3eeeb 100644 --- a/frontend/src/pages/chat/ui/components/chat/UserProfileDialog.tsx +++ b/frontend/src/pages/chat/ui/components/chat/UserProfileDialog.tsx @@ -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"; diff --git a/frontend/src/pages/chat/ui/components/profile/ImageCropper.tsx b/frontend/src/pages/chat/ui/components/profile/ImageCropper.tsx index 58baedc..805b290 100644 --- a/frontend/src/pages/chat/ui/components/profile/ImageCropper.tsx +++ b/frontend/src/pages/chat/ui/components/profile/ImageCropper.tsx @@ -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; diff --git a/frontend/src/pages/chat/ui/components/profile/ProfileDialog.tsx b/frontend/src/pages/chat/ui/components/profile/ProfileDialog.tsx index 8a2b359..3d9f558 100644 --- a/frontend/src/pages/chat/ui/components/profile/ProfileDialog.tsx +++ b/frontend/src/pages/chat/ui/components/profile/ProfileDialog.tsx @@ -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"; diff --git a/frontend/src/pages/chat/ui/components/settings/SettingsDialog.tsx b/frontend/src/pages/chat/ui/components/settings/SettingsDialog.tsx index 7cf21eb..dddd301 100644 --- a/frontend/src/pages/chat/ui/components/settings/SettingsDialog.tsx +++ b/frontend/src/pages/chat/ui/components/settings/SettingsDialog.tsx @@ -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"); diff --git a/frontend/src/pages/chat/ui/hooks/useDM.ts b/frontend/src/pages/chat/ui/hooks/useDM.ts index 53ae1bd..713073b 100644 --- a/frontend/src/pages/chat/ui/hooks/useDM.ts +++ b/frontend/src/pages/chat/ui/hooks/useDM.ts @@ -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; diff --git a/frontend/src/pages/chat/ui/hooks/useProfile.ts b/frontend/src/pages/chat/ui/hooks/useProfile.ts index de890e6..329f753 100644 --- a/frontend/src/pages/chat/ui/hooks/useProfile.ts +++ b/frontend/src/pages/chat/ui/hooks/useProfile.ts @@ -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() { diff --git a/frontend/src/pages/chat/ui/panels/DMPanel.ts b/frontend/src/pages/chat/ui/panels/DMPanel.ts index eb6e8c0..8dc8871 100644 --- a/frontend/src/pages/chat/ui/panels/DMPanel.ts +++ b/frontend/src/pages/chat/ui/panels/DMPanel.ts @@ -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 { diff --git a/frontend/src/pages/chat/ui/panels/MessagePanel.ts b/frontend/src/pages/chat/ui/panels/MessagePanel.ts index 53d4872..f8b44cc 100644 --- a/frontend/src/pages/chat/ui/panels/MessagePanel.ts +++ b/frontend/src/pages/chat/ui/panels/MessagePanel.ts @@ -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 { diff --git a/frontend/src/pages/chat/ui/panels/PublicChatPanel.ts b/frontend/src/pages/chat/ui/panels/PublicChatPanel.ts index 38372cf..a1bf903 100644 --- a/frontend/src/pages/chat/ui/panels/PublicChatPanel.ts +++ b/frontend/src/pages/chat/ui/panels/PublicChatPanel.ts @@ -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 { diff --git a/frontend/src/pages/chat/ui/screen/ChatScreen.tsx b/frontend/src/pages/chat/ui/screen/ChatScreen.tsx deleted file mode 100644 index 04b2707..0000000 --- a/frontend/src/pages/chat/ui/screen/ChatScreen.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { LeftPanel } from "../components/chat/LeftPanel"; -import { RightPanel } from "../components/chat/RightPanel"; - -export default function ChatScreen() { - return ( -
-
- - -
-
- ); -} \ No newline at end of file diff --git a/frontend/src/pages/chat/ui/screen/DownloadAppScreen.tsx b/frontend/src/pages/chat/ui/screen/DownloadAppScreen.tsx deleted file mode 100644 index 8b4e5e5..0000000 --- a/frontend/src/pages/chat/ui/screen/DownloadAppScreen.tsx +++ /dev/null @@ -1,25 +0,0 @@ -export default function DownloadAppScreen() { - return ( -
-
-

Чтобы пользоваться мессенджером, скачайте приложение

-

- Этот сайт не предназначен для работы на маленьких экранах, поэтому - вам нужно скачать приложение мессенджера. -

- - - Скачать на GitHub - - -

- Если возникнут сложности или есть вопросы, нажмите кнопку! -

- - - Написать в поддержку - -
-
- ) -} \ No newline at end of file diff --git a/frontend/src/pages/chat/ui/screen/LoginScreen.tsx b/frontend/src/pages/chat/ui/screen/LoginScreen.tsx deleted file mode 100644 index 85adff9..0000000 --- a/frontend/src/pages/chat/ui/screen/LoginScreen.tsx +++ /dev/null @@ -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([]); - 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(null); - const passwordElement = useRef(null); - - return ( - - -
- - -
{ - 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", "Ошибка соединения с сервером"); - } - }}> - - - - - Войти - - - -
-
- ) -} \ No newline at end of file diff --git a/frontend/src/pages/chat/ui/screen/RegisterScreen.tsx b/frontend/src/pages/chat/ui/screen/RegisterScreen.tsx deleted file mode 100644 index 9fd2a6a..0000000 --- a/frontend/src/pages/chat/ui/screen/RegisterScreen.tsx +++ /dev/null @@ -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([]); - 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(null); - const passwordElement = useRef(null); - const confirmPasswordElement = useRef(null); - - return ( - - -
- - -
{ - 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", "Ошибка соединения с сервером"); - } - }}> - - - - - Зарегистрироваться - - -
-

- Уже есть аккаунт? - navigate("/login")}> - Войдите - -

-
-
-
- ) -} \ No newline at end of file diff --git a/frontend/src/pages/chat/ui/state.ts b/frontend/src/pages/chat/ui/state.ts index 2b011af..ded50fe 100644 --- a/frontend/src/pages/chat/ui/state.ts +++ b/frontend/src/pages/chat/ui/state.ts @@ -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" diff --git a/frontend/src/pages/home/HomePage.tsx b/frontend/src/pages/home/HomePage.tsx index 19578ba..f6311f2 100644 --- a/frontend/src/pages/home/HomePage.tsx +++ b/frontend/src/pages/home/HomePage.tsx @@ -1,6 +1,6 @@ import { Navigate, useNavigate } from "react-router-dom"; import { useAppState } from "../chat/ui/state"; -import { isElectron } from "../chat/electron/electron"; +import { isElectron } from "../../core/electron/electron"; import "./home.scss"; function GitHubLink({ children }: { children: React.ReactNode }) { diff --git a/frontend/src/pages/home/home.scss b/frontend/src/pages/home/home.scss index 1dcb789..9cd545a 100644 --- a/frontend/src/pages/home/home.scss +++ b/frontend/src/pages/home/home.scss @@ -1,4 +1,4 @@ -@use "../../css/common/material" as *; +@use "../../css/material" as *; .homepage { min-height: 100vh; From b638326653bb0c13a0f0cfe54adb654a2a8f5141 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Thu, 9 Oct 2025 22:34:18 +0300 Subject: [PATCH 06/10] Move auth components to the right place --- .../pages/{chat/ui/components => auth}/Auth.tsx | 17 +++++++++++++++++ frontend/src/pages/auth/LoginPage.tsx | 4 ++-- frontend/src/pages/auth/RegisterPage.tsx | 4 ++-- .../src/pages/chat/ui/components/Alerts.tsx | 16 ---------------- 4 files changed, 21 insertions(+), 20 deletions(-) rename frontend/src/pages/{chat/ui/components => auth}/Auth.tsx (71%) delete mode 100644 frontend/src/pages/chat/ui/components/Alerts.tsx diff --git a/frontend/src/pages/chat/ui/components/Auth.tsx b/frontend/src/pages/auth/Auth.tsx similarity index 71% rename from frontend/src/pages/chat/ui/components/Auth.tsx rename to frontend/src/pages/auth/Auth.tsx index 65da3a3..5c6b48a 100644 --- a/frontend/src/pages/chat/ui/components/Auth.tsx +++ b/frontend/src/pages/auth/Auth.tsx @@ -36,4 +36,21 @@ export function AuthHeader({ title, icon, subtitle }: AuthHeaderProps) {

{subtitle}

) +} + +export type AlertType = "success" | "danger" + +export interface Alert { + type: AlertType; + message: string; +} + +export function AlertsContainer({ alerts }: { alerts: Alert[]}) { + return ( +
+ {alerts.slice(-3).map((alert, i) => { + return
{alert.message}
+ })} +
+ ) } \ No newline at end of file diff --git a/frontend/src/pages/auth/LoginPage.tsx b/frontend/src/pages/auth/LoginPage.tsx index be27b3a..a293c19 100644 --- a/frontend/src/pages/auth/LoginPage.tsx +++ b/frontend/src/pages/auth/LoginPage.tsx @@ -1,6 +1,6 @@ import { useImmer } from "use-immer"; -import { AlertsContainer, type Alert, type AlertType } from "../chat/ui/components/Alerts"; -import { AuthContainer, AuthHeader } from "../chat/ui/components/Auth"; +import { AlertsContainer, type Alert, type AlertType } from "./Auth"; +import { AuthContainer, AuthHeader } from "./Auth"; import type { ErrorResponse, LoginRequest, LoginResponse } from "../../core/types"; import { ensureKeysOnLogin } from "../../core/api/authApi"; import { API_BASE_URL } from "../../core/config"; diff --git a/frontend/src/pages/auth/RegisterPage.tsx b/frontend/src/pages/auth/RegisterPage.tsx index 1bddf1e..3e06a4a 100644 --- a/frontend/src/pages/auth/RegisterPage.tsx +++ b/frontend/src/pages/auth/RegisterPage.tsx @@ -1,6 +1,6 @@ import { useImmer } from "use-immer"; -import { AuthContainer, AuthHeader } from "../chat/ui/components/Auth"; -import { AlertsContainer, type Alert, type AlertType } from "../chat/ui/components/Alerts"; +import { AuthContainer, AuthHeader } from "./Auth"; +import { AlertsContainer, type Alert, type AlertType } from "./Auth"; import { useRef } from "react"; import { TextField } from "mdui/components/text-field"; import type { ErrorResponse, RegisterRequest, LoginResponse } from "../../core/types"; diff --git a/frontend/src/pages/chat/ui/components/Alerts.tsx b/frontend/src/pages/chat/ui/components/Alerts.tsx deleted file mode 100644 index 3284ac3..0000000 --- a/frontend/src/pages/chat/ui/components/Alerts.tsx +++ /dev/null @@ -1,16 +0,0 @@ -export type AlertType = "success" | "danger" - -export interface Alert { - type: AlertType; - message: string; -} - -export function AlertsContainer({ alerts }: { alerts: Alert[]}) { - return ( -
- {alerts.slice(-3).map((alert, i) => { - return
{alert.message}
- })} -
- ) -} \ No newline at end of file From 32e08cdfdbbfa549d1f0fb163979a46e50bc7cee Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Thu, 9 Oct 2025 22:35:12 +0300 Subject: [PATCH 07/10] Restructure --- frontend/src/App.tsx | 8 +- .../chat/ui/components => }/Electron.tsx | 4 +- .../core => core/components}/Dialog.tsx | 4 +- .../core => core/components}/Quote.tsx | 0 .../core => core/components}/RichTextArea.tsx | 0 .../core => core/components}/TextField.tsx | 0 .../components}/animations/AnimatedHeight.tsx | 0 .../animations/AnimatedOpacity.tsx | 0 .../components}/animations/types.d.ts | 0 .../chat/ui => core}/hooks/useCombinedRefs.ts | 0 .../chat/ui => core}/hooks/useWindowSize.ts | 0 frontend/src/pages/ProtectedRoute.tsx | 2 +- frontend/src/pages/auth/LoginPage.tsx | 4 +- frontend/src/pages/auth/RegisterPage.tsx | 4 +- .../src/pages/chat/{ui => }/hooks/useDM.ts | 6 +- .../pages/chat/{ui => }/hooks/useProfile.ts | 4 +- frontend/src/pages/chat/{ui => }/state.ts | 20 +-- frontend/src/pages/chat/{ => ui}/ChatPage.tsx | 6 +- .../ui/components/chat/MessageReactions.tsx | 117 --------------- .../ui/components/chat/ReplyMessageDialog.tsx | 60 -------- .../chat => left}/BottomAppBar.tsx | 0 .../{components/chat => left}/ChatHeader.tsx | 4 +- .../ui/{components/chat => left}/ChatTabs.tsx | 0 .../{components/chat => left}/DMUsersList.tsx | 4 +- .../{components/chat => left}/LeftPanel.tsx | 8 +- .../profile/CropperDialog.tsx | 0 .../profile/ImageCropper.tsx | 0 .../profile/ProfileDialog.tsx | 6 +- .../settings/SettingsDialog.tsx | 4 +- .../chat => right}/ChatInputWrapper.tsx | 10 +- .../chat => right}/ChatMainHeader.tsx | 0 .../chat => right}/ChatMessages.tsx | 14 +- .../{components/chat => right}/EmojiMenu.tsx | 2 +- .../ui/{components/chat => right}/Message.tsx | 136 ++++++++++++++++-- .../chat => right}/MessageContextMenu.tsx | 2 +- .../chat => right}/MessagePanelRenderer.tsx | 12 +- .../{components/chat => right}/RightPanel.tsx | 0 .../chat => right}/UserProfileDialog.tsx | 10 +- .../{components/chat => right}/emojiData.ts | 0 .../chat/ui/{ => right}/panels/DMPanel.ts | 6 +- .../ui/{ => right}/panels/MessagePanel.ts | 4 +- .../ui/{ => right}/panels/PublicChatPanel.ts | 10 +- frontend/src/pages/home/HomePage.tsx | 2 +- 43 files changed, 205 insertions(+), 268 deletions(-) rename frontend/src/{pages/chat/ui/components => }/Electron.tsx (69%) rename frontend/src/{pages/chat/ui/components/core => core/components}/Dialog.tsx (93%) rename frontend/src/{pages/chat/ui/components/core => core/components}/Quote.tsx (100%) rename frontend/src/{pages/chat/ui/components/core => core/components}/RichTextArea.tsx (100%) rename frontend/src/{pages/chat/ui/components/core => core/components}/TextField.tsx (100%) rename frontend/src/{pages/chat/ui/components/core => core/components}/animations/AnimatedHeight.tsx (100%) rename frontend/src/{pages/chat/ui/components/core => core/components}/animations/AnimatedOpacity.tsx (100%) rename frontend/src/{pages/chat/ui/components/core => core/components}/animations/types.d.ts (100%) rename frontend/src/{pages/chat/ui => core}/hooks/useCombinedRefs.ts (100%) rename frontend/src/{pages/chat/ui => core}/hooks/useWindowSize.ts (100%) rename frontend/src/pages/chat/{ui => }/hooks/useDM.ts (98%) rename frontend/src/pages/chat/{ui => }/hooks/useProfile.ts (96%) rename frontend/src/pages/chat/{ui => }/state.ts (95%) rename frontend/src/pages/chat/{ => ui}/ChatPage.tsx (61%) delete mode 100644 frontend/src/pages/chat/ui/components/chat/MessageReactions.tsx delete mode 100644 frontend/src/pages/chat/ui/components/chat/ReplyMessageDialog.tsx rename frontend/src/pages/chat/ui/{components/chat => left}/BottomAppBar.tsx (100%) rename frontend/src/pages/chat/ui/{components/chat => left}/ChatHeader.tsx (91%) rename frontend/src/pages/chat/ui/{components/chat => left}/ChatTabs.tsx (100%) rename frontend/src/pages/chat/ui/{components/chat => left}/DMUsersList.tsx (95%) rename frontend/src/pages/chat/ui/{components/chat => left}/LeftPanel.tsx (94%) rename frontend/src/pages/chat/ui/{components => left}/profile/CropperDialog.tsx (100%) rename frontend/src/pages/chat/ui/{components => left}/profile/ImageCropper.tsx (100%) rename frontend/src/pages/chat/ui/{components => left}/profile/ProfileDialog.tsx (97%) rename frontend/src/pages/chat/ui/{components => left}/settings/SettingsDialog.tsx (99%) rename frontend/src/pages/chat/ui/{components/chat => right}/ChatInputWrapper.tsx (96%) rename frontend/src/pages/chat/ui/{components/chat => right}/ChatMainHeader.tsx (100%) rename frontend/src/pages/chat/ui/{components/chat => right}/ChatMessages.tsx (94%) rename frontend/src/pages/chat/ui/{components/chat => right}/EmojiMenu.tsx (99%) rename frontend/src/pages/chat/ui/{components/chat => right}/Message.tsx (79%) rename frontend/src/pages/chat/ui/{components/chat => right}/MessageContextMenu.tsx (99%) rename frontend/src/pages/chat/ui/{components/chat => right}/MessagePanelRenderer.tsx (97%) rename frontend/src/pages/chat/ui/{components/chat => right}/RightPanel.tsx (100%) rename frontend/src/pages/chat/ui/{components/chat => right}/UserProfileDialog.tsx (90%) rename frontend/src/pages/chat/ui/{components/chat => right}/emojiData.ts (100%) rename frontend/src/pages/chat/ui/{ => right}/panels/DMPanel.ts (98%) rename frontend/src/pages/chat/ui/{ => right}/panels/MessagePanel.ts (98%) rename frontend/src/pages/chat/ui/{ => right}/panels/PublicChatPanel.ts (95%) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 696ed8c..2f420a7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,17 +1,17 @@ import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; -import { ElectronTitleBar } from "./pages/chat/ui/components/Electron"; -import { useAppState } from "./pages/chat/ui/state"; +import { ElectronTitleBar } from "./Electron"; +import { useAppState } from "./pages/chat/state"; import { useEffect, useState } from "react"; import HomePage from "./pages/home/HomePage"; import LoginPage from "./pages/auth/LoginPage"; import RegisterPage from "./pages/auth/RegisterPage"; -import ChatPage from "./pages/chat/ChatPage"; +import ChatPage from "./pages/chat/ui/ChatPage"; import DownloadAppPage from "./pages/download-app/DownloadAppPage"; import NotFoundPage from "./pages/not-found/NotFoundPage"; import ProtectedRoute from "./pages/ProtectedRoute"; import { isElectron } from "./core/electron/electron"; import { MINIMUM_WIDTH } from "./core/config"; -import useWindowSize from "./pages/chat/ui/hooks/useWindowSize"; +import useWindowSize from "./core/hooks/useWindowSize"; export default function App() { const { restoreUserFromStorage } = useAppState(); diff --git a/frontend/src/pages/chat/ui/components/Electron.tsx b/frontend/src/Electron.tsx similarity index 69% rename from frontend/src/pages/chat/ui/components/Electron.tsx rename to frontend/src/Electron.tsx index 296d9d1..03c2522 100644 --- a/frontend/src/pages/chat/ui/components/Electron.tsx +++ b/frontend/src/Electron.tsx @@ -1,5 +1,5 @@ -import { PRODUCT_NAME } from "../../../../core/config"; -import { isElectron } from "../../../../core/electron/electron"; +import { PRODUCT_NAME } from "./core/config"; +import { isElectron } from "./core/electron/electron"; export function ElectronTitleBar() { return isElectron && ( diff --git a/frontend/src/pages/chat/ui/components/core/Dialog.tsx b/frontend/src/core/components/Dialog.tsx similarity index 93% rename from frontend/src/pages/chat/ui/components/core/Dialog.tsx rename to frontend/src/core/components/Dialog.tsx index 43d1923..35ef39b 100644 --- a/frontend/src/pages/chat/ui/components/core/Dialog.tsx +++ b/frontend/src/core/components/Dialog.tsx @@ -1,8 +1,8 @@ import type { Dialog as MduiDialog } from "mdui/components/dialog"; import { useEffect, type Ref } from "react" import { createPortal } from "react-dom"; -import { id } from "../../../../../utils/utils"; -import useCombinedRefs from "../../hooks/useCombinedRefs"; +import { id } from "../../utils/utils"; +import useCombinedRefs from "../hooks/useCombinedRefs"; export interface BaseDialogProps { onOpenChange: (value: boolean) => void; diff --git a/frontend/src/pages/chat/ui/components/core/Quote.tsx b/frontend/src/core/components/Quote.tsx similarity index 100% rename from frontend/src/pages/chat/ui/components/core/Quote.tsx rename to frontend/src/core/components/Quote.tsx diff --git a/frontend/src/pages/chat/ui/components/core/RichTextArea.tsx b/frontend/src/core/components/RichTextArea.tsx similarity index 100% rename from frontend/src/pages/chat/ui/components/core/RichTextArea.tsx rename to frontend/src/core/components/RichTextArea.tsx diff --git a/frontend/src/pages/chat/ui/components/core/TextField.tsx b/frontend/src/core/components/TextField.tsx similarity index 100% rename from frontend/src/pages/chat/ui/components/core/TextField.tsx rename to frontend/src/core/components/TextField.tsx diff --git a/frontend/src/pages/chat/ui/components/core/animations/AnimatedHeight.tsx b/frontend/src/core/components/animations/AnimatedHeight.tsx similarity index 100% rename from frontend/src/pages/chat/ui/components/core/animations/AnimatedHeight.tsx rename to frontend/src/core/components/animations/AnimatedHeight.tsx diff --git a/frontend/src/pages/chat/ui/components/core/animations/AnimatedOpacity.tsx b/frontend/src/core/components/animations/AnimatedOpacity.tsx similarity index 100% rename from frontend/src/pages/chat/ui/components/core/animations/AnimatedOpacity.tsx rename to frontend/src/core/components/animations/AnimatedOpacity.tsx diff --git a/frontend/src/pages/chat/ui/components/core/animations/types.d.ts b/frontend/src/core/components/animations/types.d.ts similarity index 100% rename from frontend/src/pages/chat/ui/components/core/animations/types.d.ts rename to frontend/src/core/components/animations/types.d.ts diff --git a/frontend/src/pages/chat/ui/hooks/useCombinedRefs.ts b/frontend/src/core/hooks/useCombinedRefs.ts similarity index 100% rename from frontend/src/pages/chat/ui/hooks/useCombinedRefs.ts rename to frontend/src/core/hooks/useCombinedRefs.ts diff --git a/frontend/src/pages/chat/ui/hooks/useWindowSize.ts b/frontend/src/core/hooks/useWindowSize.ts similarity index 100% rename from frontend/src/pages/chat/ui/hooks/useWindowSize.ts rename to frontend/src/core/hooks/useWindowSize.ts diff --git a/frontend/src/pages/ProtectedRoute.tsx b/frontend/src/pages/ProtectedRoute.tsx index 5036612..27fac5e 100644 --- a/frontend/src/pages/ProtectedRoute.tsx +++ b/frontend/src/pages/ProtectedRoute.tsx @@ -1,5 +1,5 @@ import { useEffect } from "react"; -import { useAppState } from "./chat/ui/state"; +import { useAppState } from "./chat/state"; import { useNavigate } from "react-router-dom"; interface ProtectedRouteProps { diff --git a/frontend/src/pages/auth/LoginPage.tsx b/frontend/src/pages/auth/LoginPage.tsx index a293c19..78e7cd8 100644 --- a/frontend/src/pages/auth/LoginPage.tsx +++ b/frontend/src/pages/auth/LoginPage.tsx @@ -6,8 +6,8 @@ import { ensureKeysOnLogin } from "../../core/api/authApi"; import { API_BASE_URL } from "../../core/config"; import { useRef } from "react"; import type { TextField } from "mdui/components/text-field"; -import { useAppState } from "../chat/ui/state"; -import { MaterialTextField } from "../chat/ui/components/core/TextField"; +import { useAppState } from "../chat/state"; +import { MaterialTextField } from "../../core/components/TextField"; import { initialize, isSupported, startElectronReceiver, subscribe } from "../../core/push-notifications/push-notifications"; import { isElectron } from "../../core/electron/electron"; import { useNavigate } from "react-router-dom"; diff --git a/frontend/src/pages/auth/RegisterPage.tsx b/frontend/src/pages/auth/RegisterPage.tsx index 3e06a4a..f484e51 100644 --- a/frontend/src/pages/auth/RegisterPage.tsx +++ b/frontend/src/pages/auth/RegisterPage.tsx @@ -5,8 +5,8 @@ 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 "../chat/ui/state"; -import { MaterialTextField } from "../chat/ui/components/core/TextField"; +import { useAppState } from "../chat/state"; +import { MaterialTextField } from "../../core/components/TextField"; import { ensureKeysOnLogin } from "../../core/api/authApi"; import { useNavigate } from "react-router-dom"; import "./auth.scss"; diff --git a/frontend/src/pages/chat/ui/hooks/useDM.ts b/frontend/src/pages/chat/hooks/useDM.ts similarity index 98% rename from frontend/src/pages/chat/ui/hooks/useDM.ts rename to frontend/src/pages/chat/hooks/useDM.ts index 713073b..f3db530 100644 --- a/frontend/src/pages/chat/ui/hooks/useDM.ts +++ b/frontend/src/pages/chat/hooks/useDM.ts @@ -6,9 +6,9 @@ import { fetchDMHistory, decryptDm, sendDMViaWebSocket -} from "../../../../core/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; diff --git a/frontend/src/pages/chat/ui/hooks/useProfile.ts b/frontend/src/pages/chat/hooks/useProfile.ts similarity index 96% rename from frontend/src/pages/chat/ui/hooks/useProfile.ts rename to frontend/src/pages/chat/hooks/useProfile.ts index 329f753..c4c2764 100644 --- a/frontend/src/pages/chat/ui/hooks/useProfile.ts +++ b/frontend/src/pages/chat/hooks/useProfile.ts @@ -1,7 +1,7 @@ import { useState, useCallback, useEffect } from "react"; import { useAppState } from "../state"; -import { loadProfile, updateProfile, uploadProfilePicture, type ProfileData } from "../../../../core/api/profileApi"; -import { showSuccess, showError } from "../../../../utils/notification"; +import { loadProfile, updateProfile, uploadProfilePicture, type ProfileData } from "../../../core/api/profileApi"; +import { showSuccess, showError } from "../../../utils/notification"; export default function useProfile() { const { user } = useAppState(); diff --git a/frontend/src/pages/chat/ui/state.ts b/frontend/src/pages/chat/state.ts similarity index 95% rename from frontend/src/pages/chat/ui/state.ts rename to frontend/src/pages/chat/state.ts index ded50fe..a7eff88 100644 --- a/frontend/src/pages/chat/ui/state.ts +++ b/frontend/src/pages/chat/state.ts @@ -1,14 +1,14 @@ import { create } from "zustand"; -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 "../../../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"; +import type { Message, User } from "../../core/types"; +import { request } from "../../core/websocket"; +import { MessagePanel } from "./ui/right/panels/MessagePanel"; +import { PublicChatPanel } from "./ui/right/panels/PublicChatPanel"; +import { DMPanel, type DMPanelData } from "./ui/right/panels/DMPanel"; +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" diff --git a/frontend/src/pages/chat/ChatPage.tsx b/frontend/src/pages/chat/ui/ChatPage.tsx similarity index 61% rename from frontend/src/pages/chat/ChatPage.tsx rename to frontend/src/pages/chat/ui/ChatPage.tsx index 19cb5a2..b5024e9 100644 --- a/frontend/src/pages/chat/ChatPage.tsx +++ b/frontend/src/pages/chat/ui/ChatPage.tsx @@ -1,6 +1,6 @@ -import { LeftPanel } from "./ui/components/chat/LeftPanel"; -import { RightPanel } from "./ui/components/chat/RightPanel"; -import "./css/chat.scss"; +import { LeftPanel } from "./left/LeftPanel"; +import { RightPanel } from "./right/RightPanel"; +import "../css/chat.scss"; export default function ChatPage() { return ( diff --git a/frontend/src/pages/chat/ui/components/chat/MessageReactions.tsx b/frontend/src/pages/chat/ui/components/chat/MessageReactions.tsx deleted file mode 100644 index d811d1a..0000000 --- a/frontend/src/pages/chat/ui/components/chat/MessageReactions.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { useAppState } from "../../state"; -import { useState, useEffect } from "react"; -import type { Reaction } from "../../../../../core/types"; - -interface MessageReactionsProps { - reactions?: Reaction[]; - onReactionClick: (emoji: string) => void; - messageId?: number; // Add messageId to ensure unique keys -} - -export function MessageReactions({ reactions, onReactionClick, messageId }: MessageReactionsProps) { - const { user } = useAppState(); - const [visibleReactions, setVisibleReactions] = useState([]); - const [animatingReactions, setAnimatingReactions] = useState>(new Set()); - const [isVisible, setIsVisible] = useState(false); - - // Handle reactions with animation - useEffect(() => { - if (!reactions || reactions.length === 0) { - // If we have visible reactions, animate them out - if (visibleReactions.length > 0) { - visibleReactions.forEach(reaction => { - setAnimatingReactions(prev => new Set(prev).add(reaction.emoji)); - }); - // After animation completes, hide the component - setTimeout(() => { - setVisibleReactions([]); - setAnimatingReactions(new Set()); - setIsVisible(false); - }, 200); - } else { - // No visible reactions, hide immediately - setIsVisible(false); - } - return; - } - - // Show the component when we have reactions - setIsVisible(true); - - // Deduplicate reactions by emoji (safety measure) - const uniqueReactions = reactions.reduce((acc, reaction) => { - const existing = acc.find(r => r.emoji === reaction.emoji); - if (existing) { - // Keep the one with the higher count - if (reaction.count > existing.count) { - acc[acc.indexOf(existing)] = reaction; - } - } else { - acc.push(reaction); - } - return acc; - }, [] as Reaction[]); - - - // Animate out removed reactions - visibleReactions.forEach(reaction => { - if (!uniqueReactions.some(r => r.emoji === reaction.emoji)) { - setAnimatingReactions(prev => new Set(prev).add(reaction.emoji)); - setTimeout(() => { - setVisibleReactions(prev => prev.filter(r => r.emoji !== reaction.emoji)); - setAnimatingReactions(prev => { - const newSet = new Set(prev); - newSet.delete(reaction.emoji); - return newSet; - }); - }, 200); - } - }); - - // Update existing reactions and add new ones - setVisibleReactions(prev => { - const updated = [...prev]; - - // Update existing reactions - uniqueReactions.forEach(reaction => { - const existingIndex = updated.findIndex(r => r.emoji === reaction.emoji); - if (existingIndex !== -1) { - updated[existingIndex] = reaction; - } else { - // Add new reaction only if it doesn't already exist - if (!updated.some(r => r.emoji === reaction.emoji)) { - updated.push(reaction); - } - } - }); - - return updated; - }); - }, [reactions]); - - // Don't render if not visible - if (!isVisible) { - return null; - } - - return ( -
- {visibleReactions.map((reaction, index) => { - const hasUserReacted = reaction.users.some(u => u.id === user.currentUser?.id); - const isAnimating = animatingReactions.has(reaction.emoji); - - return ( - - ); - })} -
- ); -} diff --git a/frontend/src/pages/chat/ui/components/chat/ReplyMessageDialog.tsx b/frontend/src/pages/chat/ui/components/chat/ReplyMessageDialog.tsx deleted file mode 100644 index 1207f28..0000000 --- a/frontend/src/pages/chat/ui/components/chat/ReplyMessageDialog.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { useState, useEffect } from "react"; -import type { Message } from "../../../../../core/types"; -import { MaterialDialog } from "../core/Dialog"; -import { MaterialTextField } from "../core/TextField"; - -interface ReplyMessageDialogProps { - isOpen: boolean; - onOpenChange: (value: boolean) => void; - replyToMessage: Message | null; - onSendReply: (content: string, replyToId: number) => void; -} - -export function ReplyMessageDialog({ isOpen, onOpenChange, replyToMessage, onSendReply }: ReplyMessageDialogProps) { - const [replyContent, setReplyContent] = useState(""); - - useEffect(() => { - if (replyToMessage) { - setReplyContent(""); - } - }, [replyToMessage]); - - const handleSendReply = () => { - if (replyToMessage && replyContent.trim()) { - onSendReply(replyContent.trim(), replyToMessage.id); - onOpenChange(false); - } - }; - - const handleCancel = () => { - onOpenChange(false); - setReplyContent(""); - }; - - if (!replyToMessage) return null; - - return ( - -
-

Ответить на сообщение

-
-
- {replyToMessage.username} - {replyToMessage.content} -
-
- setReplyContent((e.target as HTMLInputElement).value)} - label="Reply" - variant="outlined" - placeholder="Type your reply..." - maxlength={1000} /> -
- Cancel - Send Reply -
-
-
- ); -} diff --git a/frontend/src/pages/chat/ui/components/chat/BottomAppBar.tsx b/frontend/src/pages/chat/ui/left/BottomAppBar.tsx similarity index 100% rename from frontend/src/pages/chat/ui/components/chat/BottomAppBar.tsx rename to frontend/src/pages/chat/ui/left/BottomAppBar.tsx diff --git a/frontend/src/pages/chat/ui/components/chat/ChatHeader.tsx b/frontend/src/pages/chat/ui/left/ChatHeader.tsx similarity index 91% rename from frontend/src/pages/chat/ui/components/chat/ChatHeader.tsx rename to frontend/src/pages/chat/ui/left/ChatHeader.tsx index 0569871..b94297e 100644 --- a/frontend/src/pages/chat/ui/components/chat/ChatHeader.tsx +++ b/frontend/src/pages/chat/ui/left/ChatHeader.tsx @@ -1,8 +1,8 @@ -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"; -import { ProfileDialog } from "../profile/ProfileDialog"; +import { ProfileDialog } from "./profile/ProfileDialog"; export function ChatHeader() { const { profileData } = useProfile(); diff --git a/frontend/src/pages/chat/ui/components/chat/ChatTabs.tsx b/frontend/src/pages/chat/ui/left/ChatTabs.tsx similarity index 100% rename from frontend/src/pages/chat/ui/components/chat/ChatTabs.tsx rename to frontend/src/pages/chat/ui/left/ChatTabs.tsx diff --git a/frontend/src/pages/chat/ui/components/chat/DMUsersList.tsx b/frontend/src/pages/chat/ui/left/DMUsersList.tsx similarity index 95% rename from frontend/src/pages/chat/ui/components/chat/DMUsersList.tsx rename to frontend/src/pages/chat/ui/left/DMUsersList.tsx index 89e820c..022532d 100644 --- a/frontend/src/pages/chat/ui/components/chat/DMUsersList.tsx +++ b/frontend/src/pages/chat/ui/left/DMUsersList.tsx @@ -1,8 +1,8 @@ import { useEffect } from "react"; import { useDM, type DMUser } from "../../hooks/useDM"; import { useAppState } from "../../state"; -import { fetchUserPublicKey } from "../../../../../core/api/dmApi"; -import defaultAvatar from "../../../../../images/default-avatar.png"; +import { fetchUserPublicKey } from "../../../../core/api/dmApi"; +import defaultAvatar from "../../../../images/default-avatar.png"; export function DMUsersList() { const { dmUsers, isLoadingUsers, loadUsers } = useDM(); diff --git a/frontend/src/pages/chat/ui/components/chat/LeftPanel.tsx b/frontend/src/pages/chat/ui/left/LeftPanel.tsx similarity index 94% rename from frontend/src/pages/chat/ui/components/chat/LeftPanel.tsx rename to frontend/src/pages/chat/ui/left/LeftPanel.tsx index 9d15d35..a74a8aa 100644 --- a/frontend/src/pages/chat/ui/components/chat/LeftPanel.tsx +++ b/frontend/src/pages/chat/ui/left/LeftPanel.tsx @@ -1,9 +1,9 @@ -import { PRODUCT_NAME } from "../../../../../core/config"; +import { PRODUCT_NAME } from "../../../../core/config"; import { useAppState } from "../../state"; -import defaultAvatar from "../../../../../images/default-avatar.png"; +import defaultAvatar from "../../../../images/default-avatar.png"; import { useState, type FormEvent } from "react"; -import { ProfileDialog } from "../profile/ProfileDialog"; -import { SettingsDialog } from "../settings/SettingsDialog"; +import { ProfileDialog } from "./profile/ProfileDialog"; +import { SettingsDialog } from "./settings/SettingsDialog"; import { DMUsersList } from "./DMUsersList"; import type { Tabs } from "mdui"; import type { ChatTabs } from "../../state"; diff --git a/frontend/src/pages/chat/ui/components/profile/CropperDialog.tsx b/frontend/src/pages/chat/ui/left/profile/CropperDialog.tsx similarity index 100% rename from frontend/src/pages/chat/ui/components/profile/CropperDialog.tsx rename to frontend/src/pages/chat/ui/left/profile/CropperDialog.tsx diff --git a/frontend/src/pages/chat/ui/components/profile/ImageCropper.tsx b/frontend/src/pages/chat/ui/left/profile/ImageCropper.tsx similarity index 100% rename from frontend/src/pages/chat/ui/components/profile/ImageCropper.tsx rename to frontend/src/pages/chat/ui/left/profile/ImageCropper.tsx diff --git a/frontend/src/pages/chat/ui/components/profile/ProfileDialog.tsx b/frontend/src/pages/chat/ui/left/profile/ProfileDialog.tsx similarity index 97% rename from frontend/src/pages/chat/ui/components/profile/ProfileDialog.tsx rename to frontend/src/pages/chat/ui/left/profile/ProfileDialog.tsx index 3d9f558..1ad53de 100644 --- a/frontend/src/pages/chat/ui/components/profile/ProfileDialog.tsx +++ b/frontend/src/pages/chat/ui/left/profile/ProfileDialog.tsx @@ -2,10 +2,10 @@ 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 { MaterialDialog } from "../core/Dialog"; -import useProfile from "../../hooks/useProfile"; +import { MaterialDialog } from "../../../../../core/components/Dialog"; +import useProfile from "../../../hooks/useProfile"; import { ImageCropper } from "./ImageCropper"; -import { MaterialTextField } from "../core/TextField"; +import { MaterialTextField } from "../../../../../core/components/TextField"; export function ProfileDialog({ isOpen, onOpenChange }: DialogProps) { const { profileData, isLoading, isUpdating, updateProfileData, uploadProfilePictureData } = useProfile(); diff --git a/frontend/src/pages/chat/ui/components/settings/SettingsDialog.tsx b/frontend/src/pages/chat/ui/left/settings/SettingsDialog.tsx similarity index 99% rename from frontend/src/pages/chat/ui/components/settings/SettingsDialog.tsx rename to frontend/src/pages/chat/ui/left/settings/SettingsDialog.tsx index dddd301..806891e 100644 --- a/frontend/src/pages/chat/ui/components/settings/SettingsDialog.tsx +++ b/frontend/src/pages/chat/ui/left/settings/SettingsDialog.tsx @@ -1,10 +1,10 @@ import { useState, useEffect } from "react"; import { PRODUCT_NAME, API_BASE_URL } from "../../../../../core/config"; import type { DialogProps } from "../../../../../core/types"; -import { MaterialDialog } from "../core/Dialog"; +import { MaterialDialog } from "../../../../../core/components/Dialog"; import { initialize, isSupported, startElectronReceiver, stopElectronReceiver, subscribe, unsubscribe } from "../../../../../core/push-notifications/push-notifications"; import { isElectron } from "../../../../../core/electron/electron"; -import { useAppState } from "../../state"; +import { useAppState } from "../../../state"; import type { Switch } from "mdui/components/switch"; import { getAuthHeaders } from "../../../../../core/api/authApi"; diff --git a/frontend/src/pages/chat/ui/components/chat/ChatInputWrapper.tsx b/frontend/src/pages/chat/ui/right/ChatInputWrapper.tsx similarity index 96% rename from frontend/src/pages/chat/ui/components/chat/ChatInputWrapper.tsx rename to frontend/src/pages/chat/ui/right/ChatInputWrapper.tsx index 7f2d42f..eeccf6e 100644 --- a/frontend/src/pages/chat/ui/components/chat/ChatInputWrapper.tsx +++ b/frontend/src/pages/chat/ui/right/ChatInputWrapper.tsx @@ -1,9 +1,9 @@ import { useState, useEffect, useRef } from "react"; -import { MaterialDialog } from "../core/Dialog"; -import { RichTextArea } from "../core/RichTextArea"; -import type { Message } from "../../../../../core/types"; -import Quote from "../core/Quote"; -import AnimatedHeight from "../core/animations/AnimatedHeight"; +import { MaterialDialog } from "../../../../core/components/Dialog"; +import { RichTextArea } from "../../../../core/components/RichTextArea"; +import type { Message } from "../../../../core/types"; +import Quote from "../../../../core/components/Quote"; +import AnimatedHeight from "../../../../core/components/animations/AnimatedHeight"; import { useImmer } from "use-immer"; import { EmojiMenu } from "./EmojiMenu"; diff --git a/frontend/src/pages/chat/ui/components/chat/ChatMainHeader.tsx b/frontend/src/pages/chat/ui/right/ChatMainHeader.tsx similarity index 100% rename from frontend/src/pages/chat/ui/components/chat/ChatMainHeader.tsx rename to frontend/src/pages/chat/ui/right/ChatMainHeader.tsx diff --git a/frontend/src/pages/chat/ui/components/chat/ChatMessages.tsx b/frontend/src/pages/chat/ui/right/ChatMessages.tsx similarity index 94% rename from frontend/src/pages/chat/ui/components/chat/ChatMessages.tsx rename to frontend/src/pages/chat/ui/right/ChatMessages.tsx index a5cf808..3d8d8f7 100644 --- a/frontend/src/pages/chat/ui/components/chat/ChatMessages.tsx +++ b/frontend/src/pages/chat/ui/right/ChatMessages.tsx @@ -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 "../../../../../core/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 { delay } from "../../../../utils/utils"; +import { MaterialDialog } from "../../../../core/components/Dialog"; +import { request } from "../../../../core/websocket"; +import type { AddReactionRequest, AddDmReactionRequest } from "../../../../core/types"; interface ChatMessagesProps { messages?: MessageType[]; diff --git a/frontend/src/pages/chat/ui/components/chat/EmojiMenu.tsx b/frontend/src/pages/chat/ui/right/EmojiMenu.tsx similarity index 99% rename from frontend/src/pages/chat/ui/components/chat/EmojiMenu.tsx rename to frontend/src/pages/chat/ui/right/EmojiMenu.tsx index a7c0b8d..196166c 100644 --- a/frontend/src/pages/chat/ui/components/chat/EmojiMenu.tsx +++ b/frontend/src/pages/chat/ui/right/EmojiMenu.tsx @@ -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; diff --git a/frontend/src/pages/chat/ui/components/chat/Message.tsx b/frontend/src/pages/chat/ui/right/Message.tsx similarity index 79% rename from frontend/src/pages/chat/ui/components/chat/Message.tsx rename to frontend/src/pages/chat/ui/right/Message.tsx index 058f85a..a7e8cbc 100644 --- a/frontend/src/pages/chat/ui/components/chat/Message.tsx +++ b/frontend/src/pages/chat/ui/right/Message.tsx @@ -1,19 +1,133 @@ -import { formatTime, id } from "../../../../../utils/utils"; -import type { Attachment, Message as MessageType } from "../../../../../core/types"; -import defaultAvatar from "../../../../../images/default-avatar.png"; -import Quote from "../core/Quote"; +import { formatTime, id } from "../../../../utils/utils"; +import type { Attachment, Message as MessageType, Reaction } from "../../../../core/types"; +import defaultAvatar from "../../../../images/default-avatar.png"; +import Quote from "../../../../core/components/Quote"; import { parse } from "marked"; import DOMPurify from "dompurify"; import { useEffect, useState, useRef } from "react"; -import { getCurrentKeys } from "../../../../../core/api/authApi"; -import { ecdhSharedSecret, deriveWrappingKey } from "../../../../../utils/crypto/asymmetric"; -import { importAesGcmKey, aesGcmDecrypt } from "../../../../../utils/crypto/symmetric"; -import { getAuthHeaders } from "../../../../../core/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 "../../../../core/api/authApi"; import { useAppState } from "../../state"; -import { ub64 } from "../../../../../utils/utils"; +import { ub64 } from "../../../../utils/utils"; import { useImmer } from "use-immer"; import { createPortal } from "react-dom"; -import { MessageReactions } from "./MessageReactions"; + +interface MessageReactionsProps { + reactions?: Reaction[]; + onReactionClick: (emoji: string) => void; + messageId?: number; // Add messageId to ensure unique keys +} + +function Reactions({ reactions, onReactionClick, messageId }: MessageReactionsProps) { + const { user } = useAppState(); + const [visibleReactions, setVisibleReactions] = useState([]); + const [animatingReactions, setAnimatingReactions] = useState>(new Set()); + const [isVisible, setIsVisible] = useState(false); + + // Handle reactions with animation + useEffect(() => { + if (!reactions || reactions.length === 0) { + // If we have visible reactions, animate them out + if (visibleReactions.length > 0) { + visibleReactions.forEach(reaction => { + setAnimatingReactions(prev => new Set(prev).add(reaction.emoji)); + }); + // After animation completes, hide the component + setTimeout(() => { + setVisibleReactions([]); + setAnimatingReactions(new Set()); + setIsVisible(false); + }, 200); + } else { + // No visible reactions, hide immediately + setIsVisible(false); + } + return; + } + + // Show the component when we have reactions + setIsVisible(true); + + // Deduplicate reactions by emoji (safety measure) + const uniqueReactions = reactions.reduce((acc, reaction) => { + const existing = acc.find(r => r.emoji === reaction.emoji); + if (existing) { + // Keep the one with the higher count + if (reaction.count > existing.count) { + acc[acc.indexOf(existing)] = reaction; + } + } else { + acc.push(reaction); + } + return acc; + }, [] as Reaction[]); + + + // Animate out removed reactions + visibleReactions.forEach(reaction => { + if (!uniqueReactions.some(r => r.emoji === reaction.emoji)) { + setAnimatingReactions(prev => new Set(prev).add(reaction.emoji)); + setTimeout(() => { + setVisibleReactions(prev => prev.filter(r => r.emoji !== reaction.emoji)); + setAnimatingReactions(prev => { + const newSet = new Set(prev); + newSet.delete(reaction.emoji); + return newSet; + }); + }, 200); + } + }); + + // Update existing reactions and add new ones + setVisibleReactions(prev => { + const updated = [...prev]; + + // Update existing reactions + uniqueReactions.forEach(reaction => { + const existingIndex = updated.findIndex(r => r.emoji === reaction.emoji); + if (existingIndex !== -1) { + updated[existingIndex] = reaction; + } else { + // Add new reaction only if it doesn't already exist + if (!updated.some(r => r.emoji === reaction.emoji)) { + updated.push(reaction); + } + } + }); + + return updated; + }); + }, [reactions]); + + // Don't render if not visible + if (!isVisible) { + return null; + } + + return ( +
+ {visibleReactions.map((reaction, index) => { + const hasUserReacted = reaction.users.some(u => u.id === user.currentUser?.id); + const isAnimating = animatingReactions.has(reaction.emoji); + + return ( + + ); + })} +
+ ); +} + interface MessageProps { message: MessageType; @@ -375,7 +489,7 @@ export function Message({ message, isAuthor, onProfileClick, onContextMenu, onRe )} - onReactionClick?.(message.id, emoji)} messageId={message.id} diff --git a/frontend/src/pages/chat/ui/components/chat/MessageContextMenu.tsx b/frontend/src/pages/chat/ui/right/MessageContextMenu.tsx similarity index 99% rename from frontend/src/pages/chat/ui/components/chat/MessageContextMenu.tsx rename to frontend/src/pages/chat/ui/right/MessageContextMenu.tsx index 6db8a5e..c13677e 100644 --- a/frontend/src/pages/chat/ui/components/chat/MessageContextMenu.tsx +++ b/frontend/src/pages/chat/ui/right/MessageContextMenu.tsx @@ -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 { diff --git a/frontend/src/pages/chat/ui/components/chat/MessagePanelRenderer.tsx b/frontend/src/pages/chat/ui/right/MessagePanelRenderer.tsx similarity index 97% rename from frontend/src/pages/chat/ui/components/chat/MessagePanelRenderer.tsx rename to frontend/src/pages/chat/ui/right/MessagePanelRenderer.tsx index 8b6d8de..4c5ed43 100644 --- a/frontend/src/pages/chat/ui/components/chat/MessagePanelRenderer.tsx +++ b/frontend/src/pages/chat/ui/right/MessagePanelRenderer.tsx @@ -1,13 +1,13 @@ 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 { ChatInputWrapper } from "./ChatInputWrapper"; -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"; +import { setGlobalMessageHandler } from "../../../../core/websocket"; +import type { Message, WebSocketMessage } from "../../../../core/types"; +import defaultAvatar from "../../../../images/default-avatar.png"; +import AnimatedOpacity from "../../../../core/components/animations/AnimatedOpacity"; +import type { DMPanel } from "./panels/DMPanel"; interface MessagePanelRendererProps { panel: MessagePanel | null; diff --git a/frontend/src/pages/chat/ui/components/chat/RightPanel.tsx b/frontend/src/pages/chat/ui/right/RightPanel.tsx similarity index 100% rename from frontend/src/pages/chat/ui/components/chat/RightPanel.tsx rename to frontend/src/pages/chat/ui/right/RightPanel.tsx diff --git a/frontend/src/pages/chat/ui/components/chat/UserProfileDialog.tsx b/frontend/src/pages/chat/ui/right/UserProfileDialog.tsx similarity index 90% rename from frontend/src/pages/chat/ui/components/chat/UserProfileDialog.tsx rename to frontend/src/pages/chat/ui/right/UserProfileDialog.tsx index ef3eeeb..878ed1b 100644 --- a/frontend/src/pages/chat/ui/components/chat/UserProfileDialog.tsx +++ b/frontend/src/pages/chat/ui/right/UserProfileDialog.tsx @@ -1,8 +1,8 @@ -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"; +import type { DialogProps } from "../../../../core/types"; +import type { UserProfile } from "../../../../core/types"; +import { MaterialDialog } from "../../../../core/components/Dialog"; +import { formatTime } from "../../../../utils/utils"; +import defaultAvatar from "../../../../images/default-avatar.png"; interface UserProfileDialogProps extends DialogProps { userProfile: UserProfile | null; diff --git a/frontend/src/pages/chat/ui/components/chat/emojiData.ts b/frontend/src/pages/chat/ui/right/emojiData.ts similarity index 100% rename from frontend/src/pages/chat/ui/components/chat/emojiData.ts rename to frontend/src/pages/chat/ui/right/emojiData.ts diff --git a/frontend/src/pages/chat/ui/panels/DMPanel.ts b/frontend/src/pages/chat/ui/right/panels/DMPanel.ts similarity index 98% rename from frontend/src/pages/chat/ui/panels/DMPanel.ts rename to frontend/src/pages/chat/ui/right/panels/DMPanel.ts index 8dc8871..b8e4aa6 100644 --- a/frontend/src/pages/chat/ui/panels/DMPanel.ts +++ b/frontend/src/pages/chat/ui/right/panels/DMPanel.ts @@ -6,9 +6,9 @@ import { sendDmWithFiles, editDmEnvelope, deleteDmEnvelope -} from "../../../../core/api/dmApi"; -import type { DmEncryptedJSON, DmEnvelope, DMWebSocketMessage, EncryptedMessageJson, Message } from "../../../../core/types"; -import type { UserState } from "../state"; +} from "../../../../../core/api/dmApi"; +import type { DmEncryptedJSON, DmEnvelope, DMWebSocketMessage, EncryptedMessageJson, Message } from "../../../../../core/types"; +import type { UserState } from "../../../state"; export interface DMPanelData { userId: number; diff --git a/frontend/src/pages/chat/ui/panels/MessagePanel.ts b/frontend/src/pages/chat/ui/right/panels/MessagePanel.ts similarity index 98% rename from frontend/src/pages/chat/ui/panels/MessagePanel.ts rename to frontend/src/pages/chat/ui/right/panels/MessagePanel.ts index f8b44cc..c291d03 100644 --- a/frontend/src/pages/chat/ui/panels/MessagePanel.ts +++ b/frontend/src/pages/chat/ui/right/panels/MessagePanel.ts @@ -1,5 +1,5 @@ -import type { Message, WebSocketMessage } from "../../../../core/types"; -import type { UserState } from "../state"; +import type { Message, WebSocketMessage } from "../../../../../core/types"; +import type { UserState } from "../../../state"; export interface MessagePanelState { id: string; diff --git a/frontend/src/pages/chat/ui/panels/PublicChatPanel.ts b/frontend/src/pages/chat/ui/right/panels/PublicChatPanel.ts similarity index 95% rename from frontend/src/pages/chat/ui/panels/PublicChatPanel.ts rename to frontend/src/pages/chat/ui/right/panels/PublicChatPanel.ts index a1bf903..7f9c085 100644 --- a/frontend/src/pages/chat/ui/panels/PublicChatPanel.ts +++ b/frontend/src/pages/chat/ui/right/panels/PublicChatPanel.ts @@ -1,9 +1,9 @@ import { MessagePanel } from "./MessagePanel"; -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"; +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 { private messagesLoaded: boolean = false; diff --git a/frontend/src/pages/home/HomePage.tsx b/frontend/src/pages/home/HomePage.tsx index f6311f2..3df3874 100644 --- a/frontend/src/pages/home/HomePage.tsx +++ b/frontend/src/pages/home/HomePage.tsx @@ -1,5 +1,5 @@ import { Navigate, useNavigate } from "react-router-dom"; -import { useAppState } from "../chat/ui/state"; +import { useAppState } from "../chat/state"; import { isElectron } from "../../core/electron/electron"; import "./home.scss"; From b3303f2aef42db66eb671cd184af154657065e86 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Fri, 10 Oct 2025 11:25:45 +0300 Subject: [PATCH 08/10] Remove .. relative imports and replace with absolute paths --- frontend/src/core/api/authApi.ts | 10 +++++----- frontend/src/core/api/dmApi.ts | 14 ++++++------- frontend/src/core/api/profileApi.ts | 4 ++-- frontend/src/core/components/Dialog.tsx | 4 ++-- .../push-notifications/push-notifications.ts | 8 ++++---- frontend/src/core/websocket.ts | 2 +- frontend/src/pages/auth/LoginPage.tsx | 14 ++++++------- frontend/src/pages/auth/RegisterPage.tsx | 10 +++++----- frontend/src/pages/chat/hooks/useDM.ts | 6 +++--- frontend/src/pages/chat/hooks/useProfile.ts | 6 +++--- frontend/src/pages/chat/state.ts | 14 ++++++------- frontend/src/pages/chat/ui/ChatPage.tsx | 2 +- .../src/pages/chat/ui/left/ChatHeader.tsx | 6 +++--- frontend/src/pages/chat/ui/left/ChatTabs.tsx | 2 +- .../src/pages/chat/ui/left/DMUsersList.tsx | 8 ++++---- frontend/src/pages/chat/ui/left/LeftPanel.tsx | 8 ++++---- .../chat/ui/left/profile/ImageCropper.tsx | 2 +- .../chat/ui/left/profile/ProfileDialog.tsx | 10 +++++----- .../chat/ui/left/settings/SettingsDialog.tsx | 14 ++++++------- .../pages/chat/ui/right/ChatInputWrapper.tsx | 10 +++++----- .../pages/chat/ui/right/ChatMainHeader.tsx | 4 ++-- .../src/pages/chat/ui/right/ChatMessages.tsx | 16 +++++++-------- .../src/pages/chat/ui/right/EmojiMenu.tsx | 2 +- frontend/src/pages/chat/ui/right/Message.tsx | 20 +++++++++---------- .../chat/ui/right/MessageContextMenu.tsx | 2 +- .../chat/ui/right/MessagePanelRenderer.tsx | 10 +++++----- .../src/pages/chat/ui/right/RightPanel.tsx | 2 +- .../pages/chat/ui/right/UserProfileDialog.tsx | 10 +++++----- .../src/pages/chat/ui/right/panels/DMPanel.ts | 4 ++-- .../chat/ui/right/panels/MessagePanel.ts | 4 ++-- .../chat/ui/right/panels/PublicChatPanel.ts | 10 +++++----- frontend/src/pages/home/HomePage.tsx | 4 ++-- frontend/tsconfig.json | 6 ++++++ frontend/vite.config.ts | 6 ++++++ 34 files changed, 133 insertions(+), 121 deletions(-) diff --git a/frontend/src/core/api/authApi.ts b/frontend/src/core/api/authApi.ts index a104e4f..28311a7 100644 --- a/frontend/src/core/api/authApi.ts +++ b/frontend/src/core/api/authApi.ts @@ -1,8 +1,8 @@ -import type { Headers, UploadPublicKeyRequest, BackupBlob } from "../types"; -import { generateX25519KeyPair } from "../../utils/crypto/asymmetric"; -import { encodeBlob, encryptBackupWithPassword, decryptBackupWithPassword, decodeBlob } from "../../utils/crypto/backup"; -import { b64, ub64 } from "../../utils/utils"; -import { API_BASE_URL } from "../config"; +import type { Headers, UploadPublicKeyRequest, BackupBlob } from "@/core/types"; +import { generateX25519KeyPair } from "@/utils/crypto/asymmetric"; +import { encodeBlob, encryptBackupWithPassword, decryptBackupWithPassword, decodeBlob } from "@/utils/crypto/backup"; +import { b64, ub64 } from "@/utils/utils"; +import { API_BASE_URL } from "@/core/config"; /** * Generates authentication headers for API requests diff --git a/frontend/src/core/api/dmApi.ts b/frontend/src/core/api/dmApi.ts index 59efea4..629285a 100644 --- a/frontend/src/core/api/dmApi.ts +++ b/frontend/src/core/api/dmApi.ts @@ -1,12 +1,12 @@ -import { API_BASE_URL } from "../config"; +import { API_BASE_URL } from "@/core/config"; import { getAuthHeaders } from "./authApi"; -import { ecdhSharedSecret, deriveWrappingKey } from "../../utils/crypto/asymmetric"; -import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../../utils/crypto/symmetric"; -import { randomBytes } from "../../utils/crypto/kdf"; +import { ecdhSharedSecret, deriveWrappingKey } from "@/utils/crypto/asymmetric"; +import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "@/utils/crypto/symmetric"; +import { randomBytes } from "@/utils/crypto/kdf"; import { getCurrentKeys } from "./authApi"; -import { request } from "../websocket"; -import type { SendDMRequest, DmEnvelope, User, DMEditRequest, DmEncryptedJSON, BaseDmEnvelope } from "../types"; -import { b64, ub64 } from "../../utils/utils"; +import { request } from "@/core/websocket"; +import type { SendDMRequest, DmEnvelope, User, DMEditRequest, DmEncryptedJSON, BaseDmEnvelope } from "@/core/types"; +import { b64, ub64 } from "@/utils/utils"; export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string): Promise { const keys = getCurrentKeys(); diff --git a/frontend/src/core/api/profileApi.ts b/frontend/src/core/api/profileApi.ts index 9755bdd..2e5a036 100644 --- a/frontend/src/core/api/profileApi.ts +++ b/frontend/src/core/api/profileApi.ts @@ -1,6 +1,6 @@ import { getAuthHeaders } from "./authApi"; -import { API_BASE_URL } from "../config"; -import type { UserProfile } from "../types"; +import { API_BASE_URL } from "@/core/config"; +import type { UserProfile } from "@/core/types"; export interface ProfileData { profile_picture?: string; diff --git a/frontend/src/core/components/Dialog.tsx b/frontend/src/core/components/Dialog.tsx index 35ef39b..b2247c9 100644 --- a/frontend/src/core/components/Dialog.tsx +++ b/frontend/src/core/components/Dialog.tsx @@ -1,8 +1,8 @@ import type { Dialog as MduiDialog } from "mdui/components/dialog"; import { useEffect, type Ref } from "react" import { createPortal } from "react-dom"; -import { id } from "../../utils/utils"; -import useCombinedRefs from "../hooks/useCombinedRefs"; +import { id } from "@/utils/utils"; +import useCombinedRefs from "@/core/hooks/useCombinedRefs"; export interface BaseDialogProps { onOpenChange: (value: boolean) => void; diff --git a/frontend/src/core/push-notifications/push-notifications.ts b/frontend/src/core/push-notifications/push-notifications.ts index e3edd2b..6c265f8 100644 --- a/frontend/src/core/push-notifications/push-notifications.ts +++ b/frontend/src/core/push-notifications/push-notifications.ts @@ -1,7 +1,7 @@ -import { API_BASE_URL } from "../config"; -import { isElectron } from "../electron/electron"; -import { websocket } from "../websocket"; -import type { NewMessageWebSocketMessage, WebSocketMessage } from "../types"; +import { API_BASE_URL } from "@/core/config"; +import { isElectron } from "@/core/electron/electron"; +import { websocket } from "@/core/websocket"; +import type { NewMessageWebSocketMessage, WebSocketMessage } from "@/core/types"; import serviceWorker from "./service-worker?worker&url"; export interface PushSubscriptionData { diff --git a/frontend/src/core/websocket.ts b/frontend/src/core/websocket.ts index c881cf3..4ec865b 100644 --- a/frontend/src/core/websocket.ts +++ b/frontend/src/core/websocket.ts @@ -7,7 +7,7 @@ import { API_WS_BASE_URL } from "./config"; import type { WebSocketMessage } from "./types"; -import { delay } from "../utils/utils"; +import { delay } from "@/utils/utils"; /** * Creates a new WebSocket connection to the chat server diff --git a/frontend/src/pages/auth/LoginPage.tsx b/frontend/src/pages/auth/LoginPage.tsx index 78e7cd8..4ff1004 100644 --- a/frontend/src/pages/auth/LoginPage.tsx +++ b/frontend/src/pages/auth/LoginPage.tsx @@ -1,15 +1,15 @@ import { useImmer } from "use-immer"; import { AlertsContainer, type Alert, type AlertType } from "./Auth"; import { AuthContainer, AuthHeader } from "./Auth"; -import type { ErrorResponse, LoginRequest, LoginResponse } from "../../core/types"; -import { ensureKeysOnLogin } from "../../core/api/authApi"; -import { API_BASE_URL } from "../../core/config"; +import type { ErrorResponse, LoginRequest, LoginResponse } from "@/core/types"; +import { ensureKeysOnLogin } from "@/core/api/authApi"; +import { API_BASE_URL } from "@/core/config"; import { useRef } from "react"; import type { TextField } from "mdui/components/text-field"; -import { useAppState } from "../chat/state"; -import { MaterialTextField } from "../../core/components/TextField"; -import { initialize, isSupported, startElectronReceiver, subscribe } from "../../core/push-notifications/push-notifications"; -import { isElectron } from "../../core/electron/electron"; +import { useAppState } from "@/pages/chat/state"; +import { MaterialTextField } from "@/core/components/TextField"; +import { initialize, isSupported, startElectronReceiver, subscribe } from "@/core/push-notifications/push-notifications"; +import { isElectron } from "@/core/electron/electron"; import { useNavigate } from "react-router-dom"; import "./auth.scss"; diff --git a/frontend/src/pages/auth/RegisterPage.tsx b/frontend/src/pages/auth/RegisterPage.tsx index f484e51..5e5f000 100644 --- a/frontend/src/pages/auth/RegisterPage.tsx +++ b/frontend/src/pages/auth/RegisterPage.tsx @@ -3,11 +3,11 @@ import { AuthContainer, AuthHeader } from "./Auth"; import { AlertsContainer, type Alert, type AlertType } from "./Auth"; 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 "../chat/state"; -import { MaterialTextField } from "../../core/components/TextField"; -import { ensureKeysOnLogin } from "../../core/api/authApi"; +import type { ErrorResponse, RegisterRequest, LoginResponse } from "@/core/types"; +import { API_BASE_URL } from "@/core/config"; +import { useAppState } from "@/pages/chat/state"; +import { MaterialTextField } from "@/core/components/TextField"; +import { ensureKeysOnLogin } from "@/core/api/authApi"; import { useNavigate } from "react-router-dom"; import "./auth.scss"; diff --git a/frontend/src/pages/chat/hooks/useDM.ts b/frontend/src/pages/chat/hooks/useDM.ts index f3db530..189ef64 100644 --- a/frontend/src/pages/chat/hooks/useDM.ts +++ b/frontend/src/pages/chat/hooks/useDM.ts @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback, useRef } from "react"; -import { useAppState } from "../state"; +import { useAppState } from "@/pages/chat/state"; import { fetchUsers, fetchUserPublicKey, @@ -7,8 +7,8 @@ import { decryptDm, sendDMViaWebSocket } from "../../../core/api/dmApi"; -import type { User, Message, DmEncryptedJSON } from "../../../core/types"; -import { websocket } from "../../../core/websocket"; +import type { User, Message, DmEncryptedJSON } from "@/core/types"; +import { websocket } from "@/core/websocket"; export interface DMUser extends User { lastMessage?: string; diff --git a/frontend/src/pages/chat/hooks/useProfile.ts b/frontend/src/pages/chat/hooks/useProfile.ts index c4c2764..3e0eb65 100644 --- a/frontend/src/pages/chat/hooks/useProfile.ts +++ b/frontend/src/pages/chat/hooks/useProfile.ts @@ -1,7 +1,7 @@ import { useState, useCallback, useEffect } from "react"; -import { useAppState } from "../state"; -import { loadProfile, updateProfile, uploadProfilePicture, type ProfileData } from "../../../core/api/profileApi"; -import { showSuccess, showError } from "../../../utils/notification"; +import { useAppState } from "@/pages/chat/state"; +import { loadProfile, updateProfile, uploadProfilePicture, type ProfileData } from "@/core/api/profileApi"; +import { showSuccess, showError } from "@/utils/notification"; export default function useProfile() { const { user } = useAppState(); diff --git a/frontend/src/pages/chat/state.ts b/frontend/src/pages/chat/state.ts index a7eff88..67bf059 100644 --- a/frontend/src/pages/chat/state.ts +++ b/frontend/src/pages/chat/state.ts @@ -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 "./ui/right/panels/MessagePanel"; import { PublicChatPanel } from "./ui/right/panels/PublicChatPanel"; import { DMPanel, type DMPanelData } from "./ui/right/panels/DMPanel"; -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"; +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" diff --git a/frontend/src/pages/chat/ui/ChatPage.tsx b/frontend/src/pages/chat/ui/ChatPage.tsx index b5024e9..9bc44fe 100644 --- a/frontend/src/pages/chat/ui/ChatPage.tsx +++ b/frontend/src/pages/chat/ui/ChatPage.tsx @@ -1,6 +1,6 @@ import { LeftPanel } from "./left/LeftPanel"; import { RightPanel } from "./right/RightPanel"; -import "../css/chat.scss"; +import "@/pages/chat/css/chat.scss"; export default function ChatPage() { return ( diff --git a/frontend/src/pages/chat/ui/left/ChatHeader.tsx b/frontend/src/pages/chat/ui/left/ChatHeader.tsx index b94297e..a76bc15 100644 --- a/frontend/src/pages/chat/ui/left/ChatHeader.tsx +++ b/frontend/src/pages/chat/ui/left/ChatHeader.tsx @@ -1,6 +1,6 @@ -import { PRODUCT_NAME } from "../../../../core/config"; -import useProfile from "../../hooks/useProfile"; -import defaultAvatar from "../../../resources/images/default-avatar.png"; +import { PRODUCT_NAME } from "@/core/config"; +import useProfile from "@/pages/chat/hooks/useProfile"; +import defaultAvatar from "@/images/default-avatar.png"; import { useState } from "react"; import { ProfileDialog } from "./profile/ProfileDialog"; diff --git a/frontend/src/pages/chat/ui/left/ChatTabs.tsx b/frontend/src/pages/chat/ui/left/ChatTabs.tsx index 9625567..bf5bb8f 100644 --- a/frontend/src/pages/chat/ui/left/ChatTabs.tsx +++ b/frontend/src/pages/chat/ui/left/ChatTabs.tsx @@ -1,4 +1,4 @@ -import { useAppState } from "../../state"; +import { useAppState } from "@/pages/chat/state"; export function ChatTabs() { const { chat, setActiveTab, switchToPublicChat } = useAppState(); diff --git a/frontend/src/pages/chat/ui/left/DMUsersList.tsx b/frontend/src/pages/chat/ui/left/DMUsersList.tsx index 022532d..6c25f09 100644 --- a/frontend/src/pages/chat/ui/left/DMUsersList.tsx +++ b/frontend/src/pages/chat/ui/left/DMUsersList.tsx @@ -1,8 +1,8 @@ import { useEffect } from "react"; -import { useDM, type DMUser } from "../../hooks/useDM"; -import { useAppState } from "../../state"; -import { fetchUserPublicKey } from "../../../../core/api/dmApi"; -import defaultAvatar from "../../../../images/default-avatar.png"; +import { useDM, type DMUser } from "@/pages/chat/hooks/useDM"; +import { useAppState } from "@/pages/chat/state"; +import { fetchUserPublicKey } from "@/core/api/dmApi"; +import defaultAvatar from "@/images/default-avatar.png"; export function DMUsersList() { const { dmUsers, isLoadingUsers, loadUsers } = useDM(); diff --git a/frontend/src/pages/chat/ui/left/LeftPanel.tsx b/frontend/src/pages/chat/ui/left/LeftPanel.tsx index a74a8aa..a6bf4d8 100644 --- a/frontend/src/pages/chat/ui/left/LeftPanel.tsx +++ b/frontend/src/pages/chat/ui/left/LeftPanel.tsx @@ -1,12 +1,12 @@ -import { PRODUCT_NAME } from "../../../../core/config"; -import { useAppState } from "../../state"; -import defaultAvatar from "../../../../images/default-avatar.png"; +import { PRODUCT_NAME } from "@/core/config"; +import { useAppState } from "@/pages/chat/state"; +import defaultAvatar from "@/images/default-avatar.png"; import { useState, type FormEvent } from "react"; import { ProfileDialog } from "./profile/ProfileDialog"; import { SettingsDialog } from "./settings/SettingsDialog"; import { DMUsersList } from "./DMUsersList"; import type { Tabs } from "mdui"; -import type { ChatTabs } from "../../state"; +import type { ChatTabs } from "@/pages/chat/state"; function BottomAppBar() { const [settingsOpen, onSettingsOpenChange] = useState(false); diff --git a/frontend/src/pages/chat/ui/left/profile/ImageCropper.tsx b/frontend/src/pages/chat/ui/left/profile/ImageCropper.tsx index 805b290..36ceb00 100644 --- a/frontend/src/pages/chat/ui/left/profile/ImageCropper.tsx +++ b/frontend/src/pages/chat/ui/left/profile/ImageCropper.tsx @@ -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; diff --git a/frontend/src/pages/chat/ui/left/profile/ProfileDialog.tsx b/frontend/src/pages/chat/ui/left/profile/ProfileDialog.tsx index 1ad53de..5d5414c 100644 --- a/frontend/src/pages/chat/ui/left/profile/ProfileDialog.tsx +++ b/frontend/src/pages/chat/ui/left/profile/ProfileDialog.tsx @@ -1,11 +1,11 @@ import { useState, useEffect, useRef, type FormEvent } from "react"; -import defaultAvatar from "../../../../../images/default-avatar.png"; +import defaultAvatar from "@/images/default-avatar.png"; import type { TextField } from "mdui/components/text-field"; -import type { DialogProps } from "../../../../../core/types"; -import { MaterialDialog } from "../../../../../core/components/Dialog"; -import useProfile from "../../../hooks/useProfile"; +import type { DialogProps } from "@/core/types"; +import { MaterialDialog } from "@/core/components/Dialog"; +import useProfile from "@/pages/chat/hooks/useProfile"; import { ImageCropper } from "./ImageCropper"; -import { MaterialTextField } from "../../../../../core/components/TextField"; +import { MaterialTextField } from "@/core/components/TextField"; export function ProfileDialog({ isOpen, onOpenChange }: DialogProps) { const { profileData, isLoading, isUpdating, updateProfileData, uploadProfilePictureData } = useProfile(); diff --git a/frontend/src/pages/chat/ui/left/settings/SettingsDialog.tsx b/frontend/src/pages/chat/ui/left/settings/SettingsDialog.tsx index 806891e..1ea0bb6 100644 --- a/frontend/src/pages/chat/ui/left/settings/SettingsDialog.tsx +++ b/frontend/src/pages/chat/ui/left/settings/SettingsDialog.tsx @@ -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 { MaterialDialog } from "../../../../../core/components/Dialog"; -import { initialize, isSupported, startElectronReceiver, stopElectronReceiver, subscribe, unsubscribe } from "../../../../../core/push-notifications/push-notifications"; -import { isElectron } from "../../../../../core/electron/electron"; -import { useAppState } from "../../../state"; +import { PRODUCT_NAME, API_BASE_URL } from "@/core/config"; +import type { DialogProps } from "@/core/types"; +import { MaterialDialog } from "@/core/components/Dialog"; +import { initialize, isSupported, startElectronReceiver, stopElectronReceiver, subscribe, unsubscribe } from "@/core/push-notifications/push-notifications"; +import { isElectron } from "@/core/electron/electron"; +import { useAppState } from "@/pages/chat/state"; import type { Switch } from "mdui/components/switch"; -import { getAuthHeaders } from "../../../../../core/api/authApi"; +import { getAuthHeaders } from "@/core/api/authApi"; export function SettingsDialog({ isOpen, onOpenChange }: DialogProps) { const [activePanel, setActivePanel] = useState("notifications-settings"); diff --git a/frontend/src/pages/chat/ui/right/ChatInputWrapper.tsx b/frontend/src/pages/chat/ui/right/ChatInputWrapper.tsx index eeccf6e..59ed54c 100644 --- a/frontend/src/pages/chat/ui/right/ChatInputWrapper.tsx +++ b/frontend/src/pages/chat/ui/right/ChatInputWrapper.tsx @@ -1,9 +1,9 @@ import { useState, useEffect, useRef } from "react"; -import { MaterialDialog } from "../../../../core/components/Dialog"; -import { RichTextArea } from "../../../../core/components/RichTextArea"; -import type { Message } from "../../../../core/types"; -import Quote from "../../../../core/components/Quote"; -import AnimatedHeight from "../../../../core/components/animations/AnimatedHeight"; +import { MaterialDialog } from "@/core/components/Dialog"; +import { RichTextArea } from "@/core/components/RichTextArea"; +import type { Message } from "@/core/types"; +import Quote from "@/core/components/Quote"; +import AnimatedHeight from "@/core/components/animations/AnimatedHeight"; import { useImmer } from "use-immer"; import { EmojiMenu } from "./EmojiMenu"; diff --git a/frontend/src/pages/chat/ui/right/ChatMainHeader.tsx b/frontend/src/pages/chat/ui/right/ChatMainHeader.tsx index 19720bb..09b5635 100644 --- a/frontend/src/pages/chat/ui/right/ChatMainHeader.tsx +++ b/frontend/src/pages/chat/ui/right/ChatMainHeader.tsx @@ -1,5 +1,5 @@ -import { useAppState } from "../../state"; -import defaultAvatar from "../../../resources/images/default-avatar.png"; +import { useAppState } from "@/pages/chat/state"; +import defaultAvatar from "@/images/default-avatar.png"; export function ChatMainHeader() { const { currentChat } = useAppState().chat; diff --git a/frontend/src/pages/chat/ui/right/ChatMessages.tsx b/frontend/src/pages/chat/ui/right/ChatMessages.tsx index 3d8d8f7..25b5cc3 100644 --- a/frontend/src/pages/chat/ui/right/ChatMessages.tsx +++ b/frontend/src/pages/chat/ui/right/ChatMessages.tsx @@ -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 { useAppState } from "@/pages/chat/state"; +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 "../../../../core/api/profileApi"; +import { fetchUserProfile } from "@/core/api/profileApi"; import { useEffect, useState, type ReactNode } from "react"; -import { delay } from "../../../../utils/utils"; -import { MaterialDialog } from "../../../../core/components/Dialog"; -import { request } from "../../../../core/websocket"; -import type { AddReactionRequest, AddDmReactionRequest } from "../../../../core/types"; +import { delay } from "@/utils/utils"; +import { MaterialDialog } from "@/core/components/Dialog"; +import { request } from "@/core/websocket"; +import type { AddReactionRequest, AddDmReactionRequest } from "@/core/types"; interface ChatMessagesProps { messages?: MessageType[]; diff --git a/frontend/src/pages/chat/ui/right/EmojiMenu.tsx b/frontend/src/pages/chat/ui/right/EmojiMenu.tsx index 196166c..9b1c4f5 100644 --- a/frontend/src/pages/chat/ui/right/EmojiMenu.tsx +++ b/frontend/src/pages/chat/ui/right/EmojiMenu.tsx @@ -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; diff --git a/frontend/src/pages/chat/ui/right/Message.tsx b/frontend/src/pages/chat/ui/right/Message.tsx index a7e8cbc..025528f 100644 --- a/frontend/src/pages/chat/ui/right/Message.tsx +++ b/frontend/src/pages/chat/ui/right/Message.tsx @@ -1,16 +1,16 @@ -import { formatTime, id } from "../../../../utils/utils"; -import type { Attachment, Message as MessageType, Reaction } from "../../../../core/types"; -import defaultAvatar from "../../../../images/default-avatar.png"; -import Quote from "../../../../core/components/Quote"; +import { formatTime, id } from "@/utils/utils"; +import type { Attachment, Message as MessageType, Reaction } from "@/core/types"; +import defaultAvatar from "@/images/default-avatar.png"; +import Quote from "@/core/components/Quote"; import { parse } from "marked"; import DOMPurify from "dompurify"; import { useEffect, useState, useRef } from "react"; -import { getCurrentKeys } from "../../../../core/api/authApi"; -import { ecdhSharedSecret, deriveWrappingKey } from "../../../../utils/crypto/asymmetric"; -import { importAesGcmKey, aesGcmDecrypt } from "../../../../utils/crypto/symmetric"; -import { getAuthHeaders } from "../../../../core/api/authApi"; -import { useAppState } from "../../state"; -import { ub64 } from "../../../../utils/utils"; +import { getCurrentKeys } from "@/core/api/authApi"; +import { ecdhSharedSecret, deriveWrappingKey } from "@/utils/crypto/asymmetric"; +import { importAesGcmKey, aesGcmDecrypt } from "@/utils/crypto/symmetric"; +import { getAuthHeaders } from "@/core/api/authApi"; +import { useAppState } from "@/pages/chat/state"; +import { ub64 } from "@/utils/utils"; import { useImmer } from "use-immer"; import { createPortal } from "react-dom"; diff --git a/frontend/src/pages/chat/ui/right/MessageContextMenu.tsx b/frontend/src/pages/chat/ui/right/MessageContextMenu.tsx index c13677e..8405aa5 100644 --- a/frontend/src/pages/chat/ui/right/MessageContextMenu.tsx +++ b/frontend/src/pages/chat/ui/right/MessageContextMenu.tsx @@ -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 { diff --git a/frontend/src/pages/chat/ui/right/MessagePanelRenderer.tsx b/frontend/src/pages/chat/ui/right/MessagePanelRenderer.tsx index 4c5ed43..c9bb259 100644 --- a/frontend/src/pages/chat/ui/right/MessagePanelRenderer.tsx +++ b/frontend/src/pages/chat/ui/right/MessagePanelRenderer.tsx @@ -1,12 +1,12 @@ import { useState, useEffect, useRef } from "react"; -import { useAppState } from "../../state"; +import { useAppState } from "@/pages/chat/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 defaultAvatar from "../../../../images/default-avatar.png"; -import AnimatedOpacity from "../../../../core/components/animations/AnimatedOpacity"; +import { setGlobalMessageHandler } from "@/core/websocket"; +import type { Message, WebSocketMessage } from "@/core/types"; +import defaultAvatar from "@/images/default-avatar.png"; +import AnimatedOpacity from "@/core/components/animations/AnimatedOpacity"; import type { DMPanel } from "./panels/DMPanel"; interface MessagePanelRendererProps { diff --git a/frontend/src/pages/chat/ui/right/RightPanel.tsx b/frontend/src/pages/chat/ui/right/RightPanel.tsx index 05c0d46..278b091 100644 --- a/frontend/src/pages/chat/ui/right/RightPanel.tsx +++ b/frontend/src/pages/chat/ui/right/RightPanel.tsx @@ -1,4 +1,4 @@ -import { useAppState } from "../../state"; +import { useAppState } from "@/pages/chat/state"; import { MessagePanelRenderer } from "./MessagePanelRenderer"; export function RightPanel() { diff --git a/frontend/src/pages/chat/ui/right/UserProfileDialog.tsx b/frontend/src/pages/chat/ui/right/UserProfileDialog.tsx index 878ed1b..2e890f9 100644 --- a/frontend/src/pages/chat/ui/right/UserProfileDialog.tsx +++ b/frontend/src/pages/chat/ui/right/UserProfileDialog.tsx @@ -1,8 +1,8 @@ -import type { DialogProps } from "../../../../core/types"; -import type { UserProfile } from "../../../../core/types"; -import { MaterialDialog } from "../../../../core/components/Dialog"; -import { formatTime } from "../../../../utils/utils"; -import defaultAvatar from "../../../../images/default-avatar.png"; +import type { DialogProps } from "@/core/types"; +import type { UserProfile } from "@/core/types"; +import { MaterialDialog } from "@/core/components/Dialog"; +import { formatTime } from "@/utils/utils"; +import defaultAvatar from "@/images/default-avatar.png"; interface UserProfileDialogProps extends DialogProps { userProfile: UserProfile | null; diff --git a/frontend/src/pages/chat/ui/right/panels/DMPanel.ts b/frontend/src/pages/chat/ui/right/panels/DMPanel.ts index b8e4aa6..bfc7b47 100644 --- a/frontend/src/pages/chat/ui/right/panels/DMPanel.ts +++ b/frontend/src/pages/chat/ui/right/panels/DMPanel.ts @@ -7,8 +7,8 @@ import { editDmEnvelope, deleteDmEnvelope } from "../../../../../core/api/dmApi"; -import type { DmEncryptedJSON, DmEnvelope, DMWebSocketMessage, EncryptedMessageJson, Message } from "../../../../../core/types"; -import type { UserState } from "../../../state"; +import type { DmEncryptedJSON, DmEnvelope, DMWebSocketMessage, EncryptedMessageJson, Message } from "@/core/types"; +import type { UserState } from "@/pages/chat/state"; export interface DMPanelData { userId: number; diff --git a/frontend/src/pages/chat/ui/right/panels/MessagePanel.ts b/frontend/src/pages/chat/ui/right/panels/MessagePanel.ts index c291d03..5357e16 100644 --- a/frontend/src/pages/chat/ui/right/panels/MessagePanel.ts +++ b/frontend/src/pages/chat/ui/right/panels/MessagePanel.ts @@ -1,5 +1,5 @@ -import type { Message, WebSocketMessage } from "../../../../../core/types"; -import type { UserState } from "../../../state"; +import type { Message, WebSocketMessage } from "@/core/types"; +import type { UserState } from "@/pages/chat/state"; export interface MessagePanelState { id: string; diff --git a/frontend/src/pages/chat/ui/right/panels/PublicChatPanel.ts b/frontend/src/pages/chat/ui/right/panels/PublicChatPanel.ts index 7f9c085..86da1d2 100644 --- a/frontend/src/pages/chat/ui/right/panels/PublicChatPanel.ts +++ b/frontend/src/pages/chat/ui/right/panels/PublicChatPanel.ts @@ -1,9 +1,9 @@ import { MessagePanel } from "./MessagePanel"; -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"; +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 "@/pages/chat/state"; export class PublicChatPanel extends MessagePanel { private messagesLoaded: boolean = false; diff --git a/frontend/src/pages/home/HomePage.tsx b/frontend/src/pages/home/HomePage.tsx index 3df3874..9ed2c3c 100644 --- a/frontend/src/pages/home/HomePage.tsx +++ b/frontend/src/pages/home/HomePage.tsx @@ -1,6 +1,6 @@ import { Navigate, useNavigate } from "react-router-dom"; -import { useAppState } from "../chat/state"; -import { isElectron } from "../../core/electron/electron"; +import { useAppState } from "@/pages/chat/state"; +import { isElectron } from "@/core/electron/electron"; import "./home.scss"; function GitHubLink({ children }: { children: React.ReactNode }) { diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index ad5ff1c..4189505 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -13,6 +13,12 @@ "moduleDetection": "force", "noEmit": true, + /* Path mapping */ + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + /* Linting */ "strict": true, "erasableSyntaxOnly": true, diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index bb5d989..3c46f4c 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -3,6 +3,7 @@ import { createHtmlPlugin } from "vite-plugin-html"; import autoprefixer from "autoprefixer"; import electron from "vite-plugin-electron/simple"; import react from "@vitejs/plugin-react"; +import path from "path"; const plugins: PluginOption[] = [ react(), @@ -46,6 +47,11 @@ if (process.env.VITE_ELECTRON) { export default defineConfig({ plugins: plugins, + resolve: { + alias: { + "@": path.resolve(__dirname, "./src") + } + }, server: { host: '0.0.0.0', port: 8301, From 046855395af1e547e25a083a21266d5c74d0ab05 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Fri, 10 Oct 2025 11:34:11 +0300 Subject: [PATCH 09/10] Add Cursor command --- .cursor/commands/restructure.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 .cursor/commands/restructure.md diff --git a/.cursor/commands/restructure.md b/.cursor/commands/restructure.md new file mode 100644 index 0000000..9dbfa83 --- /dev/null +++ b/.cursor/commands/restructure.md @@ -0,0 +1 @@ +Analyze my codebase and think how it could be better organized, like a better folder or code structure. \ No newline at end of file From 37cc78be78f7bc4d43b6890f8472bd90ee10c424 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Fri, 10 Oct 2025 12:06:02 +0300 Subject: [PATCH 10/10] Improve bundle size optimization --- frontend/src/App.tsx | 44 ++++++++++++++++++++++++---------- frontend/src/main.tsx | 1 - frontend/src/utils/material.ts | 1 + frontend/vite.config.ts | 10 +++++++- package.json | 3 ++- 5 files changed, 43 insertions(+), 16 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2f420a7..4a9d629 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,17 +1,19 @@ import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import { ElectronTitleBar } from "./Electron"; import { useAppState } from "./pages/chat/state"; -import { useEffect, useState } from "react"; -import HomePage from "./pages/home/HomePage"; -import LoginPage from "./pages/auth/LoginPage"; -import RegisterPage from "./pages/auth/RegisterPage"; -import ChatPage from "./pages/chat/ui/ChatPage"; -import DownloadAppPage from "./pages/download-app/DownloadAppPage"; -import NotFoundPage from "./pages/not-found/NotFoundPage"; -import ProtectedRoute from "./pages/ProtectedRoute"; +import { useEffect, useState, lazy, Suspense } from "react"; import { isElectron } from "./core/electron/electron"; import { MINIMUM_WIDTH } from "./core/config"; import useWindowSize from "./core/hooks/useWindowSize"; +import ProtectedRoute from "./pages/ProtectedRoute"; +import NotFoundPage from "./pages/not-found/NotFoundPage"; +import DownloadAppPage from "./pages/download-app/DownloadAppPage"; + +// Lazy load route components +const HomePage = lazy(() => import("./pages/home/HomePage")); +const LoginPage = lazy(() => import("./pages/auth/LoginPage")); +const RegisterPage = lazy(() => import("./pages/auth/RegisterPage")); +const ChatPage = lazy(() => import("./pages/chat/ui/ChatPage")); export default function App() { const { restoreUserFromStorage } = useAppState(); @@ -31,10 +33,24 @@ export default function App() {
- } /> - } /> - } /> - } /> + + + + } /> + + + + } /> + + + + } /> + + } /> @@ -42,7 +58,9 @@ export default function App() { } /> - } /> + + } />
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index d89d1e8..83eec38 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -6,7 +6,6 @@ */ import './css/style.scss'; -import "mdui/mdui.css"; import "./utils/material"; import "./core/init"; diff --git a/frontend/src/utils/material.ts b/frontend/src/utils/material.ts index a5cdb5e..7ab2607 100644 --- a/frontend/src/utils/material.ts +++ b/frontend/src/utils/material.ts @@ -21,6 +21,7 @@ import 'mdui/components/top-app-bar'; import 'mdui/components/top-app-bar-title'; import 'mdui/components/switch'; import 'mdui/components/chip'; +import "mdui/mdui.css"; import { setColorScheme } from 'mdui/functions/setColorScheme.js'; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 3c46f4c..6548add 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -4,6 +4,9 @@ import autoprefixer from "autoprefixer"; import electron from "vite-plugin-electron/simple"; import react from "@vitejs/plugin-react"; import path from "path"; +import { visualizer } from 'rollup-plugin-visualizer'; + +const outDir = process.env.VITE_ELECTRON ? "build/electron/dist" : "build/normal/dist"; const plugins: PluginOption[] = [ react(), @@ -18,6 +21,11 @@ const plugins: PluginOption[] = [ minifyCSS: true, minifyJS: true } + }), + visualizer({ + filename: `${outDir}/stats.html`, + open: true, + gzipSize: true }) ] @@ -85,7 +93,7 @@ export default defineConfig({ }, cssMinify: true, assetsInlineLimit: 0, - outDir: process.env.VITE_ELECTRON ? "build/electron/dist" : "build/normal/dist", + outDir: outDir, chunkSizeWarningLimit: 1024 } }); \ No newline at end of file diff --git a/package.json b/package.json index 4fb02cb..c52d0e9 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "FromChat", + "name": "fromchat", "private": true, "version": "0.0.0", "type": "module", @@ -53,6 +53,7 @@ "electron": "^38.1.2", "husky": "^9.1.7", "postcss": "^8.5.6", + "rollup-plugin-visualizer": "^6.0.4", "sass-embedded": "^1.93.0", "terser": "^5.44.0", "typescript": "~5.9.2",