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 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/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/App.tsx b/frontend/src/App.tsx index 15466af..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 "./pages/app/ui/components/Electron"; -import { useAppState } from "./pages/app/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 { ElectronTitleBar } from "./Electron"; +import { useAppState } from "./pages/chat/state"; +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 { isElectron } from "./pages/app/electron/electron"; -import { MINIMUM_WIDTH } from "./pages/app/core/config"; -import useWindowSize from "./pages/app/ui/hooks/useWindowSize"; +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() {
- z} /> - } /> - } /> - } /> + + + + } /> + + + + } /> + + + + } /> + + } /> @@ -42,7 +58,9 @@ export default function App() { } /> - } /> + + } />
diff --git a/frontend/src/pages/app/ui/components/Electron.tsx b/frontend/src/Electron.tsx similarity index 73% rename from frontend/src/pages/app/ui/components/Electron.tsx rename to frontend/src/Electron.tsx index 1052454..03c2522 100644 --- a/frontend/src/pages/app/ui/components/Electron.tsx +++ b/frontend/src/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/app/auth/crypto.ts b/frontend/src/core/api/authApi.ts similarity index 81% rename from frontend/src/pages/app/auth/crypto.ts rename to frontend/src/core/api/authApi.ts index 1e16ff1..28311a7 100644 --- a/frontend/src/pages/app/auth/crypto.ts +++ b/frontend/src/core/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 "@/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 + * @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/core/api/dmApi.ts similarity index 92% rename from frontend/src/pages/app/api/dmApi.ts rename to frontend/src/core/api/dmApi.ts index 84d6347..629285a 100644 --- a/frontend/src/pages/app/api/dmApi.ts +++ b/frontend/src/core/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 "@/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 { getCurrentKeys } from "./authApi"; +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/pages/app/api/profileApi.ts b/frontend/src/core/api/profileApi.ts similarity index 95% rename from frontend/src/pages/app/api/profileApi.ts rename to frontend/src/core/api/profileApi.ts index a555821..2e5a036 100644 --- a/frontend/src/pages/app/api/profileApi.ts +++ b/frontend/src/core/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 "@/core/config"; +import type { UserProfile } from "@/core/types"; export interface ProfileData { profile_picture?: string; diff --git a/frontend/src/pages/app/ui/components/core/Dialog.tsx b/frontend/src/core/components/Dialog.tsx similarity index 93% rename from frontend/src/pages/app/ui/components/core/Dialog.tsx rename to frontend/src/core/components/Dialog.tsx index d339d7f..b2247c9 100644 --- a/frontend/src/pages/app/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 "@/core/hooks/useCombinedRefs"; export interface BaseDialogProps { onOpenChange: (value: boolean) => void; diff --git a/frontend/src/pages/app/ui/components/core/Quote.tsx b/frontend/src/core/components/Quote.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/core/Quote.tsx rename to frontend/src/core/components/Quote.tsx diff --git a/frontend/src/pages/app/ui/components/core/RichTextArea.tsx b/frontend/src/core/components/RichTextArea.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/core/RichTextArea.tsx rename to frontend/src/core/components/RichTextArea.tsx diff --git a/frontend/src/pages/app/ui/components/core/TextField.tsx b/frontend/src/core/components/TextField.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/core/TextField.tsx rename to frontend/src/core/components/TextField.tsx diff --git a/frontend/src/pages/app/ui/components/core/animations/AnimatedHeight.tsx b/frontend/src/core/components/animations/AnimatedHeight.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/core/animations/AnimatedHeight.tsx rename to frontend/src/core/components/animations/AnimatedHeight.tsx diff --git a/frontend/src/pages/app/ui/components/core/animations/AnimatedOpacity.tsx b/frontend/src/core/components/animations/AnimatedOpacity.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/core/animations/AnimatedOpacity.tsx rename to frontend/src/core/components/animations/AnimatedOpacity.tsx diff --git a/frontend/src/pages/app/ui/components/core/animations/types.d.ts b/frontend/src/core/components/animations/types.d.ts similarity index 100% rename from frontend/src/pages/app/ui/components/core/animations/types.d.ts rename to frontend/src/core/components/animations/types.d.ts diff --git a/frontend/src/pages/app/core/config.ts b/frontend/src/core/config.ts similarity index 100% rename from frontend/src/pages/app/core/config.ts rename to frontend/src/core/config.ts diff --git a/frontend/src/pages/app/resources/css/_electron.scss b/frontend/src/core/electron/electron.scss similarity index 95% rename from frontend/src/pages/app/resources/css/_electron.scss rename to frontend/src/core/electron/electron.scss index 69aa4ed..9f45831 100644 --- a/frontend/src/pages/app/resources/css/_electron.scss +++ b/frontend/src/core/electron/electron.scss @@ -1,4 +1,4 @@ -@use "common/material" as *; +@use "../../css/material" as *; #electron-title-bar { display: none; diff --git a/frontend/src/pages/app/electron/electron.ts b/frontend/src/core/electron/electron.ts similarity index 94% rename from frontend/src/pages/app/electron/electron.ts rename to frontend/src/core/electron/electron.ts index 0eb9ab1..c69cce2 100644 --- a/frontend/src/pages/app/electron/electron.ts +++ b/frontend/src/core/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/ui/hooks/useCombinedRefs.ts b/frontend/src/core/hooks/useCombinedRefs.ts similarity index 100% rename from frontend/src/pages/app/ui/hooks/useCombinedRefs.ts rename to frontend/src/core/hooks/useCombinedRefs.ts diff --git a/frontend/src/pages/app/ui/hooks/useWindowSize.ts b/frontend/src/core/hooks/useWindowSize.ts similarity index 100% rename from frontend/src/pages/app/ui/hooks/useWindowSize.ts rename to frontend/src/core/hooks/useWindowSize.ts diff --git a/frontend/src/pages/app/core/init.ts b/frontend/src/core/init.ts similarity index 100% rename from frontend/src/pages/app/core/init.ts rename to frontend/src/core/init.ts diff --git a/frontend/src/pages/app/utils/push-notifications.ts b/frontend/src/core/push-notifications/push-notifications.ts similarity index 97% rename from frontend/src/pages/app/utils/push-notifications.ts rename to frontend/src/core/push-notifications/push-notifications.ts index 2696d30..6c265f8 100644 --- a/frontend/src/pages/app/utils/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/service-worker.ts?worker&url"; +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 { endpoint: string; diff --git a/frontend/src/pages/app/service-worker/service-worker.ts b/frontend/src/core/push-notifications/service-worker.ts similarity index 100% rename from frontend/src/pages/app/service-worker/service-worker.ts rename to frontend/src/core/push-notifications/service-worker.ts diff --git a/frontend/src/pages/app/core/types.d.ts b/frontend/src/core/types.d.ts similarity index 100% rename from frontend/src/pages/app/core/types.d.ts rename to frontend/src/core/types.d.ts diff --git a/frontend/src/pages/app/core/websocket.ts b/frontend/src/core/websocket.ts similarity index 98% rename from frontend/src/pages/app/core/websocket.ts rename to frontend/src/core/websocket.ts index c881cf3..4ec865b 100644 --- a/frontend/src/pages/app/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/app/resources/css/common/_animations.scss b/frontend/src/css/_animations.scss similarity index 100% rename from frontend/src/pages/app/resources/css/common/_animations.scss rename to frontend/src/css/_animations.scss diff --git a/frontend/src/pages/app/resources/css/common/_colors.scss b/frontend/src/css/_colors.scss similarity index 100% rename from frontend/src/pages/app/resources/css/common/_colors.scss rename to frontend/src/css/_colors.scss diff --git a/frontend/src/pages/app/resources/css/common/_components.scss b/frontend/src/css/_components.scss similarity index 50% rename from frontend/src/pages/app/resources/css/common/_components.scss rename to frontend/src/css/_components.scss index 4567775..03bb64c 100644 --- a/frontend/src/pages/app/resources/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/pages/app/resources/css/common/_material.scss b/frontend/src/css/_material.scss similarity index 100% rename from frontend/src/pages/app/resources/css/common/_material.scss rename to frontend/src/css/_material.scss diff --git a/frontend/src/pages/app/resources/css/lib/fonts/material-symbols.scss b/frontend/src/css/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/fonts/material-symbols.scss diff --git a/frontend/src/pages/app/resources/css/lib/fonts/material-symbols.woff2 b/frontend/src/css/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/fonts/material-symbols.woff2 diff --git a/frontend/src/pages/app/resources/css/lib/fonts/montserrat.scss b/frontend/src/css/fonts/montserrat.scss similarity index 100% rename from frontend/src/pages/app/resources/css/lib/fonts/montserrat.scss rename to frontend/src/css/fonts/montserrat.scss diff --git a/frontend/src/pages/app/resources/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459W1hyyTh89ZNpQ.woff2 b/frontend/src/css/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/fonts/montserrat/JTUSjIg1_i6t8kCHKm459W1hyyTh89ZNpQ.woff2 diff --git a/frontend/src/pages/app/resources/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WRhyyTh89ZNpQ.woff2 b/frontend/src/css/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/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WRhyyTh89ZNpQ.woff2 diff --git a/frontend/src/pages/app/resources/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WZhyyTh89ZNpQ.woff2 b/frontend/src/css/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/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WZhyyTh89ZNpQ.woff2 diff --git a/frontend/src/pages/app/resources/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WdhyyTh89ZNpQ.woff2 b/frontend/src/css/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/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WdhyyTh89ZNpQ.woff2 diff --git a/frontend/src/pages/app/resources/css/lib/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2 b/frontend/src/css/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/fonts/montserrat/JTUSjIg1_i6t8kCHKm459WlhyyTh89Y.woff2 diff --git a/frontend/src/pages/app/resources/css/style.scss b/frontend/src/css/style.scss similarity index 59% rename from frontend/src/pages/app/resources/css/style.scss rename to frontend/src/css/style.scss index e129c88..9ab39fe 100644 --- a/frontend/src/pages/app/resources/css/style.scss +++ b/frontend/src/css/style.scss @@ -1,21 +1,10 @@ -@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 "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/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..83eec38 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -5,17 +5,15 @@ * @version 1.0.0 */ -import './pages/app/resources/css/style.scss'; -import "mdui/mdui.css"; +import './css/style.scss'; -import "./pages/app/utils/material"; -import "./pages/app/core/init"; -import "./pages/app/electron/electron"; +import "./utils/material"; +import "./core/init"; +import "./core/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..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 "./app/ui/state"; +import { useAppState } from "./chat/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/_chat.scss b/frontend/src/pages/app/resources/css/_chat.scss deleted file mode 100644 index a333156..0000000 --- a/frontend/src/pages/app/resources/css/_chat.scss +++ /dev/null @@ -1,984 +0,0 @@ -@use "common/colors" as *; -@use "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; - } -} 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/app/ui/components/Alerts.tsx b/frontend/src/pages/app/ui/components/Alerts.tsx deleted file mode 100644 index 3284ac3..0000000 --- a/frontend/src/pages/app/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 diff --git a/frontend/src/pages/app/ui/components/chat/MessageReactions.tsx b/frontend/src/pages/app/ui/components/chat/MessageReactions.tsx deleted file mode 100644 index 808a82e..0000000 --- a/frontend/src/pages/app/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/app/ui/components/chat/ReplyMessageDialog.tsx b/frontend/src/pages/app/ui/components/chat/ReplyMessageDialog.tsx deleted file mode 100644 index f270d1a..0000000 --- a/frontend/src/pages/app/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/app/ui/screen/ChatScreen.tsx b/frontend/src/pages/app/ui/screen/ChatScreen.tsx deleted file mode 100644 index 04b2707..0000000 --- a/frontend/src/pages/app/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/app/ui/screen/DownloadAppScreen.tsx b/frontend/src/pages/app/ui/screen/DownloadAppScreen.tsx deleted file mode 100644 index 8b4e5e5..0000000 --- a/frontend/src/pages/app/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/app/ui/screen/LoginScreen.tsx b/frontend/src/pages/app/ui/screen/LoginScreen.tsx deleted file mode 100644 index 168b0d8..0000000 --- a/frontend/src/pages/app/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 "../../auth/crypto"; -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 "../../utils/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/app/ui/screen/RegisterScreen.tsx b/frontend/src/pages/app/ui/screen/RegisterScreen.tsx deleted file mode 100644 index ec16814..0000000 --- a/frontend/src/pages/app/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 "../../auth/crypto"; - -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/app/ui/components/Auth.tsx b/frontend/src/pages/auth/Auth.tsx similarity index 71% rename from frontend/src/pages/app/ui/components/Auth.tsx rename to frontend/src/pages/auth/Auth.tsx index 65da3a3..5c6b48a 100644 --- a/frontend/src/pages/app/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/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..4ff1004 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 "./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 { 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 "@/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"; 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 94% rename from frontend/src/pages/RegisterPage.tsx rename to frontend/src/pages/auth/RegisterPage.tsx index 2e437e1..5e5f000 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 "./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 "./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 "@/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"; 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 94% rename from frontend/src/pages/app/resources/css/_auth.scss rename to frontend/src/pages/auth/auth.scss index 59ad690..2427d3c 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/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 new file mode 100644 index 0000000..d837774 --- /dev/null +++ b/frontend/src/pages/chat/css/_animations.scss @@ -0,0 +1,107 @@ +@use "../../../css/colors" as *; +@use "../../../css/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..9ac5779 --- /dev/null +++ b/frontend/src/pages/chat/css/_chat-input.scss @@ -0,0 +1,318 @@ +@use "../../../css/colors" as *; +@use "../../../css/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/app/resources/css/_reactions.scss b/frontend/src/pages/chat/css/_context-menu.scss similarity index 65% rename from frontend/src/pages/app/resources/css/_reactions.scss rename to frontend/src/pages/chat/css/_context-menu.scss index f64402d..1720e46 100644 --- a/frontend/src/pages/app/resources/css/_reactions.scss +++ b/frontend/src/pages/chat/css/_context-menu.scss @@ -1,62 +1,7 @@ -@use "common/material" as *; +@use "../../../css/colors" as *; +@use "../../../css/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; @@ -138,6 +83,68 @@ } } +.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; @@ -192,16 +199,6 @@ } } -@keyframes emojiMenuEnter { - from { - opacity: 0; - } - - to { - opacity: 1; - } -} - .reaction-bar-content { display: flex; align-items: center; @@ -273,100 +270,6 @@ } } -// 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 { diff --git a/frontend/src/pages/chat/css/_layout.scss b/frontend/src/pages/chat/css/_layout.scss new file mode 100644 index 0000000..b64bea8 --- /dev/null +++ b/frontend/src/pages/chat/css/_layout.scss @@ -0,0 +1,49 @@ +@use "../../../css/colors" as *; +@use "../../../css/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/app/resources/css/_panelchat.scss b/frontend/src/pages/chat/css/_left-panel.scss similarity index 56% rename from frontend/src/pages/app/resources/css/_panelchat.scss rename to frontend/src/pages/chat/css/_left-panel.scss index 9c17533..1c47a74 100644 --- a/frontend/src/pages/app/resources/css/_panelchat.scss +++ b/frontend/src/pages/chat/css/_left-panel.scss @@ -1,13 +1,56 @@ -@use "common/colors" as *; -@use "common/material" as *; +@use "../../../css/colors" as *; +@use "../../../css/material" as *; +@use "sass:color"; - -// контейнер чата и панели с чатами -.all-container { +.header { display: flex; - flex-direction: row; - width: 100%; - height: 100%; + 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 { @@ -147,4 +190,45 @@ padding-right: 16px; margin-top: auto; } -} \ No newline at end of file +} + +// 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..7225cc2 --- /dev/null +++ b/frontend/src/pages/chat/css/_message-reactions.scss @@ -0,0 +1,59 @@ +@use "../../../css/colors" as *; +@use "../../../css/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..79556fa --- /dev/null +++ b/frontend/src/pages/chat/css/_message.scss @@ -0,0 +1,352 @@ +@use "../../../css/colors" as *; +@use "../../../css/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/app/resources/css/_profile.scss b/frontend/src/pages/chat/css/_profile-dialog.scss similarity index 98% rename from frontend/src/pages/app/resources/css/_profile.scss rename to frontend/src/pages/chat/css/_profile-dialog.scss index f3f60a4..b5315c9 100644 --- a/frontend/src/pages/app/resources/css/_profile.scss +++ b/frontend/src/pages/chat/css/_profile-dialog.scss @@ -1,6 +1,8 @@ -@use "common/colors" as *; -@use "common/material" as *; +@use "../../../css/colors" as *; +@use "../../../css/material" as *; +@use "sass:color"; +// Profile styles #profile-dialog .content { display: flex; flex-direction: column; @@ -246,4 +248,4 @@ padding-top: 16px; border-top: 1px solid $color-dark-outline; } -} \ No newline at end of file +} 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..f6ff746 --- /dev/null +++ b/frontend/src/pages/chat/css/_right-panel.scss @@ -0,0 +1,131 @@ +@use "../../../css/colors" as *; +@use "../../../css/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/app/resources/css/_settings.scss b/frontend/src/pages/chat/css/_settings-dialog.scss similarity index 96% rename from frontend/src/pages/app/resources/css/_settings.scss rename to frontend/src/pages/chat/css/_settings-dialog.scss index 4607769..fca04b6 100644 --- a/frontend/src/pages/app/resources/css/_settings.scss +++ b/frontend/src/pages/chat/css/_settings-dialog.scss @@ -1,6 +1,8 @@ -@use "common/colors" as *; -@use "common/material" as *; +@use "../../../css/colors" as *; +@use "../../../css/material" as *; +@use "sass:color"; +// Settings styles #settings-dialog { .fullscreen-wrapper { display: flex; @@ -96,4 +98,4 @@ } } } -} \ No newline at end of file +} 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/app/ui/hooks/useDM.ts b/frontend/src/pages/chat/hooks/useDM.ts similarity index 98% rename from frontend/src/pages/app/ui/hooks/useDM.ts rename to frontend/src/pages/chat/hooks/useDM.ts index 9e55858..189ef64 100644 --- a/frontend/src/pages/app/ui/hooks/useDM.ts +++ b/frontend/src/pages/chat/hooks/useDM.ts @@ -1,14 +1,14 @@ import { useState, useEffect, useCallback, useRef } from "react"; -import { useAppState } from "../state"; +import { useAppState } from "@/pages/chat/state"; import { fetchUsers, fetchUserPublicKey, 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/app/ui/hooks/useProfile.ts b/frontend/src/pages/chat/hooks/useProfile.ts similarity index 95% rename from frontend/src/pages/app/ui/hooks/useProfile.ts rename to frontend/src/pages/chat/hooks/useProfile.ts index e256f78..3e0eb65 100644 --- a/frontend/src/pages/app/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 "../../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/app/ui/state.ts b/frontend/src/pages/chat/state.ts similarity index 95% rename from frontend/src/pages/app/ui/state.ts rename to frontend/src/pages/chat/state.ts index 14899e0..67bf059 100644 --- a/frontend/src/pages/app/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 "../auth/api"; -import { restoreKeys } from "../auth/crypto"; -import { API_BASE_URL } from "../core/config"; -import { initialize, subscribe, startElectronReceiver, isSupported } from "../utils/push-notifications"; -import { isElectron } from "../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/ChatPage.tsx b/frontend/src/pages/chat/ui/ChatPage.tsx similarity index 63% rename from frontend/src/pages/ChatPage.tsx rename to frontend/src/pages/chat/ui/ChatPage.tsx index 0989a42..9bc44fe 100644 --- a/frontend/src/pages/ChatPage.tsx +++ b/frontend/src/pages/chat/ui/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 "./left/LeftPanel"; +import { RightPanel } from "./right/RightPanel"; +import "@/pages/chat/css/chat.scss"; export default function ChatPage() { return ( diff --git a/frontend/src/pages/app/ui/components/chat/BottomAppBar.tsx b/frontend/src/pages/chat/ui/left/BottomAppBar.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/chat/BottomAppBar.tsx rename to frontend/src/pages/chat/ui/left/BottomAppBar.tsx diff --git a/frontend/src/pages/app/ui/components/chat/ChatHeader.tsx b/frontend/src/pages/chat/ui/left/ChatHeader.tsx similarity index 83% rename from frontend/src/pages/app/ui/components/chat/ChatHeader.tsx rename to frontend/src/pages/chat/ui/left/ChatHeader.tsx index 2c47b88..a76bc15 100644 --- a/frontend/src/pages/app/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 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"; +import { ProfileDialog } from "./profile/ProfileDialog"; export function ChatHeader() { const { profileData } = useProfile(); diff --git a/frontend/src/pages/app/ui/components/chat/ChatTabs.tsx b/frontend/src/pages/chat/ui/left/ChatTabs.tsx similarity index 97% rename from frontend/src/pages/app/ui/components/chat/ChatTabs.tsx rename to frontend/src/pages/chat/ui/left/ChatTabs.tsx index 9625567..bf5bb8f 100644 --- a/frontend/src/pages/app/ui/components/chat/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/app/ui/components/chat/DMUsersList.tsx b/frontend/src/pages/chat/ui/left/DMUsersList.tsx similarity index 93% rename from frontend/src/pages/app/ui/components/chat/DMUsersList.tsx rename to frontend/src/pages/chat/ui/left/DMUsersList.tsx index c09bc25..6c25f09 100644 --- a/frontend/src/pages/app/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 "../../../api/dmApi"; -import defaultAvatar from "../../../resources/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/app/ui/components/chat/LeftPanel.tsx b/frontend/src/pages/chat/ui/left/LeftPanel.tsx similarity index 92% rename from frontend/src/pages/app/ui/components/chat/LeftPanel.tsx rename to frontend/src/pages/chat/ui/left/LeftPanel.tsx index 1377632..a6bf4d8 100644 --- a/frontend/src/pages/app/ui/components/chat/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 "../../../resources/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 { 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/app/ui/components/profile/CropperDialog.tsx b/frontend/src/pages/chat/ui/left/profile/CropperDialog.tsx similarity index 100% rename from frontend/src/pages/app/ui/components/profile/CropperDialog.tsx rename to frontend/src/pages/chat/ui/left/profile/CropperDialog.tsx diff --git a/frontend/src/pages/app/ui/components/profile/ImageCropper.tsx b/frontend/src/pages/chat/ui/left/profile/ImageCropper.tsx similarity index 99% rename from frontend/src/pages/app/ui/components/profile/ImageCropper.tsx rename to frontend/src/pages/chat/ui/left/profile/ImageCropper.tsx index 58baedc..36ceb00 100644 --- a/frontend/src/pages/app/ui/components/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/app/ui/components/profile/ProfileDialog.tsx b/frontend/src/pages/chat/ui/left/profile/ProfileDialog.tsx similarity index 96% rename from frontend/src/pages/app/ui/components/profile/ProfileDialog.tsx rename to frontend/src/pages/chat/ui/left/profile/ProfileDialog.tsx index 1bb65ce..5d5414c 100644 --- a/frontend/src/pages/app/ui/components/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 "../../../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"; -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/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/app/ui/components/settings/SettingsDialog.tsx b/frontend/src/pages/chat/ui/left/settings/SettingsDialog.tsx similarity index 96% rename from frontend/src/pages/app/ui/components/settings/SettingsDialog.tsx rename to frontend/src/pages/chat/ui/left/settings/SettingsDialog.tsx index 14b1ad5..1ea0bb6 100644 --- a/frontend/src/pages/app/ui/components/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/Dialog"; -import { initialize, isSupported, startElectronReceiver, stopElectronReceiver, subscribe, unsubscribe } from "../../../utils/push-notifications"; -import { isElectron } from "../../../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 "../../../auth/api"; +import { getAuthHeaders } from "@/core/api/authApi"; export function SettingsDialog({ isOpen, onOpenChange }: DialogProps) { const [activePanel, setActivePanel] = useState("notifications-settings"); diff --git a/frontend/src/pages/app/ui/components/chat/ChatInputWrapper.tsx b/frontend/src/pages/chat/ui/right/ChatInputWrapper.tsx similarity index 97% rename from frontend/src/pages/app/ui/components/chat/ChatInputWrapper.tsx rename to frontend/src/pages/chat/ui/right/ChatInputWrapper.tsx index 89df0f9..59ed54c 100644 --- a/frontend/src/pages/app/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/app/ui/components/chat/ChatMainHeader.tsx b/frontend/src/pages/chat/ui/right/ChatMainHeader.tsx similarity index 83% rename from frontend/src/pages/app/ui/components/chat/ChatMainHeader.tsx rename to frontend/src/pages/chat/ui/right/ChatMainHeader.tsx index 19720bb..09b5635 100644 --- a/frontend/src/pages/app/ui/components/chat/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/app/ui/components/chat/ChatMessages.tsx b/frontend/src/pages/chat/ui/right/ChatMessages.tsx similarity index 93% rename from frontend/src/pages/app/ui/components/chat/ChatMessages.tsx rename to frontend/src/pages/chat/ui/right/ChatMessages.tsx index efdf97c..25b5cc3 100644 --- a/frontend/src/pages/app/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 { 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 "../../../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/app/ui/components/chat/EmojiMenu.tsx b/frontend/src/pages/chat/ui/right/EmojiMenu.tsx similarity index 99% rename from frontend/src/pages/app/ui/components/chat/EmojiMenu.tsx rename to frontend/src/pages/chat/ui/right/EmojiMenu.tsx index c701c4c..9b1c4f5 100644 --- a/frontend/src/pages/app/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/app/ui/components/chat/Message.tsx b/frontend/src/pages/chat/ui/right/Message.tsx similarity index 79% rename from frontend/src/pages/app/ui/components/chat/Message.tsx rename to frontend/src/pages/chat/ui/right/Message.tsx index 5e88677..025528f 100644 --- a/frontend/src/pages/app/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 "../../../resources/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 "../../../auth/crypto"; -import { ecdhSharedSecret, deriveWrappingKey } from "../../../utils/crypto/asymmetric"; -import { importAesGcmKey, aesGcmDecrypt } from "../../../utils/crypto/symmetric"; -import { getAuthHeaders } from "../../../auth/api"; -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"; -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/app/ui/components/chat/MessageContextMenu.tsx b/frontend/src/pages/chat/ui/right/MessageContextMenu.tsx similarity index 99% rename from frontend/src/pages/app/ui/components/chat/MessageContextMenu.tsx rename to frontend/src/pages/chat/ui/right/MessageContextMenu.tsx index 81fe39a..8405aa5 100644 --- a/frontend/src/pages/app/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 { @@ -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 diff --git a/frontend/src/pages/app/ui/components/chat/MessagePanelRenderer.tsx b/frontend/src/pages/chat/ui/right/MessagePanelRenderer.tsx similarity index 96% rename from frontend/src/pages/app/ui/components/chat/MessagePanelRenderer.tsx rename to frontend/src/pages/chat/ui/right/MessagePanelRenderer.tsx index da64a2f..c9bb259 100644 --- a/frontend/src/pages/app/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 { 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 "../../../resources/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/app/ui/components/chat/RightPanel.tsx b/frontend/src/pages/chat/ui/right/RightPanel.tsx similarity index 79% rename from frontend/src/pages/app/ui/components/chat/RightPanel.tsx rename to frontend/src/pages/chat/ui/right/RightPanel.tsx index 05c0d46..278b091 100644 --- a/frontend/src/pages/app/ui/components/chat/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/app/ui/components/chat/UserProfileDialog.tsx b/frontend/src/pages/chat/ui/right/UserProfileDialog.tsx similarity index 90% rename from frontend/src/pages/app/ui/components/chat/UserProfileDialog.tsx rename to frontend/src/pages/chat/ui/right/UserProfileDialog.tsx index 1f3b8eb..2e890f9 100644 --- a/frontend/src/pages/app/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 "../../../resources/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/app/ui/components/chat/emojiData.ts b/frontend/src/pages/chat/ui/right/emojiData.ts similarity index 100% rename from frontend/src/pages/app/ui/components/chat/emojiData.ts rename to frontend/src/pages/chat/ui/right/emojiData.ts diff --git a/frontend/src/pages/app/ui/panels/DMPanel.ts b/frontend/src/pages/chat/ui/right/panels/DMPanel.ts similarity index 98% rename from frontend/src/pages/app/ui/panels/DMPanel.ts rename to frontend/src/pages/chat/ui/right/panels/DMPanel.ts index b528138..bfc7b47 100644 --- a/frontend/src/pages/app/ui/panels/DMPanel.ts +++ b/frontend/src/pages/chat/ui/right/panels/DMPanel.ts @@ -6,9 +6,9 @@ import { sendDmWithFiles, editDmEnvelope, deleteDmEnvelope -} from "../../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 "@/pages/chat/state"; export interface DMPanelData { userId: number; diff --git a/frontend/src/pages/app/ui/panels/MessagePanel.ts b/frontend/src/pages/chat/ui/right/panels/MessagePanel.ts similarity index 99% rename from frontend/src/pages/app/ui/panels/MessagePanel.ts rename to frontend/src/pages/chat/ui/right/panels/MessagePanel.ts index 53d4872..5357e16 100644 --- a/frontend/src/pages/app/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 "@/pages/chat/state"; export interface MessagePanelState { id: string; diff --git a/frontend/src/pages/app/ui/panels/PublicChatPanel.ts b/frontend/src/pages/chat/ui/right/panels/PublicChatPanel.ts similarity index 96% rename from frontend/src/pages/app/ui/panels/PublicChatPanel.ts rename to frontend/src/pages/chat/ui/right/panels/PublicChatPanel.ts index 9d3d29a..86da1d2 100644 --- a/frontend/src/pages/app/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 "../../auth/api"; -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/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..9ed2c3c 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 "@/pages/chat/state"; +import { isElectron } from "@/core/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..9cd545a 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/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 diff --git a/frontend/src/pages/app/utils/crypto/asymmetric.ts b/frontend/src/utils/crypto/asymmetric.ts similarity index 100% rename from frontend/src/pages/app/utils/crypto/asymmetric.ts rename to frontend/src/utils/crypto/asymmetric.ts diff --git a/frontend/src/pages/app/utils/crypto/backup.ts b/frontend/src/utils/crypto/backup.ts similarity index 100% rename from frontend/src/pages/app/utils/crypto/backup.ts rename to frontend/src/utils/crypto/backup.ts diff --git a/frontend/src/pages/app/utils/crypto/kdf.ts b/frontend/src/utils/crypto/kdf.ts similarity index 100% rename from frontend/src/pages/app/utils/crypto/kdf.ts rename to frontend/src/utils/crypto/kdf.ts diff --git a/frontend/src/pages/app/utils/crypto/symmetric.ts b/frontend/src/utils/crypto/symmetric.ts similarity index 100% rename from frontend/src/pages/app/utils/crypto/symmetric.ts rename to frontend/src/utils/crypto/symmetric.ts diff --git a/frontend/src/pages/app/utils/material.ts b/frontend/src/utils/material.ts similarity index 97% rename from frontend/src/pages/app/utils/material.ts rename to frontend/src/utils/material.ts index a5cdb5e..7ab2607 100644 --- a/frontend/src/pages/app/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/src/pages/app/utils/notification.ts b/frontend/src/utils/notification.ts similarity index 100% rename from frontend/src/pages/app/utils/notification.ts rename to frontend/src/utils/notification.ts diff --git a/frontend/src/pages/app/utils/utils.ts b/frontend/src/utils/utils.ts similarity index 100% rename from frontend/src/pages/app/utils/utils.ts rename to frontend/src/utils/utils.ts 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..6548add 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -3,6 +3,10 @@ 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"; +import { visualizer } from 'rollup-plugin-visualizer'; + +const outDir = process.env.VITE_ELECTRON ? "build/electron/dist" : "build/normal/dist"; const plugins: PluginOption[] = [ react(), @@ -17,6 +21,11 @@ const plugins: PluginOption[] = [ minifyCSS: true, minifyJS: true } + }), + visualizer({ + filename: `${outDir}/stats.html`, + open: true, + gzipSize: true }) ] @@ -46,6 +55,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, @@ -79,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",