Fix the UI

This commit is contained in:
2025-08-25 17:11:56 +03:00
Unverified
parent 22f7eb7a62
commit eda9de699e
5 changed files with 149 additions and 51 deletions
+7 -6
View File
@@ -13,6 +13,8 @@ import { show as showContextMenu } from "./contextMenu";
import { show as showUserProfileDialog } from "./profileDialog"; import { show as showUserProfileDialog } from "./profileDialog";
import defaultAvatar from "../resources/images/default-avatar.png"; import defaultAvatar from "../resources/images/default-avatar.png";
import { authToken, currentUser, getAuthHeaders } from "../auth/api"; 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 * Adds a new message to the chat interface
@@ -142,6 +144,7 @@ export function loadMessages(): void {
// Добавляем только новые сообщения // Добавляем только новые сообщения
data.messages.forEach(msg => { data.messages.forEach(msg => {
console.log(msg);
if (msg.id > lastMessageId) { if (msg.id > lastMessageId) {
addMessage(msg, msg.username == currentUser!.username); 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 * Updates an existing message in the chat interface
* @param {Message} message - Updated message object * @param {Message} message - Updated message object
@@ -249,3 +246,7 @@ export function handleWebSocketMessage(response: WebSocketMessage): void {
break; break;
} }
} }
export const publicChatPanel = new PublicChatPanel();
publicChatPanel.activate();
+26 -42
View File
@@ -1,9 +1,11 @@
import { API_BASE_URL } from "../core/config"; import { API_BASE_URL } from "../core/config";
import { getAuthHeaders } from "../auth/api"; import { getAuthHeaders } from "../auth/api";
import { DmPanel, ChatPanelController } from "./panel";
import { ecdhSharedSecret, deriveWrappingKey } from "../crypto/asymmetric"; import { ecdhSharedSecret, deriveWrappingKey } from "../crypto/asymmetric";
import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../crypto/symmetric"; import { importAesGcmKey, aesGcmEncrypt, aesGcmDecrypt } from "../crypto/symmetric";
import { randomBytes } from "../crypto/kdf"; import { randomBytes } from "../crypto/kdf";
import { getCurrentKeys } from "../auth/crypto"; import { getCurrentKeys } from "../auth/crypto";
import type { Tabs } from "mdui/components/tabs";
function b64(a: Uint8Array): string { return btoa(String.fromCharCode(...a)); } function b64(a: Uint8Array): string { return btoa(String.fromCharCode(...a)); }
function ub64(s: string): Uint8Array { function ub64(s: string): Uint8Array {
@@ -69,25 +71,9 @@ export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string
return new TextDecoder().decode(msg); 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 activeDm: { userId: number; username: string; publicKey: string | null } | null = null;
let usersLoaded = false; let usersLoaded = false;
let dmPanel: DmPanel | null = null;
async function loadUsers() { async function loadUsers() {
const res = await fetch(`${API_BASE_URL}/users`, { headers: getAuthHeaders(true) }); const res = await fetch(`${API_BASE_URL}/users`, { headers: getAuthHeaders(true) });
@@ -100,8 +86,21 @@ async function loadUsers() {
item.setAttribute("headline", u.username); item.setAttribute("headline", u.username);
item.addEventListener("click", async () => { item.addEventListener("click", async () => {
activeDm = { userId: u.id, username: u.username, publicKey: null }; activeDm = { userId: u.id, username: u.username, publicKey: null };
document.getElementById("chat-name")!.textContent = u.username; if (!dmPanel) {
(document.getElementById("chat-messages") as HTMLElement).innerHTML = ""; 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) }); const resPk = await fetch(`${API_BASE_URL}/crypto/public-key/of/${u.id}`, { headers: getAuthHeaders(true) });
if (resPk.ok) { if (resPk.ok) {
const pkData = await resPk.json(); const pkData = await resPk.json();
@@ -112,36 +111,21 @@ async function loadUsers() {
}); });
} }
document.addEventListener("DOMContentLoaded", () => { function init() {
const tabs = document.querySelector(".chat-tabs mdui-tabs"); const tabs = document.querySelector(".chat-tabs mdui-tabs") as Tabs;
const dmTab = tabs?.querySelector('mdui-tab[value="dms"]'); const dmTab = tabs?.querySelector('mdui-tab[value="dms"]')!;
function ensureUsersLoaded() { function ensureUsersLoaded() {
if (!usersLoaded) { if (!usersLoaded) {
usersLoaded = true; usersLoaded = true;
loadUsers(); loadUsers();
} }
} }
dmTab?.addEventListener("click", ensureUsersLoaded); dmTab.addEventListener("click", ensureUsersLoaded);
(tabs as any)?.addEventListener("change", (e: any) => { tabs.addEventListener("change", (e: any) => {
if (e?.detail?.value === "dms") ensureUsersLoaded(); 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();
+109
View File
@@ -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<void>;
protected abstract loadMessages(): void | Promise<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<void> {
const payload: WebSocketMessage = {
data: { content: text },
credentials: { scheme: "Bearer", credentials: authToken! },
type: "sendMessage"
};
await new Promise<void>((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<void>;
private loader: () => Promise<void> | void;
constructor(sender: (text: string) => Promise<void>, loader: () => Promise<void> | void) {
super();
this.sender = sender;
this.loader = loader;
}
protected async onSubmit(text: string): Promise<void> {
this.appendSimple(text, true);
await this.sender(text);
}
protected loadMessages(): void | Promise<void> {
return this.loader();
}
}
ChatPanelController.mountOnce();
+1
View File
@@ -17,4 +17,5 @@ import "./userPanel/profile/profile";
import "./chat/contextMenu"; import "./chat/contextMenu";
import "./chat/profileDialog"; import "./chat/profileDialog";
import "./electron/electron"; import "./electron/electron";
import "./chat/panel";
import "./chat/dm"; import "./chat/dm";
+3
View File
@@ -8,6 +8,7 @@
import { Dialog } from "mdui/components/dialog"; import { Dialog } from "mdui/components/dialog";
import { loadProfilePicture } from "./profile/upload"; import { loadProfilePicture } from "./profile/upload";
import { id } from "../utils/utils"; import { id } from "../utils/utils";
import { publicChatPanel } from "../chat/chat";
// сварачивание и разворачивание чата // сварачивание и разворачивание чата
const chatCollapseBtn = id('hide-chat')!; const chatCollapseBtn = id('hide-chat')!;
@@ -74,12 +75,14 @@ function setupChatSwitching(): void {
chat1.addEventListener('click', () => { chat1.addEventListener('click', () => {
animateChatSwitch(() => { animateChatSwitch(() => {
chatName.textContent = 'Общий чат'; chatName.textContent = 'Общий чат';
publicChatPanel.activate();
}); });
}); });
chat2.addEventListener('click', () => { chat2.addEventListener('click', () => {
animateChatSwitch(() => { animateChatSwitch(() => {
chatName.textContent = 'Общий чат 2'; chatName.textContent = 'Общий чат 2';
publicChatPanel.activate();
}); });
}); });
} }