Implement chat panels

This commit is contained in:
2025-09-05 22:36:24 +03:00
Unverified
parent 04d3e4d995
commit cad42e1584
11 changed files with 775 additions and 55 deletions
+179
View File
@@ -0,0 +1,179 @@
import { MessagePanel, type MessagePanelCallbacks } from "./MessagePanel";
import {
fetchUserPublicKey,
fetchDMHistory,
decryptDm,
sendDMViaWebSocket
} from "../../api/dmApi";
import type { Message, DmEnvelope } from "../../core/types";
import type { UserState } from "../state";
export interface DMPanelData {
userId: number;
username: string;
publicKey: string;
profilePicture?: string;
online: boolean;
}
export class DMPanel extends MessagePanel {
private dmData: DMPanelData | null = null;
private messagesLoaded: boolean = false;
constructor(
user: UserState,
callbacks: MessagePanelCallbacks,
onStateChange: (state: any) => void
) {
super("dm", user, callbacks, onStateChange);
}
async activate(): Promise<void> {
if (this.dmData && !this.messagesLoaded) {
await this.loadMessages();
}
}
deactivate(): void {
// DM doesn't need special cleanup
}
async loadMessages(): Promise<void> {
if (!this.currentUser.authToken || !this.dmData || this.messagesLoaded) return;
this.setLoading(true);
try {
const messages = await fetchDMHistory(this.dmData.userId, this.currentUser.authToken, 50);
const decryptedMessages: Message[] = [];
let maxIncomingId = 0;
for (const env of messages) {
try {
const text = await decryptDm(env, this.dmData!.publicKey);
const isAuthor = env.senderId !== this.dmData!.userId;
const username = isAuthor ? this.currentUser.currentUser?.username ?? "You" : this.dmData!.username;
decryptedMessages.push({
id: env.id,
content: text,
username: username,
timestamp: env.timestamp,
is_read: false,
is_edited: false
});
if (env.senderId === this.dmData!.userId && env.id > maxIncomingId) {
maxIncomingId = env.id;
}
} catch (error) {
console.error("Error decrypting message:", error);
}
}
this.clearMessages();
decryptedMessages.forEach(msg => this.addMessage(msg));
// Update last read ID
if (maxIncomingId > 0) {
this.setLastReadId(this.dmData.userId, maxIncomingId);
}
this.messagesLoaded = true;
} catch (error) {
console.error("Failed to load DM history:", error);
} finally {
this.setLoading(false);
}
}
async sendMessage(content: string): Promise<void> {
if (!this.currentUser.authToken || !this.dmData || !content.trim()) return;
try {
await sendDMViaWebSocket(
this.dmData.userId,
this.dmData.publicKey,
content,
this.currentUser.authToken
);
} catch (error) {
console.error("Failed to send DM:", error);
}
}
// Set DM conversation data
setDMData(dmData: DMPanelData): void {
this.dmData = dmData;
this.messagesLoaded = false;
this.updateState({
id: `dm-${dmData.userId}`,
title: dmData.username,
profilePicture: dmData.profilePicture,
online: dmData.online
});
}
// Handle incoming WebSocket DM messages
handleWebSocketMessage = async (response: any): Promise<void> => {
if (response.type === "dmNew" && this.dmData) {
const { senderId, recipientId, ...envelope } = response.data;
// If this is for the active DM conversation
if (senderId === this.dmData.userId || recipientId === this.dmData.userId) {
try {
const plaintext = await decryptDm(envelope, this.dmData.publicKey);
const isAuthor = senderId !== this.dmData.userId;
this.addMessage({
id: envelope.id,
content: plaintext,
username: isAuthor ? "You" : this.dmData.username,
timestamp: envelope.timestamp,
is_read: false,
is_edited: false
});
// Update last read if it's from the other user
if (senderId === this.dmData.userId) {
this.setLastReadId(this.dmData.userId, Math.max(this.getLastReadId(this.dmData.userId), envelope.id));
}
} catch (error) {
console.error("Failed to decrypt incoming DM:", error);
}
}
}
};
// Reset for DM switching
reset(): void {
this.dmData = null;
this.messagesLoaded = false;
this.clearMessages();
this.updateState({
id: "dm",
title: "Select a user",
profilePicture: undefined,
online: false
});
}
// Update auth token
setAuthToken(authToken: string): void {
this.currentUser.authToken = authToken;
}
// Helper functions for localStorage
private getLastReadId(userId: number): number {
try {
const v = localStorage.getItem(`dmLastRead:${userId}`);
return v ? Number(v) : 0;
} catch {
return 0;
}
}
private setLastReadId(userId: number, id: number): void {
try {
localStorage.setItem(`dmLastRead:${userId}`, String(id));
} catch {}
}
}