Change the structure

This commit is contained in:
2025-10-08 18:15:23 +03:00
Unverified
parent 14a35bc18d
commit 737974dfa8
97 changed files with 1019 additions and 1030 deletions
+1 -1
View File
@@ -573,4 +573,4 @@ package-lock.json
backend/alembic/** backend/alembic/**
!backend/alembic/env.py !backend/alembic/env.py
!backend/alembic/script.py.mako !backend/alembic/script.py.mako
!frontend/src/pages/app/resources/css/lib !frontend/src/css/lib
+12 -12
View File
@@ -1,17 +1,17 @@
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom"; import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { ElectronTitleBar } from "./pages/app/ui/components/Electron"; import { ElectronTitleBar } from "./pages/chat/ui/components/Electron";
import { useAppState } from "./pages/app/ui/state"; import { useAppState } from "./pages/chat/ui/state";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import HomePage from "./pages/HomePage"; import HomePage from "./pages/home/HomePage";
import LoginPage from "./pages/LoginPage"; import LoginPage from "./pages/auth/LoginPage";
import RegisterPage from "./pages/RegisterPage"; import RegisterPage from "./pages/auth/RegisterPage";
import ChatPage from "./pages/ChatPage"; import ChatPage from "./pages/chat/ChatPage";
import DownloadAppPage from "./pages/DownloadAppPage"; import DownloadAppPage from "./pages/download-app/DownloadAppPage";
import NotFoundPage from "./pages/NotFoundPage"; import NotFoundPage from "./pages/not-found/NotFoundPage";
import ProtectedRoute from "./pages/ProtectedRoute"; import ProtectedRoute from "./pages/ProtectedRoute";
import { isElectron } from "./pages/app/electron/electron"; import { isElectron } from "./pages/chat/electron/electron";
import { MINIMUM_WIDTH } from "./pages/app/core/config"; import { MINIMUM_WIDTH } from "./pages/chat/core/config";
import useWindowSize from "./pages/app/ui/hooks/useWindowSize"; import useWindowSize from "./pages/chat/ui/hooks/useWindowSize";
export default function App() { export default function App() {
const { restoreUserFromStorage } = useAppState(); const { restoreUserFromStorage } = useAppState();
@@ -31,7 +31,7 @@ export default function App() {
<ElectronTitleBar /> <ElectronTitleBar />
<div id="main-wrapper"> <div id="main-wrapper">
<Routes> <Routes>
z<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} /> <Route path="/register" element={<RegisterPage />} />
<Route path="/download-app" element={<DownloadAppPage />} /> <Route path="/download-app" element={<DownloadAppPage />} />
@@ -1,9 +1,26 @@
import { API_BASE_URL } from "../core/config"; import type { Headers, UploadPublicKeyRequest, BackupBlob } from "../pages/chat/core/types";
import { getAuthHeaders } from "./api"; import { generateX25519KeyPair } from "../pages/chat/utils/crypto/asymmetric";
import { generateX25519KeyPair } from "../utils/crypto/asymmetric"; import { encodeBlob, encryptBackupWithPassword, decryptBackupWithPassword, decodeBlob } from "../pages/chat/utils/crypto/backup";
import { encryptBackupWithPassword, decryptBackupWithPassword, encodeBlob, decodeBlob } from "../utils/crypto/backup"; import { b64, ub64 } from "../pages/chat/utils/utils";
import { b64, ub64 } from "../utils/utils"; import { API_BASE_URL } from "../pages/chat/core/config";
import type { BackupBlob, UploadPublicKeyRequest } 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;
}
let currentPublicKey: Uint8Array | null = null; let currentPublicKey: Uint8Array | null = null;
let currentPrivateKey: Uint8Array | null = null; let currentPrivateKey: Uint8Array | null = null;
@@ -1,12 +1,12 @@
import { API_BASE_URL } from "../core/config"; import { API_BASE_URL } from "../pages/chat/core/config";
import { getAuthHeaders } from "../auth/api"; import { getAuthHeaders } from "./authApi";
import { ecdhSharedSecret, deriveWrappingKey } from "../utils/crypto/asymmetric"; import { ecdhSharedSecret, deriveWrappingKey } from "../pages/chat/utils/crypto/asymmetric";
import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../utils/crypto/symmetric"; import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../pages/chat/utils/crypto/symmetric";
import { randomBytes } from "../utils/crypto/kdf"; import { randomBytes } from "../pages/chat/utils/crypto/kdf";
import { getCurrentKeys } from "../auth/crypto"; import { getCurrentKeys } from "./authApi";
import { request } from "../core/websocket"; import { request } from "../pages/chat/core/websocket";
import type { SendDMRequest, DmEnvelope, User, DMEditRequest, DmEncryptedJSON, BaseDmEnvelope } from "../core/types"; import type { SendDMRequest, DmEnvelope, User, DMEditRequest, DmEncryptedJSON, BaseDmEnvelope } from "../pages/chat/core/types";
import { b64, ub64 } from "../utils/utils"; import { b64, ub64 } from "../pages/chat/utils/utils";
export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string): Promise<string> { export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string): Promise<string> {
const keys = getCurrentKeys(); const keys = getCurrentKeys();
@@ -1,6 +1,6 @@
import { getAuthHeaders } from "../auth/api"; import { getAuthHeaders } from "./authApi";
import { API_BASE_URL } from "../core/config"; import { API_BASE_URL } from "../pages/chat/core/config";
import type { UserProfile } from "../core/types"; import type { UserProfile } from "../pages/chat/core/types";
export interface ProfileData { export interface ProfileData {
profile_picture?: string; profile_picture?: string;
@@ -1,18 +1,7 @@
@use "auth";
@use "chat";
@use "profile";
@use "settings";
@use "panelchat";
@use "common/animations"; @use "common/animations";
@use "common/components"; @use "common/components";
@use "common/colors" as *; @use "common/colors" as *;
@use "common/material" as *; @use "common/material" as *;
@use "electron";
@use "dialogs/reply";
@use "download-app";
@use "404" as not-found;
@use "homepage";
@use "reactions";
@use "lib/fonts/montserrat"; @use "lib/fonts/montserrat";
@use "lib/fonts/material-symbols"; @use "lib/fonts/material-symbols";

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

+4 -5
View File
@@ -5,17 +5,16 @@
* @version 1.0.0 * @version 1.0.0
*/ */
import './pages/app/resources/css/style.scss'; import './css/style.scss';
import "mdui/mdui.css"; import "mdui/mdui.css";
import "./pages/app/utils/material"; import "./pages/chat/utils/material";
import "./pages/app/core/init"; import "./pages/chat/core/init";
import "./pages/app/electron/electron"; import "./pages/chat/electron/electron";
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import App from './App'; import App from './App';
import { StrictMode } from 'react'; import { StrictMode } from 'react';
// Initialize React app
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<App /> <App />
+1 -1
View File
@@ -1,5 +1,5 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useAppState } from "./app/ui/state"; import { useAppState } from "./chat/ui/state";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
interface ProtectedRouteProps { interface ProtectedRouteProps {
-19
View File
@@ -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;
}
@@ -1,150 +0,0 @@
@use "common/colors" as *;
@use "common/material" as *;
// контейнер чата и панели с чатами
.all-container {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
}
#profile {
display: none;
flex-direction: column;
z-index: 2000;
top: 0;
left: 0;
position: fixed;
height: 100vh;
width: 27%;
background-color: $color-dark-surface-container;
position: relative;
.profileheader {
display: flex;
gap: 200px;
p {
color: white;
}
a {
text-decoration: none;
color: white;
border: solid 2px $color-dark-on-surface-variant;
padding: 5px;
border-radius: 10px;
&:hover {
background-color: rgba(255, 255, 255, 0.241);
}
}
}
}
#chat-list {
display: flex;
flex-direction: column;
flex-grow: 0;
width: 40%;
background-color: $color-dark-surface-container;
height: 100%;
z-index: 1000;
min-height: 0; // allow children to manage their own scrolling
.chat-header-left {
display: flex;
color: white;
font-size: 25px;
background-color: $color-dark-surface-container;
width: 100%;
justify-content: center;
align-items: center;
padding: 16px;
overflow: hidden;
.product-name {
flex-grow: 1;
}
.profile {
font-size: 24px;
display: flex;
justify-content: start;
flex-shrink: 0;
flex-grow: 0;
#closeprofile a {
text-decoration: none;
color: white;
border: solid 2px $color-dark-on-surface-variant;
padding: 5px;
border-radius: 10px;
&:hover {
background-color: rgba(255, 255, 255, 0.241);
}
}
img {
$size: 45px;
display: flex;
border-radius: 50%;
width: $size;
height: $size;
}
}
}
.chat-tabs {
margin-top: 5px;
width: 100%;
height: calc(100% - 80px);
display: flex;
flex-direction: column;
min-height: 0; // prevent flex collapse when inner overflows
--mdui-color-surface: $color-dark-surface-container;
--mdui-color-surface-variant: transparent;
img {
width: 45px;
height: 45px;
border-radius: 20%;
object-fit: cover;
margin-right: 1rem;
}
mdui-tabs {
height: 100%;
display: flex;
flex-direction: column;
min-height: 0; // enable inner panel to scroll
}
mdui-tab-panel[active] {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0; // critical to avoid collapsing
overflow: hidden;
}
mdui-list {
flex: 1;
min-height: 0; // allow scroll area to size correctly
overflow-y: auto;
padding: 0;
margin: 0;
}
}
mdui-bottom-app-bar {
position: relative;
width: 100%;
padding-left: 16px;
padding-right: 16px;
margin-top: auto;
}
}
@@ -1,249 +0,0 @@
@use "common/colors" as *;
@use "common/material" as *;
#profile-dialog .content {
display: flex;
flex-direction: column;
gap: 24px;
min-width: 400px;
.header-top {
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
position: relative;
padding-bottom: 16px;
border-bottom: 1px solid $color-dark-outline;
.profile-picture-container {
position: relative;
$size: 70px;
width: $size;
height: $size;
flex-shrink: 0;
#profile-picture {
width: $size;
height: $size;
border-radius: 50%;
object-fit: cover;
}
.upload-overlay {
position: absolute;
bottom: 0;
right: 0;
width: 28px;
height: 28px;
cursor: pointer;
}
}
mdui-text-field {
flex: 1;
}
}
#profile-form {
display: flex;
flex-direction: column;
gap: 16px;
mdui-text-field {
width: 100%;
}
.dialog-actions {
display: flex;
gap: 12px;
padding-top: 16px;
border-top: 1px solid $color-dark-outline;
> * {
flex: 1;
}
}
}
}
// User profile dialog content styles
#user-profile-dialog .content {
display: flex;
gap: 1.5rem;
.profile-picture-section {
flex-shrink: 0;
.profile-picture {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
border: 2px solid $color-dark-outline;
}
}
.profile-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
.username-section {
display: flex;
align-items: center;
gap: 0.75rem;
.username {
margin: 0;
color: $color-dark-on-surface;
font-size: 1.1rem;
font-weight: 600;
}
.online-status {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-weight: 500;
&.online {
color: $success;
background-color: rgba(76, 175, 80, 0.1);
.online-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: $success;
}
}
&.offline {
color: $color-dark-on-surface-variant;
background-color: rgba(255, 255, 255, 0.05);
.offline-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: $color-dark-on-surface-variant;
}
}
}
}
.bio-section {
label {
display: block;
color: $color-dark-on-surface-variant;
font-size: 0.85rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
.bio-display {
color: $color-dark-on-surface;
font-size: 0.9rem;
line-height: 1.4;
padding: 0.75rem;
background-color: $color-dark-surface;
border-radius: 8px;
border: 1px solid $color-dark-outline;
min-height: 60px;
}
.bio-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
}
}
.profile-stats {
display: flex;
flex-direction: column;
gap: 0.5rem;
.stat {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
.stat-label {
color: $color-dark-on-surface-variant;
font-size: 0.85rem;
}
.stat-value {
color: $color-dark-on-surface;
font-size: 0.85rem;
font-weight: 500;
}
}
}
.profile-actions {
display: flex;
gap: 0.75rem;
margin-top: 0.5rem;
mdui-button {
flex: 1;
}
}
}
}
// Cropper Dialog Styles
#cropper-dialog {
.cropper-dialog-content {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 500px;
max-width: 600px;
}
.cropper-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 16px;
border-bottom: 1px solid $color-dark-outline;
h3 {
margin: 0;
color: $color-dark-on-surface;
}
}
.cropper-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
background: $color-dark-surface-container;
border-radius: 8px;
overflow: hidden;
#cropper-area {
width: 100%;
height: 100%;
min-height: 400px;
}
}
.cropper-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
padding-top: 16px;
border-top: 1px solid $color-dark-outline;
}
}
@@ -1,389 +0,0 @@
@use "common/material" as *;
@use "sass:color";
// Reaction styles
.message-reactions {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 8px;
margin-left: 10px;
margin-right: 10px;
animation: messageReactionsFadeIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.reaction-button {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: none;
border-radius: 16px;
background-color: $color-dark-surface-container;
cursor: pointer;
transition: transform 0.2s ease, background-color 0.2s ease;
font-size: 1px;
min-height: 28px;
animation: reactionFadeIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
&.removing {
animation: reactionFadeOut 0.2s ease forwards;
}
&:hover {
background-color: $color-dark-surface-container-high;
transform: scale(1.05);
}
&.reacted {
background-color: $color-dark-primary-container;
border-color: $color-dark-primary;
color: $color-dark-on-primary-container;
&:hover {
background-color: color.adjust($color-dark-primary-container, $lightness: 20%);
}
}
}
.reaction-emoji {
font-size: 17px;
line-height: 1;
}
.reaction-count {
font-size: 12px;
font-weight: 500;
line-height: 1;
}
// Reaction bar styles (standalone)
.reaction-bar {
background: $color-dark-surface-container;
border: 1px solid $color-dark-outline;
border-radius: 24px;
padding: 8px;
opacity: 1;
transition: all 0.15s ease;
backdrop-filter: blur(8px);
transform: translateY(0);
&.closing {
opacity: 0;
transform: scale(0.8);
}
}
// Emoji menu wrapper inside reaction bar
.emoji-menu-wrapper {
width: 320px;
height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
// Context menu wrapper with animations
.context-menu-wrapper {
position: relative;
display: block;
// Animation states
&.entering {
opacity: 0;
transform: scale(0.8);
animation: contextMenuEnter 0.2s ease forwards;
}
&.entering-left {
opacity: 0;
transform: translateX(-20px) scale(0.8);
animation: contextMenuEnterLeft 0.2s ease forwards;
}
&.entering-up {
opacity: 0;
transform: translateY(20px) scale(0.8);
animation: contextMenuEnterUp 0.2s ease forwards;
}
&.entering-up-left {
opacity: 0;
transform: translateX(-20px) translateY(20px) scale(0.8);
animation: contextMenuEnterUpLeft 0.2s ease forwards;
}
&.closing {
opacity: 1;
transform: scale(1);
animation: contextMenuClose 0.2s ease forwards;
}
&.closing-left {
opacity: 1;
transform: translateX(0) scale(1);
animation: contextMenuCloseLeft 0.2s ease forwards;
}
&.closing-up {
opacity: 1;
transform: translateY(0) scale(1);
animation: contextMenuCloseUp 0.2s ease forwards;
}
&.closing-up-left {
opacity: 1;
transform: translateX(0) translateY(0) scale(1);
animation: contextMenuCloseUpLeft 0.2s ease forwards;
}
}
// Reaction bar inside context menu wrapper
.context-menu-reaction-bar {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 12px;
background: $color-dark-surface-container;
border: 1px solid $color-dark-outline;
border-radius: 16px;
position: absolute;
bottom: 100%;
justify-content: center;
margin-bottom: 10px;
transition: width 0.3s ease-out, height 0.3s ease-out;
&.left {
left: 0;
transform: translateX(0);
}
&.right {
right: 0;
transform: translateX(0);
}
&.expanded {
padding: 0;
overflow: hidden;
width: 320px;
height: 400px;
border-radius: 16px;
// Default: expand downward from the reaction bar's bottom edge
position: absolute;
bottom: auto;
top: 0;
left: 0;
transform: translateY(0);
&.expand-upward {
// Expand upward from the reaction bar's top edge
bottom: 100%;
top: auto;
margin-bottom: 10px;
margin-top: 0;
transform: translateY(0);
}
.emoji-menu-wrapper {
animation: emojiMenuEnter 0.5s ease;
}
}
}
@keyframes emojiMenuEnter {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.reaction-bar-content {
display: flex;
align-items: center;
gap: 4px;
transition: opacity 0.3s ease-out;
&.faded {
opacity: 0;
}
}
.reaction-emoji-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 16px;
background: transparent;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
font-size: 18px;
&:hover {
background: var(--mdui-color-surface-container-high);
transform: scale(1.3);
box-shadow: var(--mdui-elevation-1);
}
&:active {
transform: scale(0.95);
transition: transform 0.1s ease;
}
}
.reaction-expand-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: 1px solid var(--mdui-color-outline);
border-radius: 16px;
background: var(--mdui-color-surface);
cursor: pointer;
transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
&:hover {
background: var(--mdui-color-surface-container-high);
border-color: var(--mdui-color-primary);
transform: scale(1.1);
box-shadow: var(--mdui-elevation-1);
}
&:active {
transform: scale(0.95);
transition: transform 0.1s ease;
}
.material-symbols {
font-size: 18px;
color: var(--mdui-color-on-surface);
transition: transform 0.2s ease;
}
&:hover .material-symbols {
transform: rotate(90deg);
}
}
// Animation for reactions appearing/disappearing
@keyframes messageReactionsFadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes reactionFadeIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes reactionFadeOut {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.8);
}
}
// Context menu wrapper animations
@keyframes contextMenuEnter {
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes contextMenuEnterLeft {
to {
opacity: 1;
transform: translateX(0) scale(1);
}
}
@keyframes contextMenuEnterUp {
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes contextMenuEnterUpLeft {
to {
opacity: 1;
transform: translateX(0) translateY(0) scale(1);
}
}
@keyframes contextMenuClose {
to {
opacity: 0;
transform: scale(0.8);
}
}
@keyframes contextMenuCloseLeft {
to {
opacity: 0;
transform: translateX(-20px) scale(0.8);
}
}
@keyframes contextMenuCloseUp {
to {
opacity: 0;
transform: translateY(20px) scale(0.8);
}
}
@keyframes contextMenuCloseUpLeft {
to {
opacity: 0;
transform: translateX(-20px) translateY(20px) scale(0.8);
}
}
// Mobile responsive
@media (max-width: 768px) {
.reaction-bar {
padding: 6px;
}
.reaction-emoji-button,
.reaction-expand-button {
width: 28px;
height: 28px;
}
.reaction-emoji-button {
font-size: 16px;
}
.reaction-expand-button .material-symbols {
font-size: 16px;
}
}
@@ -1,99 +0,0 @@
@use "common/colors" as *;
@use "common/material" as *;
#settings-dialog {
.fullscreen-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: calc(100vh - (1.5rem * 2));
width: 100%;
position: relative;
#settings-dialog-inner {
max-width: 1200px;
max-height: 1000px;
width: 100%;
height: 100%;
.header {
display: flex;
flex-direction: row;
gap: 10px;
margin-bottom: 16px;
}
#settings-menu {
display: flex;
flex-direction: row;
gap: 16px;
mdui-list {
max-width: 280px;
padding-right: 16px;
overflow-y: auto;
}
.screen {
flex: 1;
overflow-y: auto;
position: relative;
.settings-panel {
display: flex;
flex-direction: column;
gap: 16px;
opacity: 0;
visibility: hidden;
transform: translateY(20px);
transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s ease;
position: absolute;
top: 0;
left: 0;
width: 100%;
&.active {
opacity: 1;
visibility: visible;
transform: translateY(0);
position: relative;
}
h3 {
margin: 0 0 16px 0;
color: $color-dark-on-surface;
}
mdui-text-field,
mdui-select,
mdui-switch,
mdui-button {
margin-bottom: 8px;
}
mdui-switch {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid $color-dark-outline;
&:last-child {
border-bottom: none;
}
}
p {
margin: 8px 0;
color: $color-dark-on-surface-variant;
}
mdui-linear-progress {
margin: 16px 0;
}
}
}
}
}
}
}
@@ -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;
}
}
}
}
@@ -1,16 +1,17 @@
import { useImmer } from "use-immer"; import { useImmer } from "use-immer";
import { AlertsContainer, type Alert, type AlertType } from "./app/ui/components/Alerts"; import { AlertsContainer, type Alert, type AlertType } from "../chat/ui/components/Alerts";
import { AuthContainer, AuthHeader } from "./app/ui/components/Auth"; import { AuthContainer, AuthHeader } from "../chat/ui/components/Auth";
import type { ErrorResponse, LoginRequest, LoginResponse } from "./app/core/types"; import type { ErrorResponse, LoginRequest, LoginResponse } from "../chat/core/types";
import { ensureKeysOnLogin } from "./app/auth/crypto"; import { ensureKeysOnLogin } from "../../api/authApi";
import { API_BASE_URL } from "./app/core/config"; import { API_BASE_URL } from "../chat/core/config";
import { useRef } from "react"; import { useRef } from "react";
import type { TextField } from "mdui/components/text-field"; import type { TextField } from "mdui/components/text-field";
import { useAppState } from "./app/ui/state"; import { useAppState } from "../chat/ui/state";
import { MaterialTextField } from "./app/ui/components/core/TextField"; import { MaterialTextField } from "../chat/ui/components/core/TextField";
import { initialize, isSupported, startElectronReceiver, subscribe } from "./app/utils/push-notifications"; import { initialize, isSupported, startElectronReceiver, subscribe } from "../chat/utils/push-notifications";
import { isElectron } from "./app/electron/electron"; import { isElectron } from "../chat/electron/electron";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import "./auth.scss";
export default function LoginPage() { export default function LoginPage() {
const [alerts, updateAlerts] = useImmer<Alert[]>([]); const [alerts, updateAlerts] = useImmer<Alert[]>([]);
@@ -1,14 +1,15 @@
import { useImmer } from "use-immer"; import { useImmer } from "use-immer";
import { AuthContainer, AuthHeader } from "./app/ui/components/Auth"; import { AuthContainer, AuthHeader } from "../chat/ui/components/Auth";
import { AlertsContainer, type Alert, type AlertType } from "./app/ui/components/Alerts"; import { AlertsContainer, type Alert, type AlertType } from "../chat/ui/components/Alerts";
import { useRef } from "react"; import { useRef } from "react";
import { TextField } from "mdui/components/text-field"; import { TextField } from "mdui/components/text-field";
import type { ErrorResponse, RegisterRequest, LoginResponse } from "./app/core/types"; import type { ErrorResponse, RegisterRequest, LoginResponse } from "../chat/core/types";
import { API_BASE_URL } from "./app/core/config"; import { API_BASE_URL } from "../chat/core/config";
import { useAppState } from "./app/ui/state"; import { useAppState } from "../chat/ui/state";
import { MaterialTextField } from "./app/ui/components/core/TextField"; import { MaterialTextField } from "../chat/ui/components/core/TextField";
import { ensureKeysOnLogin } from "./app/auth/crypto"; import { ensureKeysOnLogin } from "../../api/authApi";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import "./auth.scss";
export default function RegisterPage() { export default function RegisterPage() {
const [alerts, updateAlerts] = useImmer<Alert[]>([]); const [alerts, updateAlerts] = useImmer<Alert[]>([]);
@@ -1,5 +1,5 @@
@use "common/colors" as *; @use "../../css/common/colors" as *;
@use "common/material" as *; @use "../../css/common/material" as *;
.auth-container { .auth-container {
display: flex; display: flex;
@@ -1,5 +1,6 @@
import { LeftPanel } from "./app/ui/components/chat/LeftPanel"; import { LeftPanel } from "./ui/components/chat/LeftPanel";
import { RightPanel } from "./app/ui/components/chat/RightPanel"; import { RightPanel } from "./ui/components/chat/RightPanel";
import "./chat.scss";
export default function ChatPage() { export default function ChatPage() {
return ( return (
@@ -1,5 +1,5 @@
@use "common/colors" as *; @use "../../css/common/colors" as *;
@use "common/material" as *; @use "../../css/common/material" as *;
@use "sass:color"; @use "sass:color";
#chat-interface { #chat-interface {
@@ -982,3 +982,915 @@
overflow: visible; overflow: visible;
} }
} }
// Panel chat styles
// контейнер чата и панели с чатами
.all-container {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
}
#profile {
display: none;
flex-direction: column;
z-index: 2000;
top: 0;
left: 0;
position: fixed;
height: 100vh;
width: 27%;
background-color: $color-dark-surface-container;
position: relative;
.profileheader {
display: flex;
gap: 200px;
p {
color: white;
}
a {
text-decoration: none;
color: white;
border: solid 2px $color-dark-on-surface-variant;
padding: 5px;
border-radius: 10px;
&:hover {
background-color: rgba(255, 255, 255, 0.241);
}
}
}
}
#chat-list {
display: flex;
flex-direction: column;
flex-grow: 0;
width: 40%;
background-color: $color-dark-surface-container;
height: 100%;
z-index: 1000;
min-height: 0; // allow children to manage their own scrolling
.chat-header-left {
display: flex;
color: white;
font-size: 25px;
background-color: $color-dark-surface-container;
width: 100%;
justify-content: center;
align-items: center;
padding: 16px;
overflow: hidden;
.product-name {
flex-grow: 1;
}
.profile {
font-size: 24px;
display: flex;
justify-content: start;
flex-shrink: 0;
flex-grow: 0;
#closeprofile a {
text-decoration: none;
color: white;
border: solid 2px $color-dark-on-surface-variant;
padding: 5px;
border-radius: 10px;
&:hover {
background-color: rgba(255, 255, 255, 0.241);
}
}
img {
$size: 45px;
display: flex;
border-radius: 50%;
width: $size;
height: $size;
}
}
}
.chat-tabs {
margin-top: 5px;
width: 100%;
height: calc(100% - 80px);
display: flex;
flex-direction: column;
min-height: 0; // prevent flex collapse when inner overflows
--mdui-color-surface: $color-dark-surface-container;
--mdui-color-surface-variant: transparent;
img {
width: 45px;
height: 45px;
border-radius: 20%;
object-fit: cover;
margin-right: 1rem;
}
mdui-tabs {
height: 100%;
display: flex;
flex-direction: column;
min-height: 0; // enable inner panel to scroll
}
mdui-tab-panel[active] {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0; // critical to avoid collapsing
overflow: hidden;
}
mdui-list {
flex: 1;
min-height: 0; // allow scroll area to size correctly
overflow-y: auto;
padding: 0;
margin: 0;
}
}
mdui-bottom-app-bar {
position: relative;
width: 100%;
padding-left: 16px;
padding-right: 16px;
margin-top: auto;
}
}
// Profile styles
#profile-dialog .content {
display: flex;
flex-direction: column;
gap: 24px;
min-width: 400px;
.header-top {
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
position: relative;
padding-bottom: 16px;
border-bottom: 1px solid $color-dark-outline;
.profile-picture-container {
position: relative;
$size: 70px;
width: $size;
height: $size;
flex-shrink: 0;
#profile-picture {
width: $size;
height: $size;
border-radius: 50%;
object-fit: cover;
}
.upload-overlay {
position: absolute;
bottom: 0;
right: 0;
width: 28px;
height: 28px;
cursor: pointer;
}
}
mdui-text-field {
flex: 1;
}
}
#profile-form {
display: flex;
flex-direction: column;
gap: 16px;
mdui-text-field {
width: 100%;
}
.dialog-actions {
display: flex;
gap: 12px;
padding-top: 16px;
border-top: 1px solid $color-dark-outline;
> * {
flex: 1;
}
}
}
}
// User profile dialog content styles
#user-profile-dialog .content {
display: flex;
gap: 1.5rem;
.profile-picture-section {
flex-shrink: 0;
.profile-picture {
width: 80px;
height: 80px;
border-radius: 50%;
object-fit: cover;
border: 2px solid $color-dark-outline;
}
}
.profile-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
.username-section {
display: flex;
align-items: center;
gap: 0.75rem;
.username {
margin: 0;
color: $color-dark-on-surface;
font-size: 1.1rem;
font-weight: 600;
}
.online-status {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.85rem;
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-weight: 500;
&.online {
color: $success;
background-color: rgba(76, 175, 80, 0.1);
.online-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: $success;
}
}
&.offline {
color: $color-dark-on-surface-variant;
background-color: rgba(255, 255, 255, 0.05);
.offline-indicator {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: $color-dark-on-surface-variant;
}
}
}
}
.bio-section {
label {
display: block;
color: $color-dark-on-surface-variant;
font-size: 0.85rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
.bio-display {
color: $color-dark-on-surface;
font-size: 0.9rem;
line-height: 1.4;
padding: 0.75rem;
background-color: $color-dark-surface;
border-radius: 8px;
border: 1px solid $color-dark-outline;
min-height: 60px;
}
.bio-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
}
}
.profile-stats {
display: flex;
flex-direction: column;
gap: 0.5rem;
.stat {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
.stat-label {
color: $color-dark-on-surface-variant;
font-size: 0.85rem;
}
.stat-value {
color: $color-dark-on-surface;
font-size: 0.85rem;
font-weight: 500;
}
}
}
.profile-actions {
display: flex;
gap: 0.75rem;
margin-top: 0.5rem;
mdui-button {
flex: 1;
}
}
}
}
// Cropper Dialog Styles
#cropper-dialog {
.cropper-dialog-content {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 500px;
max-width: 600px;
}
.cropper-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 16px;
border-bottom: 1px solid $color-dark-outline;
h3 {
margin: 0;
color: $color-dark-on-surface;
}
}
.cropper-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
background: $color-dark-surface-container;
border-radius: 8px;
overflow: hidden;
#cropper-area {
width: 100%;
height: 100%;
min-height: 400px;
}
}
.cropper-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
padding-top: 16px;
border-top: 1px solid $color-dark-outline;
}
}
// Reaction styles
.message-reactions {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 8px;
margin-left: 10px;
margin-right: 10px;
animation: messageReactionsFadeIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.reaction-button {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border: none;
border-radius: 16px;
background-color: $color-dark-surface-container;
cursor: pointer;
transition: transform 0.2s ease, background-color 0.2s ease;
font-size: 1px;
min-height: 28px;
animation: reactionFadeIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
&.removing {
animation: reactionFadeOut 0.2s ease forwards;
}
&:hover {
background-color: $color-dark-surface-container-high;
transform: scale(1.05);
}
&.reacted {
background-color: $color-dark-primary-container;
border-color: $color-dark-primary;
color: $color-dark-on-primary-container;
&:hover {
background-color: color.adjust($color-dark-primary-container, $lightness: 20%);
}
}
}
.reaction-emoji {
font-size: 17px;
line-height: 1;
}
.reaction-count {
font-size: 12px;
font-weight: 500;
line-height: 1;
}
// Reaction bar styles (standalone)
.reaction-bar {
background: $color-dark-surface-container;
border: 1px solid $color-dark-outline;
border-radius: 24px;
padding: 8px;
opacity: 1;
transition: all 0.15s ease;
backdrop-filter: blur(8px);
transform: translateY(0);
&.closing {
opacity: 0;
transform: scale(0.8);
}
}
// Emoji menu wrapper inside reaction bar
.emoji-menu-wrapper {
width: 320px;
height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
// Context menu wrapper with animations
.context-menu-wrapper {
position: relative;
display: block;
// Animation states
&.entering {
opacity: 0;
transform: scale(0.8);
animation: contextMenuEnter 0.2s ease forwards;
}
&.entering-left {
opacity: 0;
transform: translateX(-20px) scale(0.8);
animation: contextMenuEnterLeft 0.2s ease forwards;
}
&.entering-up {
opacity: 0;
transform: translateY(20px) scale(0.8);
animation: contextMenuEnterUp 0.2s ease forwards;
}
&.entering-up-left {
opacity: 0;
transform: translateX(-20px) translateY(20px) scale(0.8);
animation: contextMenuEnterUpLeft 0.2s ease forwards;
}
&.closing {
opacity: 1;
transform: scale(1);
animation: contextMenuClose 0.2s ease forwards;
}
&.closing-left {
opacity: 1;
transform: translateX(0) scale(1);
animation: contextMenuCloseLeft 0.2s ease forwards;
}
&.closing-up {
opacity: 1;
transform: translateY(0) scale(1);
animation: contextMenuCloseUp 0.2s ease forwards;
}
&.closing-up-left {
opacity: 1;
transform: translateX(0) translateY(0) scale(1);
animation: contextMenuCloseUpLeft 0.2s ease forwards;
}
}
// Reaction bar inside context menu wrapper
.context-menu-reaction-bar {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 12px;
background: $color-dark-surface-container;
border: 1px solid $color-dark-outline;
border-radius: 16px;
position: absolute;
bottom: 100%;
justify-content: center;
margin-bottom: 10px;
transition: width 0.3s ease-out, height 0.3s ease-out;
&.left {
left: 0;
transform: translateX(0);
}
&.right {
right: 0;
transform: translateX(0);
}
&.expanded {
padding: 0;
overflow: hidden;
width: 320px;
height: 400px;
border-radius: 16px;
// Default: expand downward from the reaction bar's bottom edge
position: absolute;
bottom: auto;
top: 0;
left: 0;
transform: translateY(0);
&.expand-upward {
// Expand upward from the reaction bar's top edge
bottom: 100%;
top: auto;
margin-bottom: 10px;
margin-top: 0;
transform: translateY(0);
}
.emoji-menu-wrapper {
animation: emojiMenuEnter 0.5s ease;
}
}
}
@keyframes emojiMenuEnter {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.reaction-bar-content {
display: flex;
align-items: center;
gap: 4px;
transition: opacity 0.3s ease-out;
&.faded {
opacity: 0;
}
}
.reaction-emoji-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 16px;
background: transparent;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
font-size: 18px;
&:hover {
background: var(--mdui-color-surface-container-high);
transform: scale(1.3);
box-shadow: var(--mdui-elevation-1);
}
&:active {
transform: scale(0.95);
transition: transform 0.1s ease;
}
}
.reaction-expand-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: 1px solid var(--mdui-color-outline);
border-radius: 16px;
background: var(--mdui-color-surface);
cursor: pointer;
transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1);
&:hover {
background: var(--mdui-color-surface-container-high);
border-color: var(--mdui-color-primary);
transform: scale(1.1);
box-shadow: var(--mdui-elevation-1);
}
&:active {
transform: scale(0.95);
transition: transform 0.1s ease;
}
.material-symbols {
font-size: 18px;
color: var(--mdui-color-on-surface);
transition: transform 0.2s ease;
}
&:hover .material-symbols {
transform: rotate(90deg);
}
}
// Animation for reactions appearing/disappearing
@keyframes messageReactionsFadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes reactionFadeIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes reactionFadeOut {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.8);
}
}
// Context menu wrapper animations
@keyframes contextMenuEnter {
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes contextMenuEnterLeft {
to {
opacity: 1;
transform: translateX(0) scale(1);
}
}
@keyframes contextMenuEnterUp {
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes contextMenuEnterUpLeft {
to {
opacity: 1;
transform: translateX(0) translateY(0) scale(1);
}
}
@keyframes contextMenuClose {
to {
opacity: 0;
transform: scale(0.8);
}
}
@keyframes contextMenuCloseLeft {
to {
opacity: 0;
transform: translateX(-20px) scale(0.8);
}
}
@keyframes contextMenuCloseUp {
to {
opacity: 0;
transform: translateY(20px) scale(0.8);
}
}
@keyframes contextMenuCloseUpLeft {
to {
opacity: 0;
transform: translateX(-20px) translateY(20px) scale(0.8);
}
}
// Mobile responsive
@media (max-width: 768px) {
.reaction-bar {
padding: 6px;
}
.reaction-emoji-button,
.reaction-expand-button {
width: 28px;
height: 28px;
}
.reaction-emoji-button {
font-size: 16px;
}
.reaction-expand-button .material-symbols {
font-size: 16px;
}
}
// Settings styles
#settings-dialog {
.fullscreen-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: calc(100vh - (1.5rem * 2));
width: 100%;
position: relative;
#settings-dialog-inner {
max-width: 1200px;
max-height: 1000px;
width: 100%;
height: 100%;
.header {
display: flex;
flex-direction: row;
gap: 10px;
margin-bottom: 16px;
}
#settings-menu {
display: flex;
flex-direction: row;
gap: 16px;
mdui-list {
max-width: 280px;
padding-right: 16px;
overflow-y: auto;
}
.screen {
flex: 1;
overflow-y: auto;
position: relative;
.settings-panel {
display: flex;
flex-direction: column;
gap: 16px;
opacity: 0;
visibility: hidden;
transform: translateY(20px);
transition: opacity 0.3s ease, transform 0.3s ease, visibility 0.3s ease;
position: absolute;
top: 0;
left: 0;
width: 100%;
&.active {
opacity: 1;
visibility: visible;
transform: translateY(0);
position: relative;
}
h3 {
margin: 0 0 16px 0;
color: $color-dark-on-surface;
}
mdui-text-field,
mdui-select,
mdui-switch,
mdui-button {
margin-bottom: 8px;
}
mdui-switch {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid $color-dark-outline;
&:last-child {
border-bottom: none;
}
}
p {
margin: 8px 0;
color: $color-dark-on-surface-variant;
}
mdui-linear-progress {
margin: 16px 0;
}
}
}
}
}
}
}
// Reply dialog styles
.reply-dialog .dialog-content {
width: 300px;
overflow-x:hidden;
.reply-preview-dialog {
margin-bottom: 1rem;
padding: 16px;
background-color: $color-dark-surface-container;
border-radius: 16px;
.reply-content {
display: flex;
flex-direction: column;
gap: 0.25rem;
.reply-username {
font-weight: 600;
color: $color-dark-on-surface;
font-size: 0.85rem;
}
.reply-text {
color: $color-dark-on-surface-variant;
font-size: 0.9rem;
line-height: 1.4;
}
}
}
}
@@ -1,4 +1,4 @@
@use "common/material" as *; @use "../../../css/common/material" as *;
#electron-title-bar { #electron-title-bar {
display: none; display: none;
@@ -5,6 +5,8 @@
* @version 1.0.0 * @version 1.0.0
*/ */
import "./electron.scss";
export const isElectron = import.meta.env.VITE_ELECTRON && window.electronInterface != undefined; export const isElectron = import.meta.env.VITE_ELECTRON && window.electronInterface != undefined;
if (isElectron) { if (isElectron) {
@@ -4,7 +4,7 @@ import type { Message as MessageType } from "../../../core/types";
import type { UserProfile } from "../../../core/types"; import type { UserProfile } from "../../../core/types";
import { UserProfileDialog } from "./UserProfileDialog"; import { UserProfileDialog } from "./UserProfileDialog";
import { MessageContextMenu, type ContextMenuState } from "./MessageContextMenu"; import { MessageContextMenu, type ContextMenuState } from "./MessageContextMenu";
import { fetchUserProfile } from "../../../api/profileApi"; import { fetchUserProfile } from "../../../../../api/profileApi";
import { useEffect, useState, type ReactNode } from "react"; import { useEffect, useState, type ReactNode } from "react";
import { delay } from "../../../utils/utils"; import { delay } from "../../../utils/utils";
import { MaterialDialog } from "../core/Dialog"; import { MaterialDialog } from "../core/Dialog";
@@ -1,8 +1,8 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { useDM, type DMUser } from "../../hooks/useDM"; import { useDM, type DMUser } from "../../hooks/useDM";
import { useAppState } from "../../state"; import { useAppState } from "../../state";
import { fetchUserPublicKey } from "../../../api/dmApi"; import { fetchUserPublicKey } from "../../../../../api/dmApi";
import defaultAvatar from "../../../resources/images/default-avatar.png"; import defaultAvatar from "../../../../../images/default-avatar.png";
export function DMUsersList() { export function DMUsersList() {
const { dmUsers, isLoadingUsers, loadUsers } = useDM(); const { dmUsers, isLoadingUsers, loadUsers } = useDM();
@@ -1,6 +1,6 @@
import { PRODUCT_NAME } from "../../../core/config"; import { PRODUCT_NAME } from "../../../core/config";
import { useAppState } from "../../state"; import { useAppState } from "../../state";
import defaultAvatar from "../../../resources/images/default-avatar.png"; import defaultAvatar from "../../../../../images/default-avatar.png";
import { useState, type FormEvent } from "react"; import { useState, type FormEvent } from "react";
import { ProfileDialog } from "../profile/ProfileDialog"; import { ProfileDialog } from "../profile/ProfileDialog";
import { SettingsDialog } from "../settings/SettingsDialog"; import { SettingsDialog } from "../settings/SettingsDialog";
@@ -1,14 +1,14 @@
import { formatTime, id } from "../../../utils/utils"; import { formatTime, id } from "../../../utils/utils";
import type { Attachment, Message as MessageType } from "../../../core/types"; import type { Attachment, Message as MessageType } from "../../../core/types";
import defaultAvatar from "../../../resources/images/default-avatar.png"; import defaultAvatar from "../../../../../images/default-avatar.png";
import Quote from "../core/Quote"; import Quote from "../core/Quote";
import { parse } from "marked"; import { parse } from "marked";
import DOMPurify from "dompurify"; import DOMPurify from "dompurify";
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from "react";
import { getCurrentKeys } from "../../../auth/crypto"; import { getCurrentKeys } from "../../../../../api/authApi";
import { ecdhSharedSecret, deriveWrappingKey } from "../../../utils/crypto/asymmetric"; import { ecdhSharedSecret, deriveWrappingKey } from "../../../utils/crypto/asymmetric";
import { importAesGcmKey, aesGcmDecrypt } from "../../../utils/crypto/symmetric"; import { importAesGcmKey, aesGcmDecrypt } from "../../../utils/crypto/symmetric";
import { getAuthHeaders } from "../../../auth/api"; import { getAuthHeaders } from "../../../../../api/authApi";
import { useAppState } from "../../state"; import { useAppState } from "../../state";
import { ub64 } from "../../../utils/utils"; import { ub64 } from "../../../utils/utils";
import { useImmer } from "use-immer"; import { useImmer } from "use-immer";
@@ -5,7 +5,7 @@ import { ChatMessages } from "./ChatMessages";
import { ChatInputWrapper } from "./ChatInputWrapper"; import { ChatInputWrapper } from "./ChatInputWrapper";
import { setGlobalMessageHandler } from "../../../core/websocket"; import { setGlobalMessageHandler } from "../../../core/websocket";
import type { Message, WebSocketMessage } from "../../../core/types"; import type { Message, WebSocketMessage } from "../../../core/types";
import defaultAvatar from "../../../resources/images/default-avatar.png"; import defaultAvatar from "../../../../../images/default-avatar.png";
import AnimatedOpacity from "../core/animations/AnimatedOpacity"; import AnimatedOpacity from "../core/animations/AnimatedOpacity";
import type { DMPanel } from "../../panels/DMPanel"; import type { DMPanel } from "../../panels/DMPanel";
@@ -2,7 +2,7 @@ import type { DialogProps } from "../../../core/types";
import type { UserProfile } from "../../../core/types"; import type { UserProfile } from "../../../core/types";
import { MaterialDialog } from "../core/Dialog"; import { MaterialDialog } from "../core/Dialog";
import { formatTime } from "../../../utils/utils"; import { formatTime } from "../../../utils/utils";
import defaultAvatar from "../../../resources/images/default-avatar.png"; import defaultAvatar from "../../../../../images/default-avatar.png";
interface UserProfileDialogProps extends DialogProps { interface UserProfileDialogProps extends DialogProps {
userProfile: UserProfile | null; userProfile: UserProfile | null;
@@ -1,5 +1,5 @@
import { useState, useEffect, useRef, type FormEvent } from "react"; 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 { TextField } from "mdui/components/text-field";
import type { DialogProps } from "../../../core/types"; import type { DialogProps } from "../../../core/types";
import { MaterialDialog } from "../core/Dialog"; import { MaterialDialog } from "../core/Dialog";
@@ -6,7 +6,7 @@ import { initialize, isSupported, startElectronReceiver, stopElectronReceiver, s
import { isElectron } from "../../../electron/electron"; import { isElectron } from "../../../electron/electron";
import { useAppState } from "../../state"; import { useAppState } from "../../state";
import type { Switch } from "mdui/components/switch"; import type { Switch } from "mdui/components/switch";
import { getAuthHeaders } from "../../../auth/api"; import { getAuthHeaders } from "../../../../../api/authApi";
export function SettingsDialog({ isOpen, onOpenChange }: DialogProps) { export function SettingsDialog({ isOpen, onOpenChange }: DialogProps) {
const [activePanel, setActivePanel] = useState("notifications-settings"); const [activePanel, setActivePanel] = useState("notifications-settings");
@@ -6,7 +6,7 @@ import {
fetchDMHistory, fetchDMHistory,
decryptDm, decryptDm,
sendDMViaWebSocket sendDMViaWebSocket
} from "../../api/dmApi"; } from "../../../../api/dmApi";
import type { User, Message, DmEncryptedJSON } from "../../core/types"; import type { User, Message, DmEncryptedJSON } from "../../core/types";
import { websocket } from "../../core/websocket"; import { websocket } from "../../core/websocket";
@@ -1,6 +1,6 @@
import { useState, useCallback, useEffect } from "react"; import { useState, useCallback, useEffect } from "react";
import { useAppState } from "../state"; import { useAppState } from "../state";
import { loadProfile, updateProfile, uploadProfilePicture, type ProfileData } from "../../api/profileApi"; import { loadProfile, updateProfile, uploadProfilePicture, type ProfileData } from "../../../../api/profileApi";
import { showSuccess, showError } from "../../utils/notification"; import { showSuccess, showError } from "../../utils/notification";
export default function useProfile() { export default function useProfile() {
@@ -6,7 +6,7 @@ import {
sendDmWithFiles, sendDmWithFiles,
editDmEnvelope, editDmEnvelope,
deleteDmEnvelope deleteDmEnvelope
} from "../../api/dmApi"; } from "../../../../api/dmApi";
import type { DmEncryptedJSON, DmEnvelope, DMWebSocketMessage, EncryptedMessageJson, Message } from "../../core/types"; import type { DmEncryptedJSON, DmEnvelope, DMWebSocketMessage, EncryptedMessageJson, Message } from "../../core/types";
import type { UserState } from "../state"; import type { UserState } from "../state";
@@ -1,6 +1,6 @@
import { MessagePanel } from "./MessagePanel"; import { MessagePanel } from "./MessagePanel";
import { API_BASE_URL } from "../../core/config"; import { API_BASE_URL } from "../../core/config";
import { getAuthHeaders } from "../../auth/api"; import { getAuthHeaders } from "../../../../api/authApi";
import { request } from "../../core/websocket"; import { request } from "../../core/websocket";
import type { ChatWebSocketMessage, Message, SendMessageRequest, ReactionUpdateWebSocketMessage } from "../../core/types"; import type { ChatWebSocketMessage, Message, SendMessageRequest, ReactionUpdateWebSocketMessage } from "../../core/types";
import type { UserState } from "../state"; import type { UserState } from "../state";
@@ -2,7 +2,7 @@ import { useImmer } from "use-immer";
import { AlertsContainer, type Alert, type AlertType } from "../components/Alerts"; import { AlertsContainer, type Alert, type AlertType } from "../components/Alerts";
import { AuthContainer, AuthHeader } from "../components/Auth"; import { AuthContainer, AuthHeader } from "../components/Auth";
import type { ErrorResponse, LoginRequest, LoginResponse } from "../../core/types"; import type { ErrorResponse, LoginRequest, LoginResponse } from "../../core/types";
import { ensureKeysOnLogin } from "../../auth/crypto"; import { ensureKeysOnLogin } from "../../../../api/authApi";
import { API_BASE_URL } from "../../core/config"; import { API_BASE_URL } from "../../core/config";
import { useRef } from "react"; import { useRef } from "react";
import type { TextField } from "mdui/components/text-field"; import type { TextField } from "mdui/components/text-field";
@@ -9,7 +9,7 @@ import { API_BASE_URL } from "../../core/config";
import { useAppState } from "../state"; import { useAppState } from "../state";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { MaterialTextField } from "../components/core/TextField"; import { MaterialTextField } from "../components/core/TextField";
import { ensureKeysOnLogin } from "../../auth/crypto"; import { ensureKeysOnLogin } from "../../../../api/authApi";
export default function RegisterScreen() { export default function RegisterScreen() {
const [alerts, updateAlerts] = useImmer<Alert[]>([]); const [alerts, updateAlerts] = useImmer<Alert[]>([]);
@@ -4,8 +4,8 @@ import { request } from "../core/websocket";
import { MessagePanel } from "./panels/MessagePanel"; import { MessagePanel } from "./panels/MessagePanel";
import { PublicChatPanel } from "./panels/PublicChatPanel"; import { PublicChatPanel } from "./panels/PublicChatPanel";
import { DMPanel, type DMPanelData } from "./panels/DMPanel"; import { DMPanel, type DMPanelData } from "./panels/DMPanel";
import { getAuthHeaders } from "../auth/api"; import { getAuthHeaders } from "../../../api/authApi";
import { restoreKeys } from "../auth/crypto"; import { restoreKeys } from "../../../api/authApi";
import { API_BASE_URL } from "../core/config"; import { API_BASE_URL } from "../core/config";
import { initialize, subscribe, startElectronReceiver, isSupported } from "../utils/push-notifications"; import { initialize, subscribe, startElectronReceiver, isSupported } from "../utils/push-notifications";
import { isElectron } from "../electron/electron"; import { isElectron } from "../electron/electron";
@@ -1,3 +1,5 @@
import "./download-app.scss";
export default function DownloadAppPage() { export default function DownloadAppPage() {
return ( return (
<div className="download-app-screen"> <div className="download-app-screen">
@@ -1,6 +1,7 @@
import { Navigate, useNavigate } from "react-router-dom"; import { Navigate, useNavigate } from "react-router-dom";
import { useAppState } from "./app/ui/state"; import { useAppState } from "../chat/ui/state";
import { isElectron } from "./app/electron/electron"; import { isElectron } from "../chat/electron/electron";
import "./home.scss";
function GitHubLink({ children }: { children: React.ReactNode }) { function GitHubLink({ children }: { children: React.ReactNode }) {
return ( return (
@@ -1,4 +1,4 @@
@use "common/material" as *; @use "../../css/common/material" as *;
.homepage { .homepage {
min-height: 100vh; min-height: 100vh;
@@ -1,4 +1,5 @@
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import "./not-found.scss";
export default function NotFoundPage() { export default function NotFoundPage() {
const navigate = useNavigate(); const navigate = useNavigate();