diff --git a/frontend/src/chat/chat.ts b/frontend/src/chat/chat.ts index 120328d..a7c92c9 100644 --- a/frontend/src/chat/chat.ts +++ b/frontend/src/chat/chat.ts @@ -13,6 +13,8 @@ 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"; +import type { Tabs } from "mdui/components/tabs"; /** * Adds a new message to the chat interface @@ -142,6 +144,7 @@ export function loadMessages(): void { // Добавляем только новые сообщения data.messages.forEach(msg => { + console.log(msg); if (msg.id > lastMessageId) { addMessage(msg, msg.username == currentUser!.username); } @@ -184,12 +187,6 @@ export function sendMessage(): void { } } - -document.getElementById('message-form')!.addEventListener('submit', (e) => { - e.preventDefault(); - sendMessage(); -}); - /** * Updates an existing message in the chat interface * @param {Message} message - Updated message object @@ -248,4 +245,8 @@ export function handleWebSocketMessage(response: WebSocketMessage): void { } break; } -} \ No newline at end of file +} + +export const publicChatPanel = new PublicChatPanel(); + +publicChatPanel.activate(); \ No newline at end of file diff --git a/frontend/src/chat/dm.ts b/frontend/src/chat/dm.ts index a13e4b6..4b8dd96 100644 --- a/frontend/src/chat/dm.ts +++ b/frontend/src/chat/dm.ts @@ -1,9 +1,11 @@ import { API_BASE_URL } from "../core/config"; import { getAuthHeaders } from "../auth/api"; +import { DmPanel, ChatPanelController } 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 type { Tabs } from "mdui/components/tabs"; function b64(a: Uint8Array): string { return btoa(String.fromCharCode(...a)); } function ub64(s: string): Uint8Array { @@ -69,25 +71,9 @@ export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string return new TextDecoder().decode(msg); } -function appendDmMessage(text: string, isAuthor: boolean) { - const container = document.getElementById("chat-messages")!; - 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); - container.appendChild(div); - container.scrollTop = container.scrollHeight; -} - -// removed unused helper - let activeDm: { userId: number; username: string; publicKey: string | null } | null = null; let usersLoaded = false; +let dmPanel: DmPanel | null = null; async function loadUsers() { const res = await fetch(`${API_BASE_URL}/users`, { headers: getAuthHeaders(true) }); @@ -100,8 +86,21 @@ async function loadUsers() { item.setAttribute("headline", u.username); item.addEventListener("click", async () => { activeDm = { userId: u.id, username: u.username, publicKey: null }; - document.getElementById("chat-name")!.textContent = u.username; - (document.getElementById("chat-messages") as HTMLElement).innerHTML = ""; + if (!dmPanel) { + dmPanel = new DmPanel( + async (text: string) => { + if (activeDm?.publicKey) { + try { await sendDm(activeDm.userId, activeDm.publicKey, text); } catch {} + } + }, + () => { + // For v1, no DM history yet; just clear + } + ); + } + dmPanel.setTitle(u.username); + dmPanel.clearMessages(); + dmPanel.activate(); const resPk = await fetch(`${API_BASE_URL}/crypto/public-key/of/${u.id}`, { headers: getAuthHeaders(true) }); if (resPk.ok) { const pkData = await resPk.json(); @@ -112,36 +111,21 @@ async function loadUsers() { }); } -document.addEventListener("DOMContentLoaded", () => { - const tabs = document.querySelector(".chat-tabs mdui-tabs"); - const dmTab = tabs?.querySelector('mdui-tab[value="dms"]'); +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 as any)?.addEventListener("change", (e: any) => { - if (e?.detail?.value === "dms") ensureUsersLoaded(); + dmTab.addEventListener("click", ensureUsersLoaded); + tabs.addEventListener("change", (e: any) => { + if (e.detail?.value === "dms") { + ensureUsersLoaded(); + } }); - const form = document.getElementById("message-form"); - if (form) { - form.addEventListener("submit", async (e) => { - if (!activeDm) return; // Let global chat handler proceed - e.preventDefault(); - const input = document.getElementById("message-input") as HTMLInputElement; - const text = input.value.trim(); - if (!text) return; - appendDmMessage(text, true); - input.value = ""; - if (activeDm.publicKey) { - try { - await sendDm(activeDm.userId, activeDm.publicKey, text); - } catch {} - } - }); - } -}); - +} +init(); \ No newline at end of file diff --git a/frontend/src/chat/panel.ts b/frontend/src/chat/panel.ts new file mode 100644 index 0000000..6abd786 --- /dev/null +++ b/frontend/src/chat/panel.ts @@ -0,0 +1,109 @@ +import { authToken, currentUser } from "../auth/api"; +import type { WebSocketMessage } from "../core/types"; +import { websocket } from "../websocket"; +import { loadMessages } from "./chat"; + +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; + + 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 new Promise((resolve) => { + let callback: ((e: MessageEvent) => void) | null = null; + callback = (e) => { + websocket.removeEventListener("message", callback!); + resolve(); + }; + websocket.addEventListener("message", callback); + websocket.send(JSON.stringify(payload)); + }); + } + + protected loadMessages(): void { + return loadMessages(); + } +} + +export class DmPanel extends ChatPanelController { + private sender: (text: string) => Promise; + private loader: () => Promise | void; + constructor(sender: (text: string) => Promise, loader: () => Promise | void) { + super(); + this.sender = sender; + this.loader = loader; + } + protected async onSubmit(text: string): Promise { + this.appendSimple(text, true); + await this.sender(text); + } + protected loadMessages(): void | Promise { + return this.loader(); + } +} + +ChatPanelController.mountOnce(); \ No newline at end of file diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 408fbb1..b3c1090 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -17,4 +17,5 @@ import "./userPanel/profile/profile"; import "./chat/contextMenu"; import "./chat/profileDialog"; import "./electron/electron"; +import "./chat/panel"; import "./chat/dm"; \ No newline at end of file diff --git a/frontend/src/userPanel/userpanel.ts b/frontend/src/userPanel/userpanel.ts index 8ecec8a..1ec52bd 100644 --- a/frontend/src/userPanel/userpanel.ts +++ b/frontend/src/userPanel/userpanel.ts @@ -8,6 +8,7 @@ import { Dialog } from "mdui/components/dialog"; import { loadProfilePicture } from "./profile/upload"; import { id } from "../utils/utils"; +import { publicChatPanel } from "../chat/chat"; // сварачивание и разворачивание чата const chatCollapseBtn = id('hide-chat')!; @@ -74,12 +75,14 @@ function setupChatSwitching(): void { chat1.addEventListener('click', () => { animateChatSwitch(() => { chatName.textContent = 'Общий чат'; + publicChatPanel.activate(); }); }); chat2.addEventListener('click', () => { animateChatSwitch(() => { chatName.textContent = 'Общий чат 2'; + publicChatPanel.activate(); }); }); }