Fix DMs displaying

This commit is contained in:
2025-08-26 18:09:55 +03:00
Unverified
parent bf55005d38
commit 1fb01a4404
7 changed files with 176 additions and 76 deletions
+1
View File
@@ -6,3 +6,4 @@ When you work with UI:
1. Use MDUI components as HTML elements
2. Do NOT dynamically create HTML if it's going to be loaded when the page loads, instead put it statically in the HTML.
3. The supporting text slot for MDUI lists is "description".
+2 -2
View File
@@ -418,8 +418,8 @@
</div>
</div>
<div class="profile-actions">
<mdui-button id="dm-button" variant="filled" style="display: none;">
<span class="material-symbols">chat</span>
<mdui-button id="dm-button" variant="filled">
<mdui-icon slot="icon" name="chat--filled"></mdui-icon>
Send Message
</mdui-button>
</div>
+2 -2
View File
@@ -8,7 +8,7 @@
import { initializeProfile } from "../userPanel/profile/profile";
import type { ErrorResponse, LoginResponse, LoginRequest, RegisterRequest } from "../core/types";
import { API_BASE_URL } from "../core/config";
import { loadChat, showLogin, showRegister } from "../navigation";
import { showChat, showLogin, showRegister } from "../navigation";
import { setUser } from "./api";
import { ensureKeysOnLogin } from "./crypto";
import { id } from "../utils/utils";
@@ -77,7 +77,7 @@ async function handleLogin(e: Event): Promise<void> {
} catch (e) {
console.error("Key setup failed:", e);
}
loadChat();
showChat();
initializeProfile(); // Initialize profile after login
} else {
const data: ErrorResponse = await response.json();
-29
View File
@@ -123,35 +123,6 @@ export function addMessage(message: Message, isAuthor: boolean): void {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
/**
* Loads chat messages from the server
*/
export function loadMessages(): void {
fetch(`${API_BASE_URL}/get_messages`, {
headers: getAuthHeaders()
})
.then(response => response.json())
.then((data: Messages) => {
if (data.messages && data.messages.length > 0) {
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);
}
});
}
});
}
/**
* Sends a message via WebSocket
*/
+128 -18
View File
@@ -81,6 +81,23 @@ export async function decryptDm(envelope: DmEnvelope, senderPublicKeyB64: string
let activeDm: { userId: number; username: string; publicKey: string | null } | null = null;
let usersLoaded = false;
let dmPanel: DmPanel | null = null;
const dmBadgeByUserId: Map<number, HTMLElement> = new Map();
const dmSupportingTextByUserId: Map<number, HTMLElement> = 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) });
@@ -90,6 +107,7 @@ async function loadUsers() {
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");
@@ -111,19 +129,28 @@ async function loadUsers() {
// Set headline (username)
item.setAttribute("headline", u.username);
// Add last message placeholder (will be loaded lazily)
// Add supporting text container (hidden until loaded)
const lastMessageEl = document.createElement("div");
lastMessageEl.slot = "supporting-text";
lastMessageEl.textContent = "Loading...";
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, lastMessageEl);
loadLastMessage(u.id);
observer.unobserve(entry.target);
}
});
@@ -181,9 +208,11 @@ async function loadUsers() {
const messages: DmEnvelope[] = data.messages || [];
const container = document.getElementById("chat-messages")!;
container.innerHTML = "";
let maxIncomingId = 0;
for (const env of messages) {
try {
const text = await decryptDm(env, activeDm.publicKey);
// 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({
@@ -194,9 +223,20 @@ async function loadUsers() {
is_read: false,
is_edited: false
});
if (env.senderId === activeDm.userId && env.id > maxIncomingId) {
maxIncomingId = env.id;
}
} catch {}
}
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 = "";
}
}
}
}
);
@@ -204,7 +244,6 @@ async function loadUsers() {
dmPanel.setOtherUser(u.username);
dmPanel.setTitle(u.username);
dmPanel.clearMessages();
dmPanel.activate();
// Add profile click functionality to chat header
const chatHeaderAvatar = document.querySelector('.chat-header-avatar') as HTMLElement;
@@ -218,32 +257,76 @@ async function loadUsers() {
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, element: HTMLElement): Promise<void> {
async function loadLastMessage(userId: number): Promise<void> {
try {
const response = await fetch(`${API_BASE_URL}/dm/history/${userId}?limit=1`, {
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 || [];
if (messages.length > 0) {
const lastMessage = messages[messages.length - 1];
// For now, just show "Last message" since we can't decrypt without the public key
// In a real implementation, you'd need to store the public key or decrypt here
element.textContent = "Last message";
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 {
element.textContent = "No messages yet";
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 {
element.textContent = "No messages yet";
const supporting = dmSupportingTextByUserId.get(userId);
if (supporting) {
supporting.textContent = "";
supporting.style.display = "none";
}
}
} catch (error) {
element.textContent = "No messages yet";
const supporting = dmSupportingTextByUserId.get(userId);
if (supporting) {
supporting.textContent = "";
supporting.style.display = "none";
}
}
}
@@ -271,9 +354,10 @@ init();
websocket.addEventListener("message", async (e) => {
try {
const msg: WebSocketMessage = JSON.parse((e as MessageEvent).data);
if (msg.type === "dmNew" && activeDm && (msg.data.senderId === activeDm.userId || msg.data.recipientId === activeDm.userId)) {
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({
@@ -285,6 +369,32 @@ websocket.addEventListener("message", async (e) => {
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 {}
});
+29 -4
View File
@@ -1,7 +1,8 @@
import { authToken, currentUser } from "../auth/api";
import type { Message, WebSocketMessage } from "../core/types";
import { authToken, currentUser, getAuthHeaders } from "../auth/api";
import { API_BASE_URL } from "../core/config";
import type { Message, Messages, WebSocketMessage } from "../core/types";
import { request } from "../websocket";
import { loadMessages } from "./chat";
import { addMessage } from "./chat";
import { show as showContextMenu } from "./contextMenu";
import { show as showProfileDialog } from "./profileDialog";
@@ -80,7 +81,31 @@ export class PublicChatPanel extends ChatPanelController {
}
protected loadMessages(): void {
return loadMessages();
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 {
+2 -9
View File
@@ -1,5 +1,5 @@
import { clearAlerts } from "./auth/auth";
import { loadMessages } from "./chat/chat";
import { publicChatPanel } from "./chat/chat";
import { id } from "./utils/utils";
const loginForm = id("login-form");
@@ -36,14 +36,7 @@ export function showChat(): void {
loginForm.style.display = 'none';
registerForm.style.display = 'none';
chatInterface.style.display = 'block';
loadMessages();
titleBar.classList.remove("color-surface");
}
/**
* Loads the chat interface and initializes messaging.
*/
export function loadChat(): void {
showChat();
loadMessages();
publicChatPanel.activate();
}