mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Fix the UI
This commit is contained in:
@@ -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
|
||||||
@@ -248,4 +245,8 @@ export function handleWebSocketMessage(response: WebSocketMessage): void {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const publicChatPanel = new PublicChatPanel();
|
||||||
|
|
||||||
|
publicChatPanel.activate();
|
||||||
+28
-44
@@ -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();
|
||||||
@@ -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();
|
||||||
@@ -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";
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user