From 27666594ff3f5c0f16ea029b72123e976f894fc2 Mon Sep 17 00:00:00 2001 From: denis0001-dev Date: Sat, 6 Sep 2025 12:33:57 +0300 Subject: [PATCH] Clean up the code --- frontend/src/__chat/chat.ts | 216 ---------- frontend/src/__chat/contextMenu.ts | 321 -------------- frontend/src/__chat/dm.ts | 393 ------------------ frontend/src/__chat/panel.ts | 175 -------- frontend/src/__chat/profileDialog.ts | 253 ----------- frontend/src/__navigation.__ts | 42 -- frontend/src/__userPanel/profile/api.ts | 140 ------- frontend/src/__userPanel/profile/editor.ts | 122 ------ .../src/__userPanel/profile/imageCropper.ts | 219 ---------- frontend/src/__userPanel/profile/profile.ts | 41 -- frontend/src/__userPanel/profile/types.ts | 28 -- frontend/src/__userPanel/profile/upload.ts | 150 ------- frontend/src/__userPanel/settings.ts | 95 ----- frontend/src/__userPanel/userpanel.ts | 107 ----- frontend/src/api/dmApi.ts | 8 +- frontend/src/{ui => }/api/profileApi.ts | 6 +- frontend/src/auth/api.ts | 31 +- frontend/src/auth/auth.ts | 17 - frontend/src/auth/crypto.ts | 8 +- frontend/src/{ => core}/websocket.ts | 6 +- frontend/src/main.tsx | 11 +- .../src/ui/components/chat/ChatMessages.tsx | 4 +- frontend/src/ui/components/chat/DMPanel.tsx | 2 +- .../src/ui/components/chat/DMUsersList.tsx | 2 +- .../components/chat/MessagePanelRenderer.tsx | 2 +- frontend/src/ui/hooks/useChat.ts | 2 +- frontend/src/{ => ui}/hooks/useDM.ts | 8 +- frontend/src/ui/hooks/useProfile.ts | 2 +- frontend/src/ui/panels/PublicChatPanel.ts | 2 +- frontend/src/ui/state.ts | 2 +- frontend/src/{ => utils}/crypto/asymmetric.ts | 0 frontend/src/{ => utils}/crypto/backup.ts | 0 frontend/src/{ => utils}/crypto/kdf.ts | 0 frontend/src/{ => utils}/crypto/symmetric.ts | 0 frontend/src/utils/material.ts | 3 - 35 files changed, 28 insertions(+), 2390 deletions(-) delete mode 100644 frontend/src/__chat/chat.ts delete mode 100644 frontend/src/__chat/contextMenu.ts delete mode 100644 frontend/src/__chat/dm.ts delete mode 100644 frontend/src/__chat/panel.ts delete mode 100644 frontend/src/__chat/profileDialog.ts delete mode 100644 frontend/src/__navigation.__ts delete mode 100644 frontend/src/__userPanel/profile/api.ts delete mode 100644 frontend/src/__userPanel/profile/editor.ts delete mode 100644 frontend/src/__userPanel/profile/imageCropper.ts delete mode 100644 frontend/src/__userPanel/profile/profile.ts delete mode 100644 frontend/src/__userPanel/profile/types.ts delete mode 100644 frontend/src/__userPanel/profile/upload.ts delete mode 100644 frontend/src/__userPanel/settings.ts delete mode 100644 frontend/src/__userPanel/userpanel.ts rename frontend/src/{ui => }/api/profileApi.ts (95%) delete mode 100644 frontend/src/auth/auth.ts rename frontend/src/{ => core}/websocket.ts (95%) rename frontend/src/{ => ui}/hooks/useDM.ts (98%) rename frontend/src/{ => utils}/crypto/asymmetric.ts (100%) rename frontend/src/{ => utils}/crypto/backup.ts (100%) rename frontend/src/{ => utils}/crypto/kdf.ts (100%) rename frontend/src/{ => utils}/crypto/symmetric.ts (100%) diff --git a/frontend/src/__chat/chat.ts b/frontend/src/__chat/chat.ts deleted file mode 100644 index 37c4951..0000000 --- a/frontend/src/__chat/chat.ts +++ /dev/null @@ -1,216 +0,0 @@ -/** - * @fileoverview Chat functionality and message management - * @description Handles message display, loading, sending, and real-time updates - * @author Cursor - * @version 1.0.0 - */ - -import { API_BASE_URL } from "../core/config"; -import { request } from "../websocket"; -import type { Message, Messages, WebSocketMessage } from "../core/types"; -import { formatTime } from "../utils/utils"; -import { show as showContextMenu } from "./contextMenu"; -import { show as showUserProfileDialog } from "./profileDialog"; -import defaultAvatar from "../resources/images/default-avatar.png"; -import { authToken, currentUser, getAuthHeaders } from "../auth/api"; -import { ChatPanelController, PublicChatPanel } from "./panel"; - -/** - * Adds a new message to the chat interface - * @param {Message} message - Message object to display - * @param {boolean} isAuthor - Whether the current user is the message author - */ -export function addMessage(message: Message, isAuthor: boolean): void { - const messagesContainer = document.querySelector('.chat-messages') as HTMLElement; - const messageDiv = document.createElement('div'); - messageDiv.classList.add("message"); - if (isAuthor) { - messageDiv.classList.add("sent"); - } else { - messageDiv.classList.add("received"); - } - messageDiv.dataset.id = `${message.id}`; - - const messageInner = document.createElement('div'); - messageInner.classList.add('message-inner'); - - // Add profile picture for received messages - if (!isAuthor) { - const profilePicDiv = document.createElement('div'); - profilePicDiv.classList.add('message-profile-pic'); - - const profileImg = document.createElement('img'); - profileImg.src = message.profile_picture || defaultAvatar; - profileImg.alt = message.username; - - let errorLock = false; - - profileImg.addEventListener("error", () => { - if (!errorLock) { - profileImg.src = defaultAvatar; - errorLock = true; - } - }); - - // Add click handler to profile picture - profileImg.style.cursor = 'pointer'; - profileImg.addEventListener('click', () => { - showUserProfileDialog(message.username); - }); - - profilePicDiv.appendChild(profileImg); - messageDiv.appendChild(profilePicDiv); - } - - if (!isAuthor) { - const usernameDiv = document.createElement('div'); - usernameDiv.classList.add('message-username'); - usernameDiv.textContent = message.username; - - // Add click handler to username - usernameDiv.style.cursor = 'pointer'; - usernameDiv.addEventListener('click', () => { - showUserProfileDialog(message.username); - }); - - messageInner.appendChild(usernameDiv); - } - - // Add reply preview if this is a reply - if (message.reply_to) { - const replyDiv = document.createElement('div'); - replyDiv.classList.add('message-reply'); - replyDiv.innerHTML = ` -
- ${message.reply_to.username} - ${message.reply_to.content} -
- `; - messageInner.appendChild(replyDiv); - } - - const contentDiv = document.createElement('div'); - contentDiv.classList.add('message-content'); - contentDiv.textContent = message.content; - messageInner.appendChild(contentDiv); - - const timeDiv = document.createElement('div'); - timeDiv.classList.add('message-time'); - - let timeText = formatTime(message.timestamp); - if (message.is_edited) { - timeText += ' (edited)'; - } - timeDiv.textContent = timeText; - - if (isAuthor && message.is_read) { - const checkIcon = document.createElement('span'); - checkIcon.classList.add("material-symbols", "outlined"); - timeDiv.appendChild(checkIcon); - } - - messageInner.appendChild(timeDiv); - messageDiv.appendChild(messageInner); - messagesContainer.appendChild(messageDiv); - - // Add right-click context menu - messageDiv.addEventListener('contextmenu', (e) => { - e.preventDefault(); - showContextMenu(message, e.clientX, e.clientY); - }); - - // Прокрутка к новому сообщению - messagesContainer.scrollTop = messagesContainer.scrollHeight; -} - -/** - * Sends a message via WebSocket - */ -export async function sendMessage(): Promise { - const input = document.querySelector('.message-input') as HTMLInputElement; - const message = input.value.trim(); - - if (message) { - const response = await request({ - data: { - content: message - }, - credentials: { - scheme: "Bearer", - credentials: authToken! - }, - type: "sendMessage" - }) - - console.log(response) - if (!response.error) { - input.value = ""; - } - } -} - -/** - * Updates an existing message in the chat interface - * @param {Message} message - Updated message object - */ -export function updateMessage(message: Message): void { - const messageElement = document.querySelector(`[data-id="${message.id}"]`) as HTMLElement; - if (!messageElement) return; - - const contentDiv = messageElement.querySelector('.message-content') as HTMLElement; - const timeDiv = messageElement.querySelector('.message-time') as HTMLElement; - - if (contentDiv) { - contentDiv.textContent = message.content; - } - - if (timeDiv) { - let timeText = formatTime(message.timestamp); - if (message.is_edited) { - timeText += ' (edited)'; - } - timeDiv.textContent = timeText; - } -} - -/** - * Removes a message from the chat interface - * @param {number} messageId - ID of the message to remove - */ -export function removeMessage(messageId: number): void { - const messageElement = document.querySelector(`[data-id="${messageId}"]`) as HTMLElement; - if (messageElement) { - messageElement.remove(); - } -} - -/** - * Handles WebSocket message updates - * @param {WebSocketMessage} response - WebSocket response - */ -export function handleWebSocketMessage(response: WebSocketMessage): void { - if (ChatPanelController.active == publicChatPanel) { - switch (response.type) { - case 'messageEdited': - if (response.data) { - updateMessage(response.data); - } - break; - case 'messageDeleted': - if (response.data && response.data.message_id) { - removeMessage(response.data.message_id); - } - break; - case 'newMessage': - if (response.data) { - const isAuthor = response.data.username === currentUser?.username; - addMessage(response.data, isAuthor); - } - break; - } - } -} - -export const publicChatPanel = new PublicChatPanel(); - -publicChatPanel.activate(); \ No newline at end of file diff --git a/frontend/src/__chat/contextMenu.ts b/frontend/src/__chat/contextMenu.ts deleted file mode 100644 index 7c960b1..0000000 --- a/frontend/src/__chat/contextMenu.ts +++ /dev/null @@ -1,321 +0,0 @@ -/** - * @fileoverview Message context menu functionality - * @description Handles right-click context menu for message actions (edit, delete, reply) - * @author Cursor - * @version 1.0.0 - */ - -import { request } from "../websocket"; -import type { Message } from "../core/types"; -import { showSuccess, showError } from "../utils/notification"; -import { delay, id } from "../utils/utils"; -import type { Dialog } from "mdui/components/dialog"; -import type { TextField } from "mdui/components/text-field"; -import { currentUser, authToken } from "../auth/api"; - - -let menu = id("message-context-menu")!; -let editDialog = id("edit-message-dialog"); -let replyDialog = id("reply-message-dialog"); -let currentMessage: Message | null = null; - -function init() { - bindEvents(); -} - -/** - * Binds event listeners - * @private - */ -function bindEvents(): void { - // Context menu events - menu?.addEventListener('click', (e) => { - const target = e.target as HTMLElement; - const action = target.closest('.context-menu-item')?.getAttribute('data-action'); - - if (action && currentMessage) { - handleAction(action, currentMessage); - } - }); - - // Close menu when clicking outside - document.addEventListener('click', (e) => { - if (!menu?.contains(e.target as Node)) { - hide(); - } - }); - - // Edit dialog events - const editCancelBtn = editDialog?.querySelector('#edit-cancel'); - const editSaveBtn = editDialog?.querySelector('#edit-save'); - - editCancelBtn?.addEventListener('click', () => hideEditDialog()); - editSaveBtn?.addEventListener('click', () => saveEdit()); - - // Reply dialog events - const replyCancelBtn = replyDialog?.querySelector('#reply-cancel'); - const replySendBtn = replyDialog?.querySelector('#reply-send'); - - replyCancelBtn?.addEventListener('click', () => hideReplyDialog()); - replySendBtn?.addEventListener('click', () => sendReply()); - - // Keyboard shortcuts - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') { - hide(); - hideEditDialog(); - hideReplyDialog(); - } - }); -} - -/** - * Shows the context menu at the specified position - * @param {Message} message - The message to show menu for - * @param {number} x - X coordinate - * @param {number} y - Y coordinate - */ -export function show(message: Message, x: number, y: number): void { - currentMessage = message; - - // Show delete for own messages and for owner on any message - const editItem = menu.querySelector('[data-action="edit"]') as HTMLElement; - const deleteItem = menu.querySelector('[data-action="delete"]') as HTMLElement; - - const isAuthor = message.username === currentUser?.username; - const isOwner = currentUser?.admin; - - // Check if we're in a DM conversation - const isInDm = document.querySelector('.chat-tabs mdui-tab[value="dms"]')?.getAttribute('active') === 'true'; - - // Show edit only for own messages - editItem.style.display = isAuthor ? 'flex' : 'none'; - - // Show delete for own messages, admin on any message, or in DMs for any message - const canDelete = isAuthor || isOwner || isInDm; - deleteItem.style.display = canDelete ? 'flex' : 'none'; - - // Position the menu properly - menu.style.display = 'block'; - - let menuWidth = menu.offsetWidth; - let menuHeight = menu.offsetHeight; - - let adjustedX = x; - let adjustedY = y; - let vertical = "top"; - let horizontal = "right"; - - // Adjust horizontal position if menu would go off-screen - if (x + menuWidth > window.innerWidth) { - adjustedX = x - menuWidth; - horizontal = "left"; - } - - // Adjust vertical position if menu would go off-screen - if (y + menuHeight > window.innerHeight) { - adjustedY = y - menuHeight; - vertical = "bottom"; - } - - // Ensure menu doesn't go off the left or top edges - adjustedX = Math.max(0, adjustedX); - adjustedY = Math.max(0, adjustedY); - - menu.style.left = `${adjustedX}px`; - menu.style.top = `${adjustedY}px`; - menu.classList.add(`pos-${vertical}-${horizontal}`, "open"); -} - -/** - * Hides the context menu - */ -export function hide(): void { - menu.style.display = 'none'; - menu.classList.forEach((name) => { - if (name.match(/pos-\w+-\w+/)) { - menu.classList.remove(name); - } - }) - currentMessage = null; -} - -/** - * Handles context menu actions - * @param {string} action - The action to perform - * @param {Message} message - The message to act on - * @private - */ -function handleAction(action: string, message: Message): void { - hide(); - - switch (action) { - case 'edit': - showEditDialog(message); - break; - case 'delete': - deleteMessage(message); - break; - case 'reply': - showReplyDialog(message); - break; - } -} - -/** - * Shows the edit dialog - * @param {Message} message - The message to edit - * @private - */ -async function showEditDialog(message: Message): Promise { - const textField = editDialog.querySelector('#edit-message-input') as TextField; - textField.value = message.content; - - currentMessage = message; - editDialog.open = true; - - // Focus the text field - await delay(100); - textField?.focus(); -} - -/** - * Hides the edit dialog - * @private - */ -function hideEditDialog(): void { - editDialog.open = false; -} - -/** - * Saves the edited message - * @private - */ -async function saveEdit(): Promise { - if (!currentMessage) return; - - const textField = editDialog.querySelector('#edit-message-input') as TextField; - const newContent = textField?.value?.trim() || ''; - - if (!newContent) { - showError('Message cannot be empty'); - return; - } - - const response = await request({ - type: "editMessage", - data: { - message_id: currentMessage.id, - content: newContent - }, - credentials: { - scheme: "Bearer", - credentials: authToken! - } - }); - - if (response.error) { - showError(response.error.detail); - } else { - showSuccess('Message edited successfully'); - hideEditDialog(); - } -} - -/** - * Shows the reply dialog - * @param {Message} message - The message to reply to - * @private - */ -async function showReplyDialog(message: Message): Promise { - const preview = replyDialog.querySelector('#reply-preview') as HTMLElement; - preview.innerHTML = ` -
- ${message.username}: ${message.content} -
- `; - - currentMessage = message; - replyDialog.open = true; - - // Focus the text field - await delay(100); - const textField = replyDialog?.querySelector('#reply-message-input') as TextField; - textField?.focus(); -} - -/** - * Hides the reply dialog - * @private - */ -function hideReplyDialog(): void { - replyDialog.open = false; -} - -/** - * Sends the reply message - * @private - */ -async function sendReply(): Promise { - if (!currentMessage) return; - - const textField = replyDialog.querySelector('#reply-message-input') as TextField; - const content = textField?.value?.trim() || ''; - - if (!content) { - showError('Reply cannot be empty'); - return; - } - - const response = await request({ - type: "replyMessage", - data: { - content: content, - reply_to_id: currentMessage.id - }, - credentials: { - scheme: "Bearer", - credentials: authToken! - } - }); - - if (response.error) { - showError(response.error.detail); - } else { - showSuccess('Reply sent successfully'); - hideReplyDialog(); - if (textField) { - textField.value = ''; - } - } -} - -/** - * Deletes a message - * @param {Message} message - The message to delete - * @private - */ -async function deleteMessage(message: Message): Promise { - if (!confirm('Are you sure you want to delete this message?')) { - return; - } - - const response = await request({ - type: "deleteMessage", - data: { - message_id: message.id - }, - credentials: { - scheme: "Bearer", - credentials: authToken! - } - }); - - if (response.error) { - showError(response.error.detail); - } else { - showSuccess('Message deleted successfully'); - } -} - -init(); \ No newline at end of file diff --git a/frontend/src/__chat/dm.ts b/frontend/src/__chat/dm.ts deleted file mode 100644 index fc56e4f..0000000 --- a/frontend/src/__chat/dm.ts +++ /dev/null @@ -1,393 +0,0 @@ -import { API_BASE_URL } from "../core/config"; -import { authToken, getAuthHeaders, currentUser } from "../auth/api"; -import { DmPanel } from "./panel"; -import { ecdhSharedSecret, deriveWrappingKey } from "../crypto/asymmetric"; -import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../crypto/symmetric"; -import { randomBytes } from "../crypto/kdf"; -import { getCurrentKeys } from "../auth/crypto"; -import { request, websocket } from "../websocket"; -import type { FetchDMResponse, SendDMRequest, WebSocketMessage, User, DmEnvelope } from "../core/types"; -import type { Tabs } from "mdui/components/tabs"; -import { b64, ub64 } from "../utils/utils"; -import defaultAvatar from "../resources/images/default-avatar.png"; - -export async function sendDm(recipientId: number, recipientPublicKeyB64: string, plaintext: string): Promise { - const keys = getCurrentKeys(); - if (!keys) throw new Error("Keys not initialized"); - const mk = randomBytes(32); - const wkSalt = randomBytes(16); - const shared = ecdhSharedSecret(keys.privateKey, ub64(recipientPublicKeyB64)); - const wkRaw = await deriveWrappingKey(shared, wkSalt, new Uint8Array([1])); - const wk = await importAesGcmKey(wkRaw); - const encMsg = await aesGcmEncrypt(await importAesGcmKey(mk), new TextEncoder().encode(plaintext)); - const wrap = await aesGcmEncrypt(wk, mk); - - await fetch(`${API_BASE_URL}/dm/send`, { - method: "POST", - headers: getAuthHeaders(true), - body: JSON.stringify({ - recipientId: recipientId, - iv: b64(encMsg.iv), - ciphertext: b64(encMsg.ciphertext), - salt: b64(wkSalt), - iv2: b64(wrap.iv), - wrappedMk: b64(wrap.ciphertext) - }) - }); -} - - - -export async function fetchDm(since?: number): Promise { - const url = new URL(`${API_BASE_URL}/dm/fetch`); - if (since) url.searchParams.set("since", String(since)); - - const response = await fetch(url, { - headers: getAuthHeaders(true) - }); - - if (response.ok) { - const data: FetchDMResponse = await response.json(); - return data.messages ?? []; - } else { - return []; - } -} - -export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string): Promise { - const keys = getCurrentKeys(); - if (!keys) throw new Error("Keys not initialized"); - - // Obtain the key - const shared = ecdhSharedSecret(keys.privateKey, ub64(senderPublicKeyB64)); - const wkRaw = await deriveWrappingKey(shared, ub64(envelope.salt), new Uint8Array([1])); - const wk = await importAesGcmKey(wkRaw); - const mk = await aesGcmDecrypt(wk, ub64(envelope.iv2), ub64(envelope.wrappedMk)); - - // Decrypt - const msg = await aesGcmDecrypt(await importAesGcmKey(mk), ub64(envelope.iv), ub64(envelope.ciphertext)); - return new TextDecoder().decode(msg); -} - -let activeDm: { userId: number; username: string; publicKey: string | null } | null = null; -let usersLoaded = false; -let dmPanel: DmPanel | null = null; -const dmBadgeByUserId: Map = new Map(); -const dmSupportingTextByUserId: Map = new Map(); - -function getLastReadId(userId: number): number { - try { - const v = localStorage.getItem(`dmLastRead:${userId}`); - return v ? Number(v) : 0; - } catch { - return 0; - } -} - -function setLastReadId(userId: number, id: number): void { - try { - localStorage.setItem(`dmLastRead:${userId}`, String(id)); - } catch {} -} - -async function loadUsers() { - const res = await fetch(`${API_BASE_URL}/users`, { headers: getAuthHeaders(true) }); - if (!res.ok) return; - const data = await res.json(); - const list = document.getElementById("dm-users")!; - list.innerHTML = ""; - (data.users || []).forEach((u: User) => { - const item = document.createElement("mdui-list-item"); - item.id = `dm-user-${u.id}`; - - // Add avatar - const avatar = document.createElement("img"); - avatar.src = u.profile_picture || defaultAvatar; - avatar.alt = u.username; - avatar.slot = "icon"; - avatar.style.width = "40px"; - avatar.style.height = "40px"; - avatar.style.borderRadius = "50%"; - avatar.style.objectFit = "cover"; - - // Handle avatar load error - avatar.addEventListener("error", () => { - avatar.src = defaultAvatar; - }); - - item.appendChild(avatar); - - // Set headline (username) - item.setAttribute("headline", u.username); - - // Add supporting text container (hidden until loaded) - const lastMessageEl = document.createElement("div"); - lastMessageEl.slot = "description"; - lastMessageEl.style.fontSize = "12px"; - lastMessageEl.style.color = "var(--mdui-color-on-surface-variant)"; - lastMessageEl.style.whiteSpace = "pre-line"; - lastMessageEl.style.display = "none"; - item.appendChild(lastMessageEl); - dmSupportingTextByUserId.set(u.id, lastMessageEl); - - // Add unread badge (hidden by default) - const badge = document.createElement("mdui-badge"); - badge.setAttribute("slot", "end-icon"); - badge.style.display = "none"; - item.appendChild(badge); - dmBadgeByUserId.set(u.id, badge); - - // Load last message when element becomes visible - const observer = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - if (entry.isIntersecting) { - loadLastMessage(u.id); - observer.unobserve(entry.target); - } - }); - }); - observer.observe(item); - - item.addEventListener("click", async () => { - activeDm = { userId: u.id, username: u.username, publicKey: null }; - if (!dmPanel) { - dmPanel = new DmPanel( - async (text: string) => { - if (activeDm?.publicKey) { - // WebSocket realtime send - const keys = getCurrentKeys(); - if (!keys) return; - - // Encryption key - const mk = randomBytes(32); - const wkSalt = randomBytes(16); - const shared = ecdhSharedSecret(keys.privateKey, ub64(activeDm.publicKey)); - const wkRaw = await deriveWrappingKey(shared, wkSalt, new Uint8Array([1])); - const wk = await importAesGcmKey(wkRaw); - - // Encrypt the message - const encMsg = await aesGcmEncrypt(await importAesGcmKey(mk), new TextEncoder().encode(text)); - const wrap = await aesGcmEncrypt(wk, mk); - - const payload: SendDMRequest = { - recipientId: activeDm.userId, - iv: b64(encMsg.iv), - ciphertext: b64(encMsg.ciphertext), - salt: b64(wkSalt), - iv2: b64(wrap.iv), - wrappedMk: b64(wrap.ciphertext) - } - - request({ - type: "dmSend", - credentials: { - scheme: "Bearer", - credentials: authToken! - }, - data: payload - }); - } - }, - async () => { - // Load DM history for the active conversation - if (!activeDm?.publicKey || !dmPanel) return; - const response = await fetch(`${API_BASE_URL}/dm/history/${activeDm.userId}`, { - headers: getAuthHeaders(true) - }); - if (response.ok) { - const data = await response.json(); - const messages: DmEnvelope[] = data.messages || []; - const container = document.getElementById("chat-messages")!; - container.innerHTML = ""; - let maxIncomingId = 0; - for (const env of messages) { - try { - // Always use other user's public key for ECDH (our private + their public) - const text = await decryptDm(env, activeDm.publicKey!); - const isAuthor = env.senderId !== activeDm.userId; - const username = isAuthor ? (currentUser?.username || "Unknown") : (activeDm.username || "Unknown"); - dmPanel.appendMessageWithId({ - id: env.id, - content: text, - username: username, - timestamp: env.timestamp, - is_read: false, - is_edited: false - }); - if (env.senderId === activeDm.userId && env.id > maxIncomingId) { - maxIncomingId = env.id; - } - } catch (e) { - console.error("Error while loading message:", e); - } - } - container.scrollTop = container.scrollHeight; - if (maxIncomingId > 0) { - setLastReadId(activeDm.userId, maxIncomingId); - const badgeEl = dmBadgeByUserId.get(activeDm.userId); - if (badgeEl) { - badgeEl.style.display = "none"; - badgeEl.textContent = ""; - } - } - } - } - ); - } - dmPanel.setOtherUser(u.username); - dmPanel.setTitle(u.username); - dmPanel.clearMessages(); - - // Add profile click functionality to chat header - const chatHeaderAvatar = document.querySelector('.chat-header-avatar') as HTMLElement; - if (chatHeaderAvatar) { - chatHeaderAvatar.style.cursor = 'pointer'; - chatHeaderAvatar.onclick = () => dmPanel?.onProfileClicked(); - } - - const resPk = await fetch(`${API_BASE_URL}/crypto/public-key/of/${u.id}`, { headers: getAuthHeaders(true) }); - if (resPk.ok) { - const pkData = await resPk.json(); - activeDm!.publicKey = pkData.publicKey; - } - - // Only activate after we have the public key so loader can decrypt - dmPanel.activate(); - // Clear unread badge on open - const badgeEl = dmBadgeByUserId.get(u.id); - if (badgeEl) { - badgeEl.textContent = ""; - badgeEl.style.display = "none"; - } - }); - list.appendChild(item); - }); -} - -async function loadLastMessage(userId: number): Promise { - try { - const pkRes = await fetch(`${API_BASE_URL}/crypto/public-key/of/${userId}`, { headers: getAuthHeaders(true) }); - if (!pkRes.ok) return; - const pkData = await pkRes.json(); - const otherPk = pkData.publicKey as string; - const response = await fetch(`${API_BASE_URL}/dm/history/${userId}?limit=50`, { - headers: getAuthHeaders(true) - }); - if (response.ok) { - const data = await response.json(); - const messages: DmEnvelope[] = data.messages || []; - const supporting = dmSupportingTextByUserId.get(userId); - const badgeEl = dmBadgeByUserId.get(userId); - if (!supporting || !badgeEl) return; - let lastPlaintext: string | null = null; - let lastEnv: DmEnvelope | null = null; - for (const env of messages) { - if (!lastEnv || env.id > lastEnv.id) lastEnv = env; - } - if (lastEnv) { - try { lastPlaintext = await decryptDm(lastEnv, otherPk); } catch {} - } - if (lastPlaintext && lastPlaintext.trim().length > 0) { - const lines = lastPlaintext.split(/\r?\n/).slice(0, 2); - supporting.textContent = lines.join("\n"); - supporting.style.display = "block"; - } else { - supporting.textContent = ""; - supporting.style.display = "none"; - } - const lastRead = getLastReadId(userId); - let unread = 0; - for (const env of messages) { - if (env.senderId === userId && env.id > lastRead) unread++; - } - if (unread > 0) { - badgeEl.textContent = String(unread); - badgeEl.style.display = "inline-flex"; - } else { - badgeEl.textContent = ""; - badgeEl.style.display = "none"; - } - } else { - const supporting = dmSupportingTextByUserId.get(userId); - if (supporting) { - supporting.textContent = ""; - supporting.style.display = "none"; - } - } - } catch (error) { - const supporting = dmSupportingTextByUserId.get(userId); - if (supporting) { - supporting.textContent = ""; - supporting.style.display = "none"; - } - } -} - -function init() { - const tabs = document.querySelector(".chat-tabs mdui-tabs") as Tabs; - const dmTab = tabs?.querySelector('mdui-tab[value="dms"]')!; - function ensureUsersLoaded() { - if (!usersLoaded) { - usersLoaded = true; - loadUsers(); - } - } - dmTab.addEventListener("click", ensureUsersLoaded); - tabs.addEventListener("change", (e: any) => { - if (e.detail?.value === "dms") { - ensureUsersLoaded(); - dmPanel?.activate(); - } - }); -} - -init(); - -// realtime incoming DMs -websocket.addEventListener("message", async (e) => { - try { - const msg: WebSocketMessage = JSON.parse((e as MessageEvent).data); - if (msg.type === "dmNew") { - if (activeDm && (msg.data.senderId === activeDm.userId || msg.data.recipientId === activeDm.userId)) { - // Always use other user's public key (our private is implied by getCurrentKeys) - const plaintext = await decryptDm(msg.data, activeDm.publicKey!); - if (dmPanel) { - const isAuthor = msg.data.senderId !== activeDm.userId; - dmPanel.appendMessageWithId({ - id: msg.data.id, - content: plaintext, - username: isAuthor ? (currentUser?.username || "Unknown") : (activeDm.username || "Unknown"), - timestamp: msg.data.timestamp, - is_read: false, - is_edited: false - }); - } - if (msg.data.senderId === activeDm.userId) { - setLastReadId(activeDm.userId, Math.max(getLastReadId(activeDm.userId), msg.data.id)); - } - } else { - const otherUserId = msg.data.senderId; - const badgeEl = dmBadgeByUserId.get(otherUserId); - if (badgeEl) { - const current = Number(badgeEl.textContent || 0); - const next = (current || 0) + 1; - badgeEl.textContent = String(next); - badgeEl.style.display = "inline-flex"; - } - try { - const pkRes = await fetch(`${API_BASE_URL}/crypto/public-key/of/${otherUserId}`, { headers: getAuthHeaders(true) }); - if (pkRes.ok) { - const pkData = await pkRes.json(); - const plaintext = await decryptDm(msg.data, pkData.publicKey); - const supporting = dmSupportingTextByUserId.get(otherUserId); - if (supporting && plaintext) { - const lines = plaintext.split(/\r?\n/).slice(0, 2); - supporting.textContent = lines.join("\n"); - supporting.style.display = lines.length ? "block" : "none"; - } - } - } catch {} - } - } - } catch {} -}); \ No newline at end of file diff --git a/frontend/src/__chat/panel.ts b/frontend/src/__chat/panel.ts deleted file mode 100644 index 8614d5b..0000000 --- a/frontend/src/__chat/panel.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { authToken, currentUser, getAuthHeaders } from "../auth/api.ts"; -import { API_BASE_URL } from "../core/config.ts"; -import type { Message, Messages, WebSocketMessage } from "../core/types"; -import { request } from "../websocket.ts"; -import { addMessage } from "./chat.ts"; -import { show as showContextMenu } from "./contextMenu.ts"; -import { show as showProfileDialog } from "./profileDialog.ts"; - -const titleEl = document.getElementById("chat-name")!; -const messages = document.getElementById("chat-messages")!; -const input = document.getElementById("message-input") as HTMLInputElement; -const form = document.getElementById("message-form") as HTMLFormElement; - -export abstract class ChatPanelController { - static active: ChatPanelController | null = null; - static mounted = false; - - activate(): void { - ChatPanelController.active = this; - if (currentUser) { - this.loadMessages(); - } - } - - setTitle(title: string): void { - titleEl.textContent = title; - } - - clearMessages(): void { - messages.innerHTML = ""; - } - - appendSimple(text: string, isAuthor: boolean): void { - const div = document.createElement("div"); - div.className = `message ${isAuthor ? "sent" : "received"}`; - const inner = document.createElement("div"); - inner.className = "message-inner"; - const content = document.createElement("div"); - content.className = "message-content"; - content.textContent = text; - inner.appendChild(content); - div.appendChild(inner); - messages.appendChild(div); - messages.scrollTop = messages.scrollHeight; - } - - protected abstract onSubmit(text: string): void | Promise; - protected abstract loadMessages(): void | Promise; - public abstract onProfileClicked(): void; - - static mountOnce(): void { - if (this.mounted) return; - this.mounted = true; - if (!form) return; - form.addEventListener( - "submit", - (e) => { - if (!ChatPanelController.active) return; // let others handle - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - const text = input.value.trim(); - if (!text) return; - Promise.resolve(ChatPanelController.active.onSubmit(text)).finally(() => { - input.value = ""; - }); - }, - true - ); - } -} - -export class PublicChatPanel extends ChatPanelController { - protected async onSubmit(text: string): Promise { - const payload: WebSocketMessage = { - data: { content: text }, - credentials: { scheme: "Bearer", credentials: authToken! }, - type: "sendMessage" - }; - await request(payload); - } - - protected loadMessages(): void { - fetch(`${API_BASE_URL}/get_messages`, { - headers: getAuthHeaders() - }) - .then(response => response.json()) - .then((data: Messages) => { - if (data.messages && data.messages.length > 0) { - messages.innerHTML = ""; - - const messagesContainer = document.querySelector('.chat-messages') as HTMLElement; - - const lastMessage = messagesContainer.lastElementChild as HTMLElement - let lastMessageId: number = 0 - if (lastMessage) { - lastMessageId = Number(lastMessage.dataset.id) - } - - // Добавляем только новые сообщения - data.messages.forEach(msg => { - console.log(msg); - if (msg.id > lastMessageId) { - addMessage(msg, msg.username == currentUser!.username); - } - }); - } - }); - } - - public onProfileClicked(): void { - // Public chat doesn't have a specific profile to show - } -} - -export class DmPanel extends ChatPanelController { - private sender: (text: string) => Promise; - private loader: () => Promise | void; - private otherUsername: string | null = null; - - constructor(sender: (text: string) => Promise, loader: () => Promise | void) { - super(); - this.sender = sender; - this.loader = loader; - } - - setOtherUser(username: string): void { - this.otherUsername = username; - } - - protected async onSubmit(text: string): Promise { - this.appendSimple(text, true); - await this.sender(text); - } - - protected loadMessages(): void | Promise { - return this.loader(); - } - - public onProfileClicked(): void { - if (this.otherUsername) { - // Import and show the profile dialog - showProfileDialog(this.otherUsername!); - } - } - - appendMessageWithId(message: Message): void { - const div = document.createElement("div"); - const isAuthor = message.username === currentUser?.username; - div.className = `message ${isAuthor ? "sent" : "received"}`; - div.setAttribute("data-message-id", message.id.toString()); - div.setAttribute("data-timestamp", message.timestamp); - - const inner = document.createElement("div"); - inner.className = "message-inner"; - - const content = document.createElement("div"); - content.className = "message-content"; - content.textContent = message.content; - - inner.appendChild(content); - div.appendChild(inner); - - // Add context menu support - div.addEventListener("contextmenu", (e) => { - e.preventDefault(); - showContextMenu(message, e.clientX, e.clientY); - }); - - messages.appendChild(div); - messages.scrollTop = messages.scrollHeight; - } -} - -ChatPanelController.mountOnce(); \ No newline at end of file diff --git a/frontend/src/__chat/profileDialog.ts b/frontend/src/__chat/profileDialog.ts deleted file mode 100644 index 3b8906e..0000000 --- a/frontend/src/__chat/profileDialog.ts +++ /dev/null @@ -1,253 +0,0 @@ -/** - * @fileoverview User profile dialog functionality - * @description Handles displaying user profiles in a modal dialog - * @author Cursor - * @version 1.0.0 - */ - -import { getAuthHeaders, currentUser } from "../auth/api"; -import { API_BASE_URL } from "../core/config"; -import type { UserProfile } from "../core/types"; -import { showError, showSuccess } from "../utils/notification"; -import { delay, formatTime, id } from "../utils/utils"; -import defaultAvatar from "../resources/images/default-avatar.png"; -import type { Tabs } from "mdui/components/tabs"; -import type { Dialog } from "mdui/components/dialog"; -import type { TextField } from "mdui/components/text-field"; - - -let dialog = id("user-profile-dialog"); -let currentProfile: UserProfile | null = null; -let isOwnProfile: boolean = false; - -function init() { - bindEvents(); -} - -/** - * Binds event listeners - * @private - */ -function bindEvents(): void { - // Edit bio events - const editBioBtn = dialog?.querySelector('#edit-bio-btn'); - const saveBioBtn = dialog?.querySelector('#save-bio-btn'); - const cancelBioBtn = dialog?.querySelector('#cancel-bio-btn'); - - editBioBtn?.addEventListener('click', () => startEditBio()); - saveBioBtn?.addEventListener('click', () => saveBio()); - cancelBioBtn?.addEventListener('click', () => cancelEditBio()); - - // DM button event - const dmButton = dialog?.querySelector('#dm-button'); - dmButton?.addEventListener('click', () => startDm()); -} - -/** - * Starts a direct message conversation - * @private - */ -async function startDm(): Promise { - if (!currentProfile || isOwnProfile) return; - - // Hide the profile dialog - hide(); - - // Switch to DMs tab - const tabs = document.querySelector('.chat-tabs mdui-tabs') as Tabs; - if (tabs) { - tabs.value = 'dms'; - } - - // Find and click on the user in the DM users list - await delay(100); - const dmUsersList = document.getElementById("dm-users"); - if (dmUsersList) { - const userItems = dmUsersList.querySelectorAll('mdui-list-item'); - for (const item of userItems) { - const headline = item.getAttribute('headline'); - if (headline === currentProfile?.username) { - (item as HTMLElement).click(); - break; - } - } - } -} - -/** - * Shows the profile dialog for a specific user - * @param {string} username - Username to show profile for - */ -export async function show(username: string): Promise { - if (!dialog) return; - - try { - const response = await fetch(`${API_BASE_URL}/user/${username}`, { - headers: getAuthHeaders() - }); - - if (!response.ok) { - throw new Error('Failed to load user profile'); - } - - const profile: UserProfile = await response.json(); - currentProfile = profile; - isOwnProfile = profile.username === currentUser?.username; - populateDialog(profile); - dialog.open = true; - - } catch (error) { - showError('Failed to load user profile'); - console.error('Error loading user profile:', error); - } -} - -/** - * Populates the dialog with user data - * @param {UserProfile} profile - User profile data - * @private - */ -function populateDialog(profile: UserProfile): void { - if (!dialog) return; - - // Profile picture - const profilePic = dialog.querySelector('.profile-picture') as HTMLImageElement; - profilePic.src = profile.profile_picture || defaultAvatar; - - let errorLock = false - - profilePic.addEventListener("error", () => { - if (!errorLock) { - profilePic.src = defaultAvatar; - errorLock = true; - } - }); - - // Username - const usernameEl = dialog.querySelector('.username') as HTMLElement; - usernameEl.textContent = profile.username; - - // Online status - const onlineStatus = dialog.querySelector('.online-status') as HTMLElement; - if (profile.online) { - onlineStatus.innerHTML = ' Online'; - onlineStatus.classList.add("online-status", "online"); - } else { - onlineStatus.innerHTML = ` Last seen ${formatTime(profile.last_seen)}`; - onlineStatus.classList.add("online-status", "offline"); - } - - // Bio - const bioDisplay = dialog.querySelector('.bio-display') as HTMLElement; - const bioEdit = dialog.querySelector('#bio-edit-field') as TextField; - - if (profile.bio) { - bioDisplay.textContent = profile.bio; - } else { - bioDisplay.textContent = isOwnProfile ? 'No bio yet. Click "Edit Bio" to add one!' : 'No bio available.'; - } - - if (bioEdit) { - bioEdit.value = profile.bio || ''; - } - - // Stats - const memberSince = dialog.querySelector('.member-since') as HTMLElement; - const lastSeen = dialog.querySelector('.last-seen') as HTMLElement; - - memberSince.textContent = formatTime(profile.created_at); - lastSeen.textContent = formatTime(profile.last_seen); -} - -/** - * Starts editing the bio - * @private - */ -function startEditBio(): void { - if (!dialog) return; - - const bioDisplay = dialog.querySelector('.bio-display') as HTMLElement; - const bioEdit = dialog.querySelector('#bio-edit-field') as TextField; - const bioActions = dialog.querySelector('.bio-actions') as HTMLElement; - const editBioBtn = dialog.querySelector('#edit-bio-btn') as HTMLElement; - - bioDisplay.style.display = 'none'; - if (bioEdit) bioEdit.style.display = 'block'; - bioActions.style.display = 'flex'; - editBioBtn.style.display = 'none'; - - if (bioEdit) { - bioEdit.focus(); - bioEdit.setSelectionRange(bioEdit.value.length, bioEdit.value.length); - } -} - -/** - * Saves the bio - * @private - */ -export async function saveBio(): Promise { - if (!dialog || !currentProfile) return; - - const bioEdit = dialog.querySelector('#bio-edit-field') as TextField; - const newBio = bioEdit?.value?.trim() || ''; - - try { - const response = await fetch(`${API_BASE_URL}/user/bio`, { - method: 'PUT', - headers: { - ...getAuthHeaders(), - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ bio: newBio }) - }); - - if (!response.ok) { - throw new Error('Failed to update bio'); - } - - const result = await response.json(); - currentProfile.bio = result.bio; - populateDialog(currentProfile); - cancelEditBio(); - showSuccess('Bio updated successfully'); - - } catch (error) { - showError('Failed to update bio'); - console.error('Error updating bio:', error); - } -} - -/** - * Cancels bio editing - * @private - */ -function cancelEditBio(): void { - if (!dialog) return; - - const bioDisplay = dialog.querySelector('.bio-display') as HTMLElement; - const bioEdit = dialog.querySelector('#bio-edit-field') as TextField; - const bioActions = dialog.querySelector('.bio-actions') as HTMLElement; - const editBioBtn = dialog.querySelector('#edit-bio-btn') as HTMLElement; - - bioDisplay.style.display = 'block'; - if (bioEdit) bioEdit.style.display = 'none'; - bioActions.style.display = 'none'; - editBioBtn.style.display = isOwnProfile ? 'block' : 'none'; - - // Reset bio edit to current value - if (bioEdit) { - bioEdit.value = currentProfile?.bio || ''; - } -} - -/** - * Hides the dialog - */ -export function hide(): void { - dialog.open = false; - currentProfile = null; - isOwnProfile = false; -} - -init(); \ No newline at end of file diff --git a/frontend/src/__navigation.__ts b/frontend/src/__navigation.__ts deleted file mode 100644 index ca1e994..0000000 --- a/frontend/src/__navigation.__ts +++ /dev/null @@ -1,42 +0,0 @@ -import { clearAlerts } from "./auth/auth.ts"; -import { publicChatPanel } from "./chat/chat.ts"; -import { id } from "./utils/utils.ts"; - -const loginForm = id("login-form"); -const registerForm = id("register-form"); -const chatInterface = id("chat-interface"); -const titleBar = id("electron-title-bar"); - -/** - * Shows the login form and hides other interfaces. - */ -export function showLogin(): void { - loginForm.style.display = 'flex'; - registerForm.style.display = 'none'; - chatInterface.style.display = 'none'; - clearAlerts(); - titleBar.classList.add("color-surface"); -} - -/** - * Shows the registration form and hides other interfaces. - */ -export function showRegister(): void { - loginForm.style.display = 'none'; - registerForm.style.display = 'flex'; - chatInterface.style.display = 'none'; - clearAlerts(); - titleBar.classList.add("color-surface"); -} - -/** - * Shows the chat interface and hides authentication forms. - */ -export function showChat(): void { - loginForm.style.display = 'none'; - registerForm.style.display = 'none'; - chatInterface.style.display = 'block'; - titleBar.classList.remove("color-surface"); - - publicChatPanel.activate(); -} \ No newline at end of file diff --git a/frontend/src/__userPanel/profile/api.ts b/frontend/src/__userPanel/profile/api.ts deleted file mode 100644 index 826c583..0000000 --- a/frontend/src/__userPanel/profile/api.ts +++ /dev/null @@ -1,140 +0,0 @@ -/** - * @fileoverview Profile-related API calls - * @description Handles all profile-related HTTP requests to the backend - * @author Cursor - * @version 1.0.0 - */ - -import { getAuthHeaders } from '../../auth/api'; -import type { ProfileData, UploadResponse } from './types'; - -/** - * Loads user profile data from the server - * @async - * @returns User profile data or null if failed - * @example - * const profile = await loadProfile(); - * if (profile) { - * console.log('User nickname:', profile.nickname); - * } - */ -export async function loadProfile(): Promise { - try { - const response = await fetch('/api/user/profile', { - headers: getAuthHeaders() - }); - - if (response.ok) { - const data = await response.json(); - // Map backend fields to frontend fields - return { - profile_picture: data.profile_picture, - nickname: data.username, - description: data.bio - }; - } - - return null; - } catch (error) { - console.error('Error loading profile:', error); - return null; - } -} - -/** - * Uploads a profile picture to the server - * @param {Blob} file - The image file to upload - * @returns {Promise} Upload response with URL or null if failed - * @example - * const fileInput = document.getElementById('file-input'); - * const file = fileInput.files[0]; - * const result = await uploadProfilePicture(file); - * if (result) { - * console.log('Uploaded to:', result.profile_picture_url); - * } - */ -export async function uploadProfilePicture(file: Blob): Promise { - try { - const formData = new FormData(); - formData.append('profile_picture', file, 'profile_picture.jpg'); - - const response = await fetch('/api/upload-profile-picture', { - method: 'POST', - body: formData, - headers: getAuthHeaders(false) - }); - - if (response.ok) { - return await response.json(); - } - return null; - } catch (error) { - console.error('Upload error:', error); - return null; - } -} - -/** - * Updates user profile information - * @param {Partial} data - Profile data to update - * @returns {Promise} True if update was successful, false otherwise - * @example - * const success = await updateProfile({ - * nickname: 'New Name', - * description: 'Updated bio' - * }); - * if (success) { - * console.log('Profile updated successfully'); - * } - */ -export async function updateProfile(data: Partial): Promise { - try { - // Map frontend fields to backend fields - const backendData = { - nickname: data.nickname, - description: data.description - }; - - const response = await fetch('/api/user/profile', { - method: 'PUT', - headers: { - ...getAuthHeaders(), - 'Content-Type': 'application/json' - }, - body: JSON.stringify(backendData) - }); - - return response.ok; - } catch (error) { - console.error('Error updating profile:', error); - return false; - } -} - -/** - * Updates user bio - * @param {string} bio - New bio text - * @returns {Promise} True if update was successful, false otherwise - * @example - * const success = await updateBio('My new bio text'); - * if (success) { - * console.log('Bio updated successfully'); - * } - */ -export async function updateBio(bio: string): Promise { - try { - const response = await fetch('/api/user/bio', { - method: 'PUT', - headers: { - ...getAuthHeaders(), - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ bio }) - }); - - return response.ok; - } catch (error) { - console.error('Error updating bio:', error); - return false; - } -} diff --git a/frontend/src/__userPanel/profile/editor.ts b/frontend/src/__userPanel/profile/editor.ts deleted file mode 100644 index b67ff09..0000000 --- a/frontend/src/__userPanel/profile/editor.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * @fileoverview Profile editing functionality - * @description Handles profile form editing and MDUI text field integration - * @author Cursor - * @version 1.0.0 - */ - -import { updateProfile } from './api'; -import { loadProfile } from './api'; -import { showSuccess, showError } from '../../utils/notification'; -import { TextField } from 'mdui/components/text-field'; -import { id } from '../../utils/utils'; - -let profileForm = id('profile-form')!; -let nicknameField = id('username-field'); -let descriptionField = id('description-field'); - -/** - * Initialization state flag - * @type {boolean} - */ -let isInitialized = false; - -/** - * Sets the username field value - * @param {string} value - The username value to set - */ -export function setUsernameValue(value: string): void { - if (nicknameField && nicknameField.value !== undefined) { - nicknameField.value = value; - } -} - -/** - * Sets the description field value - * @param {string} value - The description value to set - */ -export function setDescriptionValue(value: string): void { - if (descriptionField && descriptionField.value !== undefined) { - descriptionField.value = value; - } -} - -/** - * Gets the current username field value - * @returns {string} The current username value - */ -export function getUsernameValue(): string { - if (nicknameField && nicknameField.value !== undefined) { - return nicknameField.value; - } - return ''; -} - -/** - * Gets the current description field value - * @returns {string} The current description value - */ -export function getDescriptionValue(): string { - if (descriptionField && descriptionField.value !== undefined) { - return descriptionField.value; - } - return ''; -} - -/** - * Loads profile data from the server and populates the form fields - */ -export async function loadProfileData(): Promise { - const userData = await loadProfile(); - if (userData) { - if (userData.nickname) { - setUsernameValue(userData.nickname); - } - if (userData.description) { - setDescriptionValue(userData.description); - } - } -} - -/** - * Handles profile form submission - * @param {Event} e - Form submission event - * @private - */ -async function handleFormSubmission(e: Event): Promise { - e.preventDefault(); - - const nickname = getUsernameValue(); - const description = getDescriptionValue(); - - if (nickname || description) { - const success = await updateProfile({ - nickname: nickname || undefined, - description: description || undefined - }); - - if (success) { - showSuccess('Профиль обновлен!'); - } else { - showError('Ошибка при обновлении профиля'); - } - } -} - -/** - * Sets up form submission handler - * @private - */ -function setupFormHandler(): void { - if (!isInitialized) { - profileForm.addEventListener('submit', handleFormSubmission); - isInitialized = true; - } -} - -/** - * Initializes profile editor functionality - */ -export function initializeProfileEditor(): void { - setupFormHandler(); -} diff --git a/frontend/src/__userPanel/profile/imageCropper.ts b/frontend/src/__userPanel/profile/imageCropper.ts deleted file mode 100644 index e3f52a2..0000000 --- a/frontend/src/__userPanel/profile/imageCropper.ts +++ /dev/null @@ -1,219 +0,0 @@ -/** - * @fileoverview Canvas-based image cropping component - * @description Provides circular image cropping functionality with drag support - * @author Cursor - * @version 1.0.0 - */ - -import type { Size2D } from "../../core/types"; - -/** - * Image cropper class for circular profile picture cropping - * @class ImageCropper - */ -export class ImageCropper { - private canvas: HTMLCanvasElement; - private ctx: CanvasRenderingContext2D; - - /** - * Image element to be cropped - * @type {HTMLImageElement} - * @private - */ - private image!: HTMLImageElement; - - /** - * Size of the crop area (diameter) - * @type {number} - * @private - */ - private cropSize: number = 200; - private isDragging: boolean = false; - - /** - * Starting position of the drag operation - * @type {Size2D} - * @private - */ - private dragStart: Size2D = { x: 0, y: 0 }; - - /** - * Current position of the crop area - * @type {Size2D} - * @private - */ - private cropPosition: Size2D = { x: 0, y: 0 }; - - /** - * Creates a new ImageCropper instance - * @param {HTMLElement} container - Container element to append the canvas to - * @constructor - * @example - * const cropper = new ImageCropper(document.getElementById('cropper-area')); - */ - constructor(container: HTMLElement) { - this.canvas = document.createElement('canvas'); - this.canvas.width = this.cropSize; - this.canvas.height = this.cropSize; - this.ctx = this.canvas.getContext('2d')!; - - container.appendChild(this.canvas); - this.setupEventListeners(); - } - - /** - * Sets up mouse and touch event listeners - * @private - */ - private setupEventListeners(): void { - this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this)); - this.canvas.addEventListener('mousemove', this.onMouseMove.bind(this)); - this.canvas.addEventListener('mouseup', this.onMouseUp.bind(this)); - this.canvas.addEventListener('touchstart', this.onTouchStart.bind(this)); - this.canvas.addEventListener('touchmove', this.onTouchMove.bind(this)); - this.canvas.addEventListener('touchend', this.onTouchEnd.bind(this)); - } - - /** - * Handles mouse down events - * @param {MouseEvent} e - Mouse event - * @private - */ - private onMouseDown(e: MouseEvent): void { - this.isDragging = true; - this.dragStart = { x: e.clientX, y: e.clientY }; - } - - /** - * Handles mouse move events during dragging - * @param {MouseEvent} e - Mouse event - * @private - */ - private onMouseMove(e: MouseEvent): void { - if (!this.isDragging) return; - - const deltaX = e.clientX - this.dragStart.x; - const deltaY = e.clientY - this.dragStart.y; - - this.cropPosition.x += deltaX; - this.cropPosition.y += deltaY; - - this.dragStart = { x: e.clientX, y: e.clientY }; - this.render(); - } - - /** - * Handles mouse up events - * @private - */ - private onMouseUp(): void { - this.isDragging = false; - } - - /** - * Handles touch start events - * @param {TouchEvent} e - Touch event - * @private - */ - private onTouchStart(e: TouchEvent): void { - e.preventDefault(); - const touch = e.touches[0]; - this.isDragging = true; - this.dragStart = { x: touch.clientX, y: touch.clientY }; - } - - /** - * Handles touch move events during dragging - * @param {TouchEvent} e - Touch event - * @private - */ - private onTouchMove(e: TouchEvent): void { - e.preventDefault(); - if (!this.isDragging) return; - - const touch = e.touches[0]; - const deltaX = touch.clientX - this.dragStart.x; - const deltaY = touch.clientY - this.dragStart.y; - - this.cropPosition.x += deltaX; - this.cropPosition.y += deltaY; - - this.dragStart = { x: touch.clientX, y: touch.clientY }; - this.render(); - } - - /** - * Handles touch end events - * @private - */ - private onTouchEnd(): void { - this.isDragging = false; - } - - /** - * Loads an image file for cropping - * @param {File} file - Image file to load - * @returns {Promise} Promise that resolves when image is loaded - */ - loadImage(file: File): Promise { - return new Promise((resolve) => { - this.image = new Image(); - this.image.onload = () => { - this.render(); - resolve(); - }; - this.image.src = URL.createObjectURL(file); - }); - } - - /** - * Renders the image with circular crop overlay - * @private - */ - private render(): void { - if (!this.image) return; - - // Clear canvas - this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); - - // Calculate crop area - const scale = Math.max(this.cropSize / this.image.width, this.cropSize / this.image.height); - const scaledWidth = this.image.width * scale; - const scaledHeight = this.image.height * scale; - - // Draw image - this.ctx.save(); - this.ctx.globalCompositeOperation = 'source-over'; - this.ctx.drawImage( - this.image, - this.cropPosition.x, - this.cropPosition.y, - scaledWidth, - scaledHeight - ); - this.ctx.restore(); - - // Draw crop overlay - this.ctx.save(); - this.ctx.globalCompositeOperation = 'destination-in'; - this.ctx.beginPath(); - this.ctx.arc(this.cropSize / 2, this.cropSize / 2, this.cropSize / 2, 0, 2 * Math.PI); - this.ctx.fill(); - this.ctx.restore(); - } - - /** - * Gets the cropped image as a data URL - * @returns {string} Data URL of the cropped image - */ - getCroppedImage(): string { - return this.canvas.toDataURL('image/jpeg', 0.8); - } - - /** - * Destroys the cropper and removes the canvas from DOM - */ - destroy(): void { - this.canvas.remove(); - } -} diff --git a/frontend/src/__userPanel/profile/profile.ts b/frontend/src/__userPanel/profile/profile.ts deleted file mode 100644 index 1cf8ba0..0000000 --- a/frontend/src/__userPanel/profile/profile.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @fileoverview Profile module entry point and initialization - * @description Coordinates profile system initialization and form handling - * @author Cursor - * @version 1.0.0 - */ - -import type { Dialog } from "mdui/components/dialog"; -import { loadProfileData } from './editor'; -import { loadProfilePicture, initializeProfileUpload } from "./upload"; -import { initializeProfileEditor } from './editor'; -import { id } from "../../utils/utils"; - -// Handle profile form submission -const form = id("profile-form")!; -const dialog = id("profile-dialog"); - -form.addEventListener("submit", async (e) => { - e.preventDefault(); - - // TODO: Process form data if needed - // For now, just close the dialog - dialog.open = false; -}); - -/** - * Initializes profile functionality after user login - */ -export function initializeProfile(): void { - // Initialize profile modules - initializeProfileUpload(); - initializeProfileEditor(); - - // Load profile data - Promise.all([ - loadProfilePicture(), - loadProfileData() - ]).catch(error => { - console.error('Error initializing profile:', error); - }); -} \ No newline at end of file diff --git a/frontend/src/__userPanel/profile/types.ts b/frontend/src/__userPanel/profile/types.ts deleted file mode 100644 index ec134c6..0000000 --- a/frontend/src/__userPanel/profile/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @fileoverview Profile-specific type definitions - * @description Contains type definitions for profile-related functionality - * @author Cursor - * @version 1.0.0 - */ - -/** - * User profile data structure - * @interface ProfileData - * @property {string} [profile_picture] - URL to user's profile picture - * @property {string} [nickname] - User's display name - * @property {string} [description] - User's bio or description - */ -export interface ProfileData { - profile_picture?: string; - nickname?: string; - description?: string; -} - -/** - * Profile picture upload response structure - * @interface UploadResponse - * @property {string} profile_picture_url - URL to the uploaded profile picture - */ -export interface UploadResponse { - profile_picture_url: string; -} \ No newline at end of file diff --git a/frontend/src/__userPanel/profile/upload.ts b/frontend/src/__userPanel/profile/upload.ts deleted file mode 100644 index 5327102..0000000 --- a/frontend/src/__userPanel/profile/upload.ts +++ /dev/null @@ -1,150 +0,0 @@ -/** - * @fileoverview Profile picture upload functionality - * @description Handles file selection, image cropping, and profile picture upload - * @author Cursor - * @version 1.0.0 - */ - -import type { Dialog } from "mdui/components/dialog"; -import { ImageCropper } from './imageCropper'; -import { uploadProfilePicture } from './api'; -import { loadProfile } from './api'; -import { showSuccess, showError } from '../../utils/notification'; -import { id } from "../../utils/utils"; - -/** - * Global image cropper instance - */ -let cropper: ImageCropper | null = null; - -/** - * Initialization state flag - */ -let isInitialized = false; - -let cropperDialog = id('cropper-dialog'); -let fileInput = id('pfp-file-input'); -let uploadBtn = id('upload-pfp-btn'); -let cropSaveBtn = id('crop-save'); -let cropCancelBtn = id('crop-cancel'); -let cropperCloseBtn = id('cropper-close'); -let cropperArea = id('cropper-area'); - -/** - * Opens the image cropper with the selected file - * @param {File} file - The image file to crop - * @private - */ -async function openCropper(file: File): Promise { - // Clear previous cropper - cropperArea.innerHTML = ''; - - // Create new cropper - cropper = new ImageCropper(cropperArea); - - // Load image - await cropper.loadImage(file); - - // Open dialog - cropperDialog.open = true; -} - -/** - * Closes the image cropper and cleans up resources - * @private - */ -function closeCropper(): void { - cropperDialog.open = false; - cropperArea.innerHTML = ''; - if (cropper) { - cropper.destroy(); - cropper = null; - } - fileInput.value = ''; -} - -/** - * Saves the cropped image and uploads it to the server - * @private - */ -async function saveCroppedImage(): Promise { - if (!cropper) return; - - const croppedImageData = cropper.getCroppedImage(); - - // Convert data URL to blob - const response = await fetch(croppedImageData); - const blob = await response.blob(); - - const result = await uploadProfilePicture(blob); - - if (result) { - // Update profile picture display - const profilePicture = id('profile-picture'); - profilePicture.src = `${result.profile_picture_url}?t=${Date.now()}`; // Cache bust - - // Close cropper - closeCropper(); - - // Show success message - showSuccess('Фото профиля обновлено!'); - } else { - showError('Ошибка при загрузке фото'); - } -} - -/** - * Sets up event listeners for upload functionality - * @private - */ -function setupEventListeners(): void { - if (isInitialized) return; - - uploadBtn.addEventListener('click', () => { - fileInput.click(); - }); - - fileInput.addEventListener('change', (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; - if (file) { - openCropper(file); - } - }); - - cropSaveBtn.addEventListener('click', () => { - saveCroppedImage(); - }); - - cropCancelBtn.addEventListener('click', () => { - closeCropper(); - }); - - cropperCloseBtn.addEventListener('click', () => { - closeCropper(); - }); - - isInitialized = true; -} - -/** - * Loads and displays the user's profile picture - * @async - */ -export async function loadProfilePicture(): Promise { - const userData = await loadProfile(); - if (userData?.profile_picture) { - const url = `${userData.profile_picture}?t=${Date.now()}`; - - const profilePicture = id('profile-picture'); - const profilePicture2 = id("preview1"); - profilePicture.src = url; - profilePicture2.src = url; - } -} - -/** - * Initializes profile upload functionality - */ -export function initializeProfileUpload(): void { - setupEventListeners(); -} diff --git a/frontend/src/__userPanel/settings.ts b/frontend/src/__userPanel/settings.ts deleted file mode 100644 index bd58b30..0000000 --- a/frontend/src/__userPanel/settings.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * @fileoverview Settings dialog management and panel navigation - * @description Handles settings dialog functionality and dynamic panel switching - * @author Cursor - * @version 1.0.0 - */ - -import type { Dialog } from "mdui/components/dialog"; -import { id } from "../utils/utils"; - -const dialog = id('settings-dialog'); -const openButton = id('settings-open'); -const closeButton = id('settings-close'); - -// Settings panel management -const settingsList = document.querySelector('#settings-menu mdui-list')!; -const settingsPanels = document.querySelectorAll('.settings-panel'); - -/** - * Mapping between list item text and their corresponding panel IDs - */ -const panelMapping: {[x: string]: string} = { - 'Уведомления': 'notifications-settings', - 'Внешний вид': 'appearance-settings', - 'Безопасность': 'security-settings', - 'Язык': 'language-settings', - 'Хранилище': 'storage-settings', - 'Помощь': 'help-settings', - 'О приложении': 'about-settings' -}; - -/** - * Handles click events on settings list items - * @param {Element} item - The clicked list item element - * @private - */ -function handleListItemClick(item: Element): void { - // Remove active class from all items and panels - const listItems = settingsList.querySelectorAll('mdui-list-item'); - listItems.forEach(li => li.removeAttribute('active')); - settingsPanels.forEach(panel => panel.classList.remove('active')); - - // Add active class to clicked item - item.setAttribute('active', ''); - - // Show corresponding panel using the mapping - const itemText = item.textContent!.trim(); - const panelId = panelMapping[itemText]; - - if (panelId) { - const targetPanel = id(panelId); - if (targetPanel) { - targetPanel.classList.add('active'); - } - } -} - -/** - * Resets settings dialog to show the first panel - * @private - */ -function resetToFirstPanel(): void { - const firstItem = settingsList.querySelector('mdui-list-item'); - const firstPanel = document.querySelector('.settings-panel'); - - if (firstItem && firstPanel) { - settingsList.querySelectorAll('mdui-list-item').forEach(li => li.removeAttribute('active')); - settingsPanels.forEach(panel => panel.classList.remove('active')); - firstItem.setAttribute('active', ''); - firstPanel.classList.add('active'); - } -} - -/** - * Initializes settings. - * @private - */ -function init() { - // Set up settings navigation - settingsList.querySelectorAll('mdui-list-item').forEach((item) => { - item.addEventListener('click', () => handleListItemClick(item)); - }); - - // Set up dialog listeners - openButton.addEventListener('click', () => { - dialog.open = true; - resetToFirstPanel(); - }); - - closeButton.addEventListener('click', () => { - dialog.open = false; - }); -} - -init(); \ No newline at end of file diff --git a/frontend/src/__userPanel/userpanel.ts b/frontend/src/__userPanel/userpanel.ts deleted file mode 100644 index 537a2ec..0000000 --- a/frontend/src/__userPanel/userpanel.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * @fileoverview Left panel UI controls and interactions - * @description Handles chat collapse/expand, chat switching, and profile dialog - * @author Cursor - * @version 1.0.0 - */ - -import { Dialog } from "mdui/components/dialog"; -import { loadProfilePicture } from "./profile/upload"; -import { id } from "../utils/utils"; -import { publicChatPanel } from "../chat/chat.ts"; - -// сварачивание и разворачивание чата -const chatCollapseBtn = id('hide-chat')!; -const chat1 = id('chat-list-chat-1')!; -const chat2 = id('chat-list-chat-2')!; -const chatInner = id('chat-inner')!; -const chatContainer = document.querySelector('#chat-interface .chat-container') as HTMLElement; -const chatName = id('chat-name')!; -const profileButton = id('profile-open')!; -const dialog = id("profile-dialog"); -const dialogClose = id("profile-dialog-close")!; - -/** - * Sets up chat collapse functionality - * @private - */ -function setupChatCollapse(): void { - chatCollapseBtn.addEventListener('click', () => { - chatCollapseBtn.style.display = 'none'; - chatInner.style.display = 'none'; - }); -} - -/** - * Sets up chat switching functionality - * @private - */ -function animateChatSwitch(updateFn: () => void): Promise { - return new Promise((resolve, reject) => { - // Ensure panel is visible - chatCollapseBtn.style.display = 'flex'; - chatInner.style.display = 'flex'; - - // Start out animation - if (!chatContainer) { - reject("Chat container is missing"); - return; - } - chatContainer.classList.remove('chat-switch-in'); - chatContainer.classList.add('chat-switch-out'); - - const onOutEnd = () => { - chatContainer.removeEventListener('animationend', onOutEnd); - // Update content while hidden - updateFn(); - - // Then play in animation from the same offset - chatContainer.classList.remove('chat-switch-out'); - chatContainer.classList.add('chat-switch-in'); - - const onInEnd = () => { - chatContainer.removeEventListener("animationend", onInEnd); - resolve(); - } - - chatContainer.addEventListener("animationend", onInEnd); - }; - - chatContainer.addEventListener('animationend', onOutEnd); - }); -} - -function setupChatSwitching(): void { - chat1.addEventListener('click', () => { - animateChatSwitch(() => { - chatName.textContent = 'Общий чат'; - publicChatPanel.activate(); - }); - }); - - chat2.addEventListener('click', () => { - animateChatSwitch(() => { - chatName.textContent = 'Общий чат 2'; - publicChatPanel.activate(); - }); - }); -} - -/** - * Sets up profile dialog functionality - * @private - */ -function setupProfileDialog(): void { - profileButton.addEventListener('click', () => { - dialog.open = true; - loadProfilePicture(); - }); - - dialogClose.addEventListener("click", () => { - dialog.open = false; - }); -} - -setupChatCollapse(); -setupChatSwitching(); -setupProfileDialog(); \ No newline at end of file diff --git a/frontend/src/api/dmApi.ts b/frontend/src/api/dmApi.ts index 612d887..415de5a 100644 --- a/frontend/src/api/dmApi.ts +++ b/frontend/src/api/dmApi.ts @@ -1,10 +1,10 @@ import { API_BASE_URL } from "../core/config"; import { getAuthHeaders } from "../auth/api"; -import { ecdhSharedSecret, deriveWrappingKey } from "../crypto/asymmetric"; -import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../crypto/symmetric"; -import { randomBytes } from "../crypto/kdf"; +import { ecdhSharedSecret, deriveWrappingKey } from "../utils/crypto/asymmetric"; +import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../utils/crypto/symmetric"; +import { randomBytes } from "../utils/crypto/kdf"; import { getCurrentKeys } from "../auth/crypto"; -import { request } from "../websocket"; +import { request } from "../core/websocket"; import type { FetchDMResponse, SendDMRequest, DmEnvelope, User } from "../core/types"; import { b64, ub64 } from "../utils/utils"; diff --git a/frontend/src/ui/api/profileApi.ts b/frontend/src/api/profileApi.ts similarity index 95% rename from frontend/src/ui/api/profileApi.ts rename to frontend/src/api/profileApi.ts index 1939e93..a555821 100644 --- a/frontend/src/ui/api/profileApi.ts +++ b/frontend/src/api/profileApi.ts @@ -1,6 +1,6 @@ -import { getAuthHeaders } from "../../auth/api"; -import { API_BASE_URL } from "../../core/config"; -import type { UserProfile } from "../../core/types"; +import { getAuthHeaders } from "../auth/api"; +import { API_BASE_URL } from "../core/config"; +import type { UserProfile } from "../core/types"; export interface ProfileData { profile_picture?: string; diff --git a/frontend/src/auth/api.ts b/frontend/src/auth/api.ts index ffdeec4..875404d 100644 --- a/frontend/src/auth/api.ts +++ b/frontend/src/auth/api.ts @@ -1,4 +1,3 @@ -// import { showLogin } from "../navigation"; import type { Headers } from "../core/types"; /** @@ -17,32 +16,4 @@ export function getAuthHeaders(token: string | null, json: boolean = true): Head headers['Authorization'] = `Bearer ${token}`; } return headers; -} - -/** - * Checks authentication status on page load - */ -export async function checkAuthStatus(): Promise { - // For JWT, we don't have a persistent token on page load - // So we'll just show the login form - // showLogin(); -} - -/** - * Logs out the current user and clears session data - */ -// export async function logout(): Promise { -// try { -// await fetch(`${API_BASE_URL}/logout`, { -// method: 'GET', -// headers: getAuthHeaders() -// }); -// } catch (error) { -// console.error('Logout error:', error); -// } - -// currentUser = null; -// authToken = null; -// // showLogin(); -// clearAlerts(); -// } \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/src/auth/auth.ts b/frontend/src/auth/auth.ts deleted file mode 100644 index e37fa40..0000000 --- a/frontend/src/auth/auth.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @fileoverview Authentication system implementation - * @description Handles user authentication, registration, and session management - * @author Cursor - * @version 1.0.0 - */ - -import { id } from "../utils/utils"; - -/** - * Clears all alert messages from authentication forms - * @private - */ -export function clearAlerts(): void { - id('login-alerts').innerHTML = ''; - id('register-alerts').innerHTML = ''; -} \ No newline at end of file diff --git a/frontend/src/auth/crypto.ts b/frontend/src/auth/crypto.ts index 7b18e6c..f8e30e4 100644 --- a/frontend/src/auth/crypto.ts +++ b/frontend/src/auth/crypto.ts @@ -1,7 +1,7 @@ import { API_BASE_URL } from "../core/config"; import { getAuthHeaders } from "./api"; -import { generateX25519KeyPair } from "../crypto/asymmetric"; -import { encryptBackupWithPassword, decryptBackupWithPassword, encodeBlob, decodeBlob } from "../crypto/backup"; +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"; @@ -97,6 +97,4 @@ export async function ensureKeysOnLogin(password: string, token: string): Promis const encBlob = await encryptBackupWithPassword(password, { version: 1, privateKey: currentPrivateKey }); await uploadBackupBlob(encodeBlob(encBlob), token); return { publicKey: currentPublicKey, privateKey: currentPrivateKey }; -} - - +} \ No newline at end of file diff --git a/frontend/src/websocket.ts b/frontend/src/core/websocket.ts similarity index 95% rename from frontend/src/websocket.ts rename to frontend/src/core/websocket.ts index a6f0900..e062641 100644 --- a/frontend/src/websocket.ts +++ b/frontend/src/core/websocket.ts @@ -5,9 +5,9 @@ * @version 1.0.0 */ -import { API_WS_BASE_URL } from "./core/config"; -import type { WebSocketMessage } from "./core/types"; -import { delay } from "./utils/utils"; +import { API_WS_BASE_URL } from "./config"; +import type { WebSocketMessage } from "./types"; +import { delay } from "../utils/utils"; /** * Creates a new WebSocket connection to the chat server diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 24413a8..5dd0530 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -9,16 +9,7 @@ import './resources/css/style.scss'; import "mdui/mdui.css"; import "./utils/material"; -// import "./chat/chat"; -// import "./userPanel/settings"; -// import "./userPanel/userpanel"; -// import "./core/init"; -// import "./userPanel/profile/profile"; -// import "./chat/contextMenu"; -// import "./chat/profileDialog"; -// import "./electron/electron"; -// import "./chat/panel"; -// import "./chat/dm"; +import "./core/init"; import { createRoot } from 'react-dom/client'; import App from './ui/App'; import { StrictMode } from 'react'; diff --git a/frontend/src/ui/components/chat/ChatMessages.tsx b/frontend/src/ui/components/chat/ChatMessages.tsx index ddf2a3e..357a102 100644 --- a/frontend/src/ui/components/chat/ChatMessages.tsx +++ b/frontend/src/ui/components/chat/ChatMessages.tsx @@ -5,10 +5,10 @@ import type { Message as MessageType } from "../../../core/types"; import type { UserProfile } from "../../../core/types"; import { UserProfileDialog } from "./UserProfileDialog"; import { MessageContextMenu, type ContextMenuState } from "./MessageContextMenu"; -import { fetchUserProfile } from "../../api/profileApi"; +import { fetchUserProfile } from "../../../api/profileApi"; import { useState, type ReactNode } from "react"; import { delay } from "../../../utils/utils"; -import { request } from "../../../websocket"; +import { request } from "../../../core/websocket"; interface ChatMessagesProps { messages?: MessageType[]; diff --git a/frontend/src/ui/components/chat/DMPanel.tsx b/frontend/src/ui/components/chat/DMPanel.tsx index 54cd125..bb055b3 100644 --- a/frontend/src/ui/components/chat/DMPanel.tsx +++ b/frontend/src/ui/components/chat/DMPanel.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useRef } from "react"; import { useAppState } from "../../state"; -import { useDM } from "../../../hooks/useDM"; +import { useDM } from "../../hooks/useDM"; import { ChatMessages } from "./ChatMessages"; import defaultAvatar from "../../../resources/images/default-avatar.png"; diff --git a/frontend/src/ui/components/chat/DMUsersList.tsx b/frontend/src/ui/components/chat/DMUsersList.tsx index 28b8c8f..df01f1c 100644 --- a/frontend/src/ui/components/chat/DMUsersList.tsx +++ b/frontend/src/ui/components/chat/DMUsersList.tsx @@ -1,5 +1,5 @@ import { useEffect } from "react"; -import { useDM } from "../../../hooks/useDM"; +import { useDM } from "../../hooks/useDM"; import { useAppState } from "../../state"; import { fetchUserPublicKey } from "../../../api/dmApi"; import defaultAvatar from "../../../resources/images/default-avatar.png"; diff --git a/frontend/src/ui/components/chat/MessagePanelRenderer.tsx b/frontend/src/ui/components/chat/MessagePanelRenderer.tsx index 4c53a9b..e48114e 100644 --- a/frontend/src/ui/components/chat/MessagePanelRenderer.tsx +++ b/frontend/src/ui/components/chat/MessagePanelRenderer.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from "react"; import { MessagePanel, type MessagePanelState } from "../../panels/MessagePanel"; import { ChatMessages } from "./ChatMessages"; import { ChatInputWrapper } from "./ChatInputWrapper"; -import { setGlobalMessageHandler } from "../../../websocket"; +import { setGlobalMessageHandler } from "../../../core/websocket"; import defaultAvatar from "../../../resources/images/default-avatar.png"; interface MessagePanelRendererProps { diff --git a/frontend/src/ui/hooks/useChat.ts b/frontend/src/ui/hooks/useChat.ts index 510793a..45fa822 100644 --- a/frontend/src/ui/hooks/useChat.ts +++ b/frontend/src/ui/hooks/useChat.ts @@ -1,6 +1,6 @@ import { useEffect, useCallback, useRef } from "react"; import { useAppState } from "../state"; -import { request } from "../../websocket"; +import { request } from "../../core/websocket"; import { API_BASE_URL } from "../../core/config"; import type { Message } from "../../core/types"; import { getAuthHeaders } from "../../auth/api"; diff --git a/frontend/src/hooks/useDM.ts b/frontend/src/ui/hooks/useDM.ts similarity index 98% rename from frontend/src/hooks/useDM.ts rename to frontend/src/ui/hooks/useDM.ts index 5167d68..af7f5d7 100644 --- a/frontend/src/hooks/useDM.ts +++ b/frontend/src/ui/hooks/useDM.ts @@ -1,14 +1,14 @@ import { useState, useEffect, useCallback, useRef } from "react"; -import { useAppState } from "../ui/state"; +import { useAppState } from "../state"; import { fetchUsers, fetchUserPublicKey, fetchDMHistory, decryptDm, sendDMViaWebSocket -} from "../api/dmApi"; -import type { User, Message } from "../core/types"; -import { websocket } from "../websocket"; +} from "../../api/dmApi"; +import type { User, Message } from "../../core/types"; +import { websocket } from "../../core/websocket"; interface DMUser extends User { lastMessage?: string; diff --git a/frontend/src/ui/hooks/useProfile.ts b/frontend/src/ui/hooks/useProfile.ts index 4922d9d..aafb970 100644 --- a/frontend/src/ui/hooks/useProfile.ts +++ b/frontend/src/ui/hooks/useProfile.ts @@ -1,6 +1,6 @@ import { useState, useCallback, useEffect } from "react"; import { useAppState } from "../state"; -import { loadProfile, updateProfile, uploadProfilePicture, type ProfileData } from "../api/profileApi"; +import { loadProfile, updateProfile, uploadProfilePicture, type ProfileData } from "../../api/profileApi"; import { showSuccess, showError } from "../../utils/notification"; export function useProfile() { diff --git a/frontend/src/ui/panels/PublicChatPanel.ts b/frontend/src/ui/panels/PublicChatPanel.ts index ed4a78e..1722241 100644 --- a/frontend/src/ui/panels/PublicChatPanel.ts +++ b/frontend/src/ui/panels/PublicChatPanel.ts @@ -1,7 +1,7 @@ import { MessagePanel, type MessagePanelCallbacks } from "./MessagePanel"; import { API_BASE_URL } from "../../core/config"; import { getAuthHeaders } from "../../auth/api"; -import { request } from "../../websocket"; +import { request } from "../../core/websocket"; import type { Message, WebSocketMessage } from "../../core/types"; import type { UserState } from "../state"; diff --git a/frontend/src/ui/state.ts b/frontend/src/ui/state.ts index a88a3de..e5665f3 100644 --- a/frontend/src/ui/state.ts +++ b/frontend/src/ui/state.ts @@ -1,6 +1,6 @@ import { create } from "zustand"; import type { Message, User, WebSocketMessage } from "../core/types"; -import { request } from "../websocket"; +import { request } from "../core/websocket"; import { MessagePanel } from "./panels/MessagePanel"; import { PublicChatPanel } from "./panels/PublicChatPanel"; import { DMPanel, type DMPanelData } from "./panels/DMPanel"; diff --git a/frontend/src/crypto/asymmetric.ts b/frontend/src/utils/crypto/asymmetric.ts similarity index 100% rename from frontend/src/crypto/asymmetric.ts rename to frontend/src/utils/crypto/asymmetric.ts diff --git a/frontend/src/crypto/backup.ts b/frontend/src/utils/crypto/backup.ts similarity index 100% rename from frontend/src/crypto/backup.ts rename to frontend/src/utils/crypto/backup.ts diff --git a/frontend/src/crypto/kdf.ts b/frontend/src/utils/crypto/kdf.ts similarity index 100% rename from frontend/src/crypto/kdf.ts rename to frontend/src/utils/crypto/kdf.ts diff --git a/frontend/src/crypto/symmetric.ts b/frontend/src/utils/crypto/symmetric.ts similarity index 100% rename from frontend/src/crypto/symmetric.ts rename to frontend/src/utils/crypto/symmetric.ts diff --git a/frontend/src/utils/material.ts b/frontend/src/utils/material.ts index 5f220df..265d42b 100644 --- a/frontend/src/utils/material.ts +++ b/frontend/src/utils/material.ts @@ -19,9 +19,6 @@ import 'mdui/components/text-field'; import 'mdui/components/button-icon'; import 'mdui/components/top-app-bar'; import 'mdui/components/top-app-bar-title'; -import 'mdui/components/dropdown.js'; -import 'mdui/components/menu.js'; -import 'mdui/components/menu-item.js'; import { setColorScheme } from 'mdui/functions/setColorScheme.js';