mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Implement live last message update
This commit is contained in:
@@ -16,6 +16,7 @@ When working with this project, follow these rules:
|
|||||||
- Use double quotes ("") for strings consistently.
|
- Use double quotes ("") for strings consistently.
|
||||||
- Prefer functional components over class components in React.
|
- Prefer functional components over class components in React.
|
||||||
- Use TypeScript strictly - avoid `any` types unless absolutely necessary.
|
- Use TypeScript strictly - avoid `any` types unless absolutely necessary.
|
||||||
|
- DO NOT leave placeholders - ask me when it would be better or implement it fully.
|
||||||
|
|
||||||
## File Operations
|
## File Operations
|
||||||
- If possible, try to update files in a single edit when making multiple changes.
|
- If possible, try to update files in a single edit when making multiple changes.
|
||||||
|
|||||||
@@ -835,6 +835,8 @@ class MessaggingSocketManager:
|
|||||||
"type": "dmEdited",
|
"type": "dmEdited",
|
||||||
"data": {
|
"data": {
|
||||||
"id": env.id,
|
"id": env.id,
|
||||||
|
"senderId": env.sender_id,
|
||||||
|
"recipientId": env.recipient_id,
|
||||||
"iv": env.iv_b64,
|
"iv": env.iv_b64,
|
||||||
"ciphertext": env.ciphertext_b64,
|
"ciphertext": env.ciphertext_b64,
|
||||||
"iv2": env.iv2_b64,
|
"iv2": env.iv2_b64,
|
||||||
|
|||||||
@@ -170,7 +170,13 @@ export async function deleteDmEnvelope(id: number, recipientId: number, authToke
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchDMConversations(token: string): Promise<any[]> {
|
export interface DMConversationResponse {
|
||||||
|
user: User;
|
||||||
|
lastMessage: DmEnvelope;
|
||||||
|
unreadCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDMConversations(token: string): Promise<DMConversationResponse[]> {
|
||||||
const res = await fetch(`${API_BASE_URL}/dm/conversations`, {
|
const res = await fetch(`${API_BASE_URL}/dm/conversations`, {
|
||||||
headers: getAuthHeaders(token, true)
|
headers: getAuthHeaders(token, true)
|
||||||
});
|
});
|
||||||
|
|||||||
Vendored
+3
@@ -305,11 +305,14 @@ export interface Attachment {
|
|||||||
// Utils
|
// Utils
|
||||||
export interface DMEditPayload {
|
export interface DMEditPayload {
|
||||||
id: number;
|
id: number;
|
||||||
|
senderId: number;
|
||||||
|
recipientId: number;
|
||||||
iv: string;
|
iv: string;
|
||||||
ciphertext: string;
|
ciphertext: string;
|
||||||
iv2: string;
|
iv2: string;
|
||||||
wrappedMk: string;
|
wrappedMk: string;
|
||||||
salt: string;
|
salt: string;
|
||||||
|
timestamp: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Requests
|
// Requests
|
||||||
|
|||||||
@@ -239,3 +239,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Description styling for list items
|
||||||
|
.list-description {
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import {
|
|||||||
fetchDMHistory,
|
fetchDMHistory,
|
||||||
decryptDm,
|
decryptDm,
|
||||||
sendDMViaWebSocket,
|
sendDMViaWebSocket,
|
||||||
fetchDMConversations
|
fetchDMConversations,
|
||||||
|
type DMConversationResponse
|
||||||
} from "@/core/api/dmApi";
|
} from "@/core/api/dmApi";
|
||||||
import type { User, Message, DmEncryptedJSON } from "@/core/types";
|
import type { User, Message, DmEncryptedJSON } from "@/core/types";
|
||||||
import { websocket } from "@/core/websocket";
|
import { websocket } from "@/core/websocket";
|
||||||
@@ -16,8 +17,34 @@ export interface DMUser extends User {
|
|||||||
publicKey?: string | null;
|
publicKey?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Utility function for consistent username formatting in DM messages
|
||||||
|
export function formatDMUsername(
|
||||||
|
senderId: number,
|
||||||
|
_recipientId: number,
|
||||||
|
currentUserId: number,
|
||||||
|
otherUsername: string
|
||||||
|
): string {
|
||||||
|
const isFromCurrentUser = senderId === currentUserId;
|
||||||
|
return isFromCurrentUser ? "Вы" : otherUsername;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility function for consistent message content formatting
|
||||||
|
export function formatDMMessageContent(
|
||||||
|
content: string,
|
||||||
|
senderId: number,
|
||||||
|
currentUserId: number
|
||||||
|
): string {
|
||||||
|
const isFromCurrentUser = senderId === currentUserId;
|
||||||
|
const prefix = isFromCurrentUser ? "Вы: " : "";
|
||||||
|
const maxContentLength = 50 - prefix.length;
|
||||||
|
const truncatedContent = content.length > maxContentLength
|
||||||
|
? content.substring(0, maxContentLength) + "..."
|
||||||
|
: content;
|
||||||
|
return prefix + truncatedContent;
|
||||||
|
}
|
||||||
|
|
||||||
export function useDM() {
|
export function useDM() {
|
||||||
const { user, chat, setDmUsers, setActiveDm, addMessage, clearMessages } = useAppState();
|
const { user, setDmUsers, setActiveDm, addMessage, clearMessages } = useAppState();
|
||||||
const [dmUsers, setDmUsersState] = useState<DMUser[]>([]);
|
const [dmUsers, setDmUsersState] = useState<DMUser[]>([]);
|
||||||
const [isLoadingUsers, setIsLoadingUsers] = useState(false);
|
const [isLoadingUsers, setIsLoadingUsers] = useState(false);
|
||||||
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
|
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
|
||||||
@@ -80,17 +107,42 @@ export function useDM() {
|
|||||||
setIsLoadingUsers(true);
|
setIsLoadingUsers(true);
|
||||||
try {
|
try {
|
||||||
const conversations = await fetchDMConversations(user.authToken);
|
const conversations = await fetchDMConversations(user.authToken);
|
||||||
console.log("Fetched conversations:", conversations);
|
|
||||||
|
|
||||||
const dmUsersWithState: DMUser[] = conversations.map((conv: any) => ({
|
// Process conversations and decrypt last messages
|
||||||
|
const dmUsersWithState: DMUser[] = await Promise.all(
|
||||||
|
conversations.map(async (conv: DMConversationResponse) => {
|
||||||
|
let lastMessageContent: string | undefined = undefined;
|
||||||
|
|
||||||
|
if (conv.lastMessage) {
|
||||||
|
try {
|
||||||
|
// Get the public key for the other user
|
||||||
|
const otherUserId = conv.lastMessage.senderId === user.currentUser?.id
|
||||||
|
? conv.lastMessage.recipientId
|
||||||
|
: conv.lastMessage.senderId;
|
||||||
|
|
||||||
|
const publicKey = await fetchUserPublicKey(otherUserId, user.authToken!);
|
||||||
|
if (publicKey) {
|
||||||
|
// Decrypt the last message
|
||||||
|
const decryptedJson = await decryptDm(conv.lastMessage, publicKey!);
|
||||||
|
const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON;
|
||||||
|
lastMessageContent = formatDMMessageContent(decryptedData.data.content, conv.lastMessage.senderId, user.currentUser?.id!);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to decrypt last message for user", conv.user.id, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
...conv.user,
|
...conv.user,
|
||||||
unreadCount: conv.unreadCount,
|
unreadCount: conv.unreadCount,
|
||||||
lastMessage: conv.lastMessage ? "Последнее сообщение" : undefined,
|
lastMessage: lastMessageContent,
|
||||||
publicKey: null
|
publicKey: null
|
||||||
}));
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
setDmUsersState(dmUsersWithState);
|
setDmUsersState(dmUsersWithState);
|
||||||
setDmUsers(conversations.map((conv: any) => conv.user));
|
setDmUsers(conversations.map((conv: DMConversationResponse) => conv.user));
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to load DM conversations:", error);
|
console.error("Failed to load DM conversations:", error);
|
||||||
@@ -192,7 +244,67 @@ export function useDM() {
|
|||||||
}
|
}
|
||||||
}, [user.authToken, setActiveDm, loadDMHistory]);
|
}, [user.authToken, setActiveDm, loadDMHistory]);
|
||||||
|
|
||||||
// WebSocket message handler
|
// Force reload users (useful for refreshing the list)
|
||||||
|
const reloadUsers = useCallback(() => {
|
||||||
|
usersLoadedRef.current = false;
|
||||||
|
loadUsers();
|
||||||
|
}, [loadUsers]);
|
||||||
|
|
||||||
|
// Reload a specific user's conversation data
|
||||||
|
const reloadUserConversation = useCallback(async (userId: number) => {
|
||||||
|
if (!user.authToken) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const conversations = await fetchDMConversations(user.authToken);
|
||||||
|
const userConversation = conversations.find(conv => conv.user.id === userId);
|
||||||
|
|
||||||
|
if (userConversation) {
|
||||||
|
let lastMessageContent: string | undefined = undefined;
|
||||||
|
|
||||||
|
if (userConversation.lastMessage) {
|
||||||
|
try {
|
||||||
|
// Get the public key for the other user
|
||||||
|
const otherUserId = userConversation.lastMessage.senderId === user.currentUser?.id
|
||||||
|
? userConversation.lastMessage.recipientId
|
||||||
|
: userConversation.lastMessage.senderId;
|
||||||
|
|
||||||
|
const publicKey = await fetchUserPublicKey(otherUserId, user.authToken!);
|
||||||
|
if (publicKey) {
|
||||||
|
// Decrypt the last message
|
||||||
|
const decryptedJson = await decryptDm(userConversation.lastMessage, publicKey!);
|
||||||
|
const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON;
|
||||||
|
lastMessageContent = formatDMMessageContent(decryptedData.data.content, userConversation.lastMessage.senderId, user.currentUser?.id!);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to decrypt last message for user", userId, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the specific user in the state
|
||||||
|
setDmUsersState(prev => prev.map(u => {
|
||||||
|
if (u.id === userId) {
|
||||||
|
return {
|
||||||
|
...u,
|
||||||
|
lastMessage: lastMessageContent,
|
||||||
|
unreadCount: userConversation.unreadCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return u;
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// If conversation no longer exists, remove the user from the list
|
||||||
|
setDmUsersState(prev => prev.filter(u => u.id !== userId));
|
||||||
|
// Get current dmUsers and filter out the removed user
|
||||||
|
const currentDmUsers = useAppState.getState().chat.dmUsers;
|
||||||
|
setDmUsers(currentDmUsers.filter((u: User) => u.id !== userId));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to reload user conversation:", error);
|
||||||
|
}
|
||||||
|
}, [user.authToken]);
|
||||||
|
|
||||||
|
|
||||||
|
// WebSocket message handler for conversation list updates
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function handleWebSocketMessage(e: MessageEvent) {
|
async function handleWebSocketMessage(e: MessageEvent) {
|
||||||
try {
|
try {
|
||||||
@@ -200,47 +312,27 @@ export function useDM() {
|
|||||||
if (msg.type === "dmNew") {
|
if (msg.type === "dmNew") {
|
||||||
const { senderId, recipientId, ...envelope } = msg.data;
|
const { senderId, recipientId, ...envelope } = msg.data;
|
||||||
|
|
||||||
// If this is for the active DM conversation
|
// Update conversation list (not active conversation - that's handled by DMPanel)
|
||||||
if (chat.activeDm && (senderId === chat.activeDm.userId || recipientId === chat.activeDm.userId)) {
|
if (!user.currentUser?.id) {
|
||||||
try {
|
return;
|
||||||
const plaintext = await decryptDm(envelope, chat.activeDm.publicKey!);
|
|
||||||
const isAuthor = senderId !== chat.activeDm.userId;
|
|
||||||
|
|
||||||
addMessage({
|
|
||||||
id: envelope.id,
|
|
||||||
content: plaintext,
|
|
||||||
username: isAuthor ? (user.currentUser?.username || "Unknown") : (chat.activeDm.username || "Unknown"),
|
|
||||||
timestamp: envelope.timestamp,
|
|
||||||
is_read: false,
|
|
||||||
is_edited: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update last read if it's from the other user
|
|
||||||
if (senderId === chat.activeDm.userId) {
|
|
||||||
setLastReadId(chat.activeDm.userId, Math.max(getLastReadId(chat.activeDm.userId), envelope.id));
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
const otherUserId = senderId === user.currentUser.id ? recipientId : senderId;
|
||||||
console.error("Failed to decrypt incoming DM:", error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Update unread count for other users
|
|
||||||
const otherUserId = senderId;
|
|
||||||
setDmUsersState(prev => prev.map(u =>
|
|
||||||
u.id === otherUserId
|
|
||||||
? { ...u, unreadCount: u.unreadCount + 1 }
|
|
||||||
: u
|
|
||||||
));
|
|
||||||
|
|
||||||
// Update last message preview
|
// Update unread count and last message preview
|
||||||
try {
|
try {
|
||||||
const publicKey = await fetchUserPublicKey(otherUserId, user.authToken!);
|
const publicKey = await fetchUserPublicKey(otherUserId, user.authToken!);
|
||||||
if (publicKey) {
|
if (publicKey) {
|
||||||
const plaintext = await decryptDm(envelope, publicKey);
|
const decryptedJson = await decryptDm(envelope, publicKey);
|
||||||
|
const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON;
|
||||||
|
const messageContent = decryptedData.data.content;
|
||||||
|
const formattedMessage = formatDMMessageContent(messageContent, senderId, user.currentUser.id);
|
||||||
|
|
||||||
setDmUsersState(prev => prev.map(u =>
|
setDmUsersState(prev => prev.map(u =>
|
||||||
u.id === otherUserId
|
u.id === otherUserId
|
||||||
? {
|
? {
|
||||||
...u,
|
...u,
|
||||||
lastMessage: plaintext.split(/\r?\n/).slice(0, 2).join("\n"),
|
unreadCount: senderId !== user.currentUser?.id ? u.unreadCount + 1 : u.unreadCount,
|
||||||
|
lastMessage: formattedMessage,
|
||||||
publicKey
|
publicKey
|
||||||
}
|
}
|
||||||
: u
|
: u
|
||||||
@@ -249,7 +341,41 @@ export function useDM() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to update last message preview:", error);
|
console.error("Failed to update last message preview:", error);
|
||||||
}
|
}
|
||||||
|
} else if (msg.type === "dmEdited") {
|
||||||
|
const { id, senderId, recipientId, ...envelope } = msg.data;
|
||||||
|
|
||||||
|
// Update last message preview for conversation list
|
||||||
|
if (!user.currentUser?.id) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
const otherUserId = senderId === user.currentUser.id ? recipientId : senderId;
|
||||||
|
try {
|
||||||
|
const publicKey = await fetchUserPublicKey(otherUserId, user.authToken!);
|
||||||
|
if (publicKey) {
|
||||||
|
const decryptedJson = await decryptDm(envelope, publicKey);
|
||||||
|
const decryptedData = JSON.parse(decryptedJson) as DmEncryptedJSON;
|
||||||
|
const messageContent = decryptedData.data.content;
|
||||||
|
const formattedMessage = formatDMMessageContent(messageContent, senderId, user.currentUser.id);
|
||||||
|
setDmUsersState(prev => prev.map(u =>
|
||||||
|
u.id === otherUserId
|
||||||
|
? {
|
||||||
|
...u,
|
||||||
|
lastMessage: formattedMessage,
|
||||||
|
publicKey
|
||||||
|
}
|
||||||
|
: u
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update edited message preview:", error);
|
||||||
|
}
|
||||||
|
} else if (msg.type === "dmDeleted") {
|
||||||
|
const { senderId, recipientId } = msg.data;
|
||||||
|
|
||||||
|
// Reload only the specific user's conversation
|
||||||
|
if (!user.currentUser?.id) return;
|
||||||
|
const otherUserId = senderId === user.currentUser.id ? recipientId : senderId;
|
||||||
|
reloadUserConversation(otherUserId);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to handle WebSocket message:", error);
|
console.error("Failed to handle WebSocket message:", error);
|
||||||
@@ -259,13 +385,7 @@ export function useDM() {
|
|||||||
websocket.addEventListener("message", handleWebSocketMessage);
|
websocket.addEventListener("message", handleWebSocketMessage);
|
||||||
|
|
||||||
return () => websocket.removeEventListener("message", handleWebSocketMessage);
|
return () => websocket.removeEventListener("message", handleWebSocketMessage);
|
||||||
}, [chat.activeDm, user.currentUser, addMessage]);
|
}, [user.currentUser, user.authToken, reloadUserConversation]);
|
||||||
|
|
||||||
// Force reload users (useful for refreshing the list)
|
|
||||||
const reloadUsers = useCallback(() => {
|
|
||||||
usersLoadedRef.current = false;
|
|
||||||
loadUsers();
|
|
||||||
}, [loadUsers]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dmUsers,
|
dmUsers,
|
||||||
@@ -273,6 +393,7 @@ export function useDM() {
|
|||||||
isLoadingHistory,
|
isLoadingHistory,
|
||||||
loadUsers,
|
loadUsers,
|
||||||
reloadUsers,
|
reloadUsers,
|
||||||
|
reloadUserConversation,
|
||||||
startDMConversation,
|
startDMConversation,
|
||||||
sendDMMessage,
|
sendDMMessage,
|
||||||
loadUserLastMessage
|
loadUserLastMessage
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
import { useAppState } from "@/pages/chat/state";
|
import { useAppState, type ChatTabs } from "@/pages/chat/state";
|
||||||
|
import { UnifiedChatsList } from "./UnifiedChatsList";
|
||||||
|
import type { FormEvent } from "react";
|
||||||
|
import type { Tabs } from "mdui/components/tabs";
|
||||||
|
|
||||||
export function ChatTabs() {
|
export function ChatTabs() {
|
||||||
const { chat, setActiveTab, switchToPublicChat } = useAppState();
|
const { chat, setActiveTab } = useAppState();
|
||||||
|
|
||||||
|
function handleChange(e: FormEvent<Tabs> & CustomEvent<{ value: string }>) {
|
||||||
|
setActiveTab(e.detail.value as ChatTabs);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-tabs">
|
<div className="chat-tabs">
|
||||||
<mdui-tabs value={chat.activeTab} full-width onChange={(e: any) => setActiveTab(e.detail.value)}>
|
<mdui-tabs
|
||||||
|
value={chat.activeTab}
|
||||||
|
full-width
|
||||||
|
onChange={handleChange}>
|
||||||
<mdui-tab value="chats">
|
<mdui-tab value="chats">
|
||||||
Чаты
|
Чаты
|
||||||
</mdui-tab>
|
</mdui-tab>
|
||||||
@@ -15,37 +25,12 @@ export function ChatTabs() {
|
|||||||
<mdui-tab value="contacts">
|
<mdui-tab value="contacts">
|
||||||
Контакты
|
Контакты
|
||||||
</mdui-tab>
|
</mdui-tab>
|
||||||
<mdui-tab value="dms">
|
|
||||||
ЛС
|
|
||||||
</mdui-tab>
|
|
||||||
|
|
||||||
<mdui-tab-panel slot="panel" value="chats">
|
<mdui-tab-panel slot="panel" value="chats">
|
||||||
<mdui-list>
|
<UnifiedChatsList />
|
||||||
<mdui-list-item
|
|
||||||
headline="Общий чат"
|
|
||||||
description="Вы: Последнее сообщение"
|
|
||||||
id="chat-list-chat-1"
|
|
||||||
onClick={async () => await switchToPublicChat("Общий чат")}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
<img src="./src/resources/images/default-avatar.png" alt="" slot="icon" />
|
|
||||||
</mdui-list-item>
|
|
||||||
<mdui-list-item
|
|
||||||
headline="Общий чат 2"
|
|
||||||
description="Вы: Последнее сообщение"
|
|
||||||
id="chat-list-chat-2"
|
|
||||||
onClick={async () => await switchToPublicChat("Общий чат 2")}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
<img src="./src/resources/images/default-avatar.png" alt="" slot="icon" />
|
|
||||||
</mdui-list-item>
|
|
||||||
</mdui-list>
|
|
||||||
</mdui-tab-panel>
|
</mdui-tab-panel>
|
||||||
<mdui-tab-panel slot="panel" value="channels">Скоро будет...</mdui-tab-panel>
|
<mdui-tab-panel slot="panel" value="channels">Скоро будет...</mdui-tab-panel>
|
||||||
<mdui-tab-panel slot="panel" value="contacts">Скоро будет...</mdui-tab-panel>
|
<mdui-tab-panel slot="panel" value="contacts">Скоро будет...</mdui-tab-panel>
|
||||||
<mdui-tab-panel slot="panel" value="dms">
|
|
||||||
<mdui-list id="dm-users"></mdui-list>
|
|
||||||
</mdui-tab-panel>
|
|
||||||
</mdui-tabs>
|
</mdui-tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
import { useEffect } from "react";
|
|
||||||
import { useDM, type DMUser } from "@/pages/chat/hooks/useDM";
|
|
||||||
import { useAppState } from "@/pages/chat/state";
|
|
||||||
import { fetchUserPublicKey } from "@/core/api/dmApi";
|
|
||||||
import defaultAvatar from "@/images/default-avatar.png";
|
|
||||||
|
|
||||||
export function DMUsersList() {
|
|
||||||
const { dmUsers, isLoadingUsers, loadUsers } = useDM();
|
|
||||||
const { chat, switchToDM } = useAppState();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (chat.activeTab === "chats") {
|
|
||||||
loadUsers();
|
|
||||||
}
|
|
||||||
}, [chat.activeTab, loadUsers]);
|
|
||||||
|
|
||||||
if (isLoadingUsers) {
|
|
||||||
return (
|
|
||||||
<mdui-circular-progress />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleUserClick(user: DMUser) {
|
|
||||||
if (!user.publicKey) {
|
|
||||||
// Get public key if not already loaded
|
|
||||||
const authToken = useAppState.getState().user.authToken;
|
|
||||||
if (!authToken) return;
|
|
||||||
|
|
||||||
const publicKey = await fetchUserPublicKey(user.id, authToken);
|
|
||||||
if (publicKey) {
|
|
||||||
user.publicKey = publicKey;
|
|
||||||
} else {
|
|
||||||
console.error("Failed to get public key for user:", user.id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await switchToDM({
|
|
||||||
userId: user.id,
|
|
||||||
username: user.username,
|
|
||||||
publicKey: user.publicKey,
|
|
||||||
profilePicture: user.profile_picture,
|
|
||||||
online: user.online || false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<mdui-list>
|
|
||||||
{dmUsers.map((user: DMUser) => (
|
|
||||||
<mdui-list-item
|
|
||||||
key={user.id}
|
|
||||||
headline={user.username}
|
|
||||||
description={user.lastMessage || "Нет сообщений"}
|
|
||||||
onClick={() => handleUserClick(user)}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={user.profile_picture || defaultAvatar}
|
|
||||||
alt={user.username}
|
|
||||||
slot="icon"
|
|
||||||
style={{
|
|
||||||
width: "40px",
|
|
||||||
height: "40px",
|
|
||||||
borderRadius: "50%",
|
|
||||||
objectFit: "cover"
|
|
||||||
}}
|
|
||||||
onError={(e) => {
|
|
||||||
(e.target as HTMLImageElement).src = defaultAvatar;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{user.unreadCount > 0 && (
|
|
||||||
<mdui-badge slot="end-icon">
|
|
||||||
{user.unreadCount}
|
|
||||||
</mdui-badge>
|
|
||||||
)}
|
|
||||||
</mdui-list-item>
|
|
||||||
))}
|
|
||||||
</mdui-list>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
import { PRODUCT_NAME } from "@/core/config";
|
|
||||||
import { useAppState } from "@/pages/chat/state";
|
import { useAppState } from "@/pages/chat/state";
|
||||||
import defaultAvatar from "@/images/default-avatar.png";
|
import { useState } from "react";
|
||||||
import { useState, type FormEvent } from "react";
|
|
||||||
import { ProfileDialog } from "./profile/ProfileDialog";
|
|
||||||
import { SettingsDialog } from "./settings/SettingsDialog";
|
import { SettingsDialog } from "./settings/SettingsDialog";
|
||||||
import { DMUsersList } from "./DMUsersList";
|
|
||||||
import { UsernameSearch } from "./UsernameSearch";
|
import { UsernameSearch } from "./UsernameSearch";
|
||||||
import type { Tabs } from "mdui";
|
import { ChatTabs } from "./ChatTabs";
|
||||||
import type { ChatTabs } from "@/pages/chat/state";
|
import { ChatHeader } from "./ChatHeader";
|
||||||
|
|
||||||
function BottomAppBar() {
|
function BottomAppBar() {
|
||||||
const [settingsOpen, onSettingsOpenChange] = useState(false);
|
const [settingsOpen, onSettingsOpenChange] = useState(false);
|
||||||
@@ -36,75 +32,6 @@ function BottomAppBar() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function ChatTabs() {
|
|
||||||
const { chat, setActiveTab, switchToPublicChat } = useAppState();
|
|
||||||
const { activeTab } = chat;
|
|
||||||
|
|
||||||
async function handleChatClick(chatName: string) {
|
|
||||||
await switchToPublicChat(chatName);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleTabChange(e: FormEvent<Tabs>) {
|
|
||||||
setActiveTab((e.target as Tabs).value as ChatTabs);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="chat-tabs">
|
|
||||||
<mdui-tabs value={activeTab} full-width onChange={handleTabChange}>
|
|
||||||
<mdui-tab value="chats">Чаты</mdui-tab>
|
|
||||||
<mdui-tab value="channels">Каналы</mdui-tab>
|
|
||||||
<mdui-tab value="contacts">Контакты</mdui-tab>
|
|
||||||
|
|
||||||
<mdui-tab-panel slot="panel" value="chats">
|
|
||||||
<mdui-list>
|
|
||||||
<mdui-list-item
|
|
||||||
headline="Общий чат"
|
|
||||||
description="Вы: Последнее сообщение"
|
|
||||||
id="chat-list-chat-1"
|
|
||||||
onClick={() => handleChatClick("Общий чат")}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
<img src={defaultAvatar} alt="" slot="icon" />
|
|
||||||
</mdui-list-item>
|
|
||||||
<mdui-list-item
|
|
||||||
headline="Общий чат 2"
|
|
||||||
description="Вы: Последнее сообщение"
|
|
||||||
id="chat-list-chat-2"
|
|
||||||
onClick={() => handleChatClick("Общий чат 2")}
|
|
||||||
style={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
<img src={defaultAvatar} alt="" slot="icon" />
|
|
||||||
</mdui-list-item>
|
|
||||||
|
|
||||||
{/* DM conversations will be loaded here */}
|
|
||||||
<DMUsersList />
|
|
||||||
</mdui-list>
|
|
||||||
</mdui-tab-panel>
|
|
||||||
<mdui-tab-panel slot="panel" value="channels">Скоро будет...</mdui-tab-panel>
|
|
||||||
<mdui-tab-panel slot="panel" value="contacts">Скоро будет...</mdui-tab-panel>
|
|
||||||
</mdui-tabs>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function ChatHeader() {
|
|
||||||
const [isProfileOpen, setProfileOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header className="chat-header-left">
|
|
||||||
<div className="product-name">{PRODUCT_NAME}</div>
|
|
||||||
<div className="profile">
|
|
||||||
<a href="#" id="profile-open" onClick={() => setProfileOpen(true)}>
|
|
||||||
<img src={defaultAvatar} alt="" id="preview1" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<ProfileDialog isOpen={isProfileOpen} onOpenChange={setProfileOpen} />
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function LeftPanel() {
|
export function LeftPanel() {
|
||||||
return (
|
return (
|
||||||
<div className="chat-list" id="chat-list">
|
<div className="chat-list" id="chat-list">
|
||||||
|
|||||||
@@ -0,0 +1,272 @@
|
|||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import { useAppState } from "@/pages/chat/state";
|
||||||
|
import { useDM, type DMUser } from "@/pages/chat/hooks/useDM";
|
||||||
|
import { API_BASE_URL } from "@/core/config";
|
||||||
|
import { getAuthHeaders } from "@/core/api/authApi";
|
||||||
|
import { fetchUserPublicKey } from "@/core/api/dmApi";
|
||||||
|
import type { Message } from "@/core/types";
|
||||||
|
import { websocket } from "@/core/websocket";
|
||||||
|
import defaultAvatar from "@/images/default-avatar.png";
|
||||||
|
|
||||||
|
interface PublicChat {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: "public";
|
||||||
|
lastMessage?: Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DMConversation {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
profile_picture?: string;
|
||||||
|
online?: boolean;
|
||||||
|
type: "dm";
|
||||||
|
lastMessage?: string;
|
||||||
|
unreadCount: number;
|
||||||
|
publicKey?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChatItem = PublicChat | DMConversation;
|
||||||
|
|
||||||
|
export function UnifiedChatsList() {
|
||||||
|
const { user, switchToPublicChat, switchToDM, chat } = useAppState();
|
||||||
|
const { dmUsers, isLoadingUsers, loadUsers } = useDM();
|
||||||
|
|
||||||
|
const [publicChats] = useState<PublicChat[]>([
|
||||||
|
{ id: "general", name: "Общий чат", type: "public" },
|
||||||
|
{ id: "general2", name: "Общий чат 2", type: "public" }
|
||||||
|
]);
|
||||||
|
const [lastMessages, setLastMessages] = useState<Record<string, Message | undefined>>({});
|
||||||
|
const [allChats, setAllChats] = useState<ChatItem[]>([]);
|
||||||
|
|
||||||
|
// Load public chat last messages
|
||||||
|
const loadLastMessages = useCallback(async () => {
|
||||||
|
if (!user.authToken) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/get_messages`, {
|
||||||
|
headers: getAuthHeaders(user.authToken)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.messages && data.messages.length > 0) {
|
||||||
|
const lastMessage = data.messages[data.messages.length - 1];
|
||||||
|
|
||||||
|
setLastMessages({
|
||||||
|
general: lastMessage,
|
||||||
|
general2: lastMessage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading last messages:", error);
|
||||||
|
}
|
||||||
|
}, [user.authToken]);
|
||||||
|
|
||||||
|
// Load DM users when chats tab is active
|
||||||
|
useEffect(() => {
|
||||||
|
if (chat.activeTab === "chats") {
|
||||||
|
loadUsers();
|
||||||
|
loadLastMessages();
|
||||||
|
}
|
||||||
|
}, [chat.activeTab, loadUsers, loadLastMessages]);
|
||||||
|
|
||||||
|
// Combine public chats and DMs into one list
|
||||||
|
useEffect(() => {
|
||||||
|
const publicChatItems: ChatItem[] = publicChats.map(chat => ({
|
||||||
|
...chat,
|
||||||
|
lastMessage: lastMessages[chat.id]
|
||||||
|
}));
|
||||||
|
|
||||||
|
const dmChatItems: ChatItem[] = dmUsers.map((user: DMUser) => ({
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
profile_picture: user.profile_picture,
|
||||||
|
online: user.online,
|
||||||
|
type: "dm" as const,
|
||||||
|
lastMessage: user.lastMessage,
|
||||||
|
unreadCount: user.unreadCount,
|
||||||
|
publicKey: user.publicKey
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Combine and sort by last message timestamp (DMs first, then public chats)
|
||||||
|
const combined = [...dmChatItems, ...publicChatItems];
|
||||||
|
setAllChats(combined);
|
||||||
|
}, [publicChats, lastMessages, dmUsers]);
|
||||||
|
|
||||||
|
// WebSocket listener for public chat message updates
|
||||||
|
useEffect(() => {
|
||||||
|
if (!websocket) return;
|
||||||
|
|
||||||
|
const handleWebSocketMessage = (e: MessageEvent) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(e.data);
|
||||||
|
|
||||||
|
if (msg.type === "newMessage") {
|
||||||
|
const newMessage = msg.data as Message;
|
||||||
|
// Update all public chats with the new message
|
||||||
|
setLastMessages(prev => {
|
||||||
|
const updated = { ...prev };
|
||||||
|
publicChats.forEach(chat => {
|
||||||
|
updated[chat.id] = newMessage;
|
||||||
|
});
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
} else if (msg.type === "messageEdited") {
|
||||||
|
const editedMessage = msg.data as Message;
|
||||||
|
// Update only if the edited message is the current last message
|
||||||
|
setLastMessages(prev => {
|
||||||
|
const updated = { ...prev };
|
||||||
|
publicChats.forEach(chat => {
|
||||||
|
if (updated[chat.id]?.id === editedMessage.id) {
|
||||||
|
updated[chat.id] = editedMessage;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
} else if (msg.type === "messageDeleted") {
|
||||||
|
const deletedMessageId = msg.data?.message_id;
|
||||||
|
let needsReload = false;
|
||||||
|
|
||||||
|
setLastMessages(prev => {
|
||||||
|
const updated = { ...prev };
|
||||||
|
publicChats.forEach(chat => {
|
||||||
|
if (updated[chat.id]?.id === deletedMessageId) {
|
||||||
|
updated[chat.id] = undefined;
|
||||||
|
needsReload = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (needsReload) {
|
||||||
|
loadLastMessages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to handle WebSocket message in UnifiedChatsList:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.addEventListener("message", handleWebSocketMessage);
|
||||||
|
return () => websocket.removeEventListener("message", handleWebSocketMessage);
|
||||||
|
}, [publicChats, loadLastMessages]);
|
||||||
|
|
||||||
|
const formatPublicChatMessage = (chatId: string): string => {
|
||||||
|
const lastMessage = lastMessages[chatId];
|
||||||
|
if (!lastMessage) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCurrentUser = lastMessage.username === user.currentUser?.username;
|
||||||
|
const prefix = isCurrentUser ? "Вы: " : `${lastMessage.username}: `;
|
||||||
|
|
||||||
|
const maxContentLength = 50 - prefix.length;
|
||||||
|
const content = lastMessage.content.length > maxContentLength
|
||||||
|
? lastMessage.content.substring(0, maxContentLength) + "..."
|
||||||
|
: lastMessage.content;
|
||||||
|
|
||||||
|
return prefix + content;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const handlePublicChatClick = async (chatName: string) => {
|
||||||
|
await switchToPublicChat(chatName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDMClick = async (dmConversation: DMConversation) => {
|
||||||
|
if (!dmConversation.publicKey) {
|
||||||
|
const authToken = useAppState.getState().user.authToken;
|
||||||
|
if (!authToken) return;
|
||||||
|
|
||||||
|
const publicKey = await fetchUserPublicKey(dmConversation.id, authToken);
|
||||||
|
if (publicKey) {
|
||||||
|
dmConversation.publicKey = publicKey;
|
||||||
|
} else {
|
||||||
|
console.error("Failed to get public key for user:", dmConversation.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await switchToDM({
|
||||||
|
userId: dmConversation.id,
|
||||||
|
username: dmConversation.username,
|
||||||
|
publicKey: dmConversation.publicKey,
|
||||||
|
profilePicture: dmConversation.profile_picture,
|
||||||
|
online: dmConversation.online || false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoadingUsers) {
|
||||||
|
return (
|
||||||
|
<mdui-circular-progress />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<mdui-list>
|
||||||
|
{allChats.map((chat) => {
|
||||||
|
if (chat.type === "public") {
|
||||||
|
return (
|
||||||
|
<mdui-list-item
|
||||||
|
key={`public-${chat.id}`}
|
||||||
|
headline={chat.name}
|
||||||
|
onClick={() => handlePublicChatClick(chat.name)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{formatPublicChatMessage(chat.id) && (
|
||||||
|
<span slot="description" className="list-description">
|
||||||
|
{formatPublicChatMessage(chat.id)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<img
|
||||||
|
src={defaultAvatar}
|
||||||
|
alt={chat.name}
|
||||||
|
slot="icon"
|
||||||
|
style={{
|
||||||
|
width: "40px",
|
||||||
|
height: "40px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
objectFit: "cover"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</mdui-list-item>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<mdui-list-item
|
||||||
|
key={`dm-${chat.id}`}
|
||||||
|
headline={chat.username}
|
||||||
|
onClick={() => handleDMClick(chat)}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
<span slot="description" className="list-description">
|
||||||
|
{chat.lastMessage || "Нет сообщений"}
|
||||||
|
</span>
|
||||||
|
<img
|
||||||
|
src={chat.profile_picture || defaultAvatar}
|
||||||
|
alt={chat.username}
|
||||||
|
slot="icon"
|
||||||
|
style={{
|
||||||
|
width: "40px",
|
||||||
|
height: "40px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
objectFit: "cover"
|
||||||
|
}}
|
||||||
|
onError={(e) => {
|
||||||
|
(e.target as HTMLImageElement).src = defaultAvatar;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{chat.unreadCount > 0 && (
|
||||||
|
<mdui-badge slot="end-icon">
|
||||||
|
{chat.unreadCount}
|
||||||
|
</mdui-badge>
|
||||||
|
)}
|
||||||
|
</mdui-list-item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</mdui-list>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -130,10 +130,12 @@ export function UsernameSearch() {
|
|||||||
<mdui-list-item
|
<mdui-list-item
|
||||||
key={searchUser.id}
|
key={searchUser.id}
|
||||||
headline={searchUser.username}
|
headline={searchUser.username}
|
||||||
description={searchUser.online ? "В сети" : "Не в сети"}
|
|
||||||
onClick={() => handleUserClick(searchUser)}
|
onClick={() => handleUserClick(searchUser)}
|
||||||
style={{ cursor: "pointer" }}
|
style={{ cursor: "pointer" }}
|
||||||
>
|
>
|
||||||
|
<span slot="description" className="list-description">
|
||||||
|
{searchUser.online ? "В сети" : "Не в сети"}
|
||||||
|
</span>
|
||||||
<img
|
<img
|
||||||
src={searchUser.profile_picture || defaultAvatar}
|
src={searchUser.profile_picture || defaultAvatar}
|
||||||
alt={searchUser.username}
|
alt={searchUser.username}
|
||||||
|
|||||||
@@ -154,7 +154,10 @@ export function ChatMessages({ messages = [], children, isDm = false, onReplySel
|
|||||||
<Message
|
<Message
|
||||||
key={message.id}
|
key={message.id}
|
||||||
message={message}
|
message={message}
|
||||||
isAuthor={message.username === user.currentUser?.username}
|
isAuthor={isDm ?
|
||||||
|
(message.runtimeData?.dmEnvelope?.senderId === user.currentUser?.id) :
|
||||||
|
(message.username === user.currentUser?.username)
|
||||||
|
}
|
||||||
onProfileClick={handleProfileClick}
|
onProfileClick={handleProfileClick}
|
||||||
onContextMenu={handleContextMenu}
|
onContextMenu={handleContextMenu}
|
||||||
onReactionClick={handleReactionClick}
|
onReactionClick={handleReactionClick}
|
||||||
@@ -189,7 +192,10 @@ export function ChatMessages({ messages = [], children, isDm = false, onReplySel
|
|||||||
{contextMenu.message && (
|
{contextMenu.message && (
|
||||||
<MessageContextMenu
|
<MessageContextMenu
|
||||||
message={contextMenu.message}
|
message={contextMenu.message}
|
||||||
isAuthor={contextMenu.message.username === user.currentUser?.username}
|
isAuthor={isDm ?
|
||||||
|
(contextMenu.message.runtimeData?.dmEnvelope?.senderId === user.currentUser?.id) :
|
||||||
|
(contextMenu.message.username === user.currentUser?.username)
|
||||||
|
}
|
||||||
onEdit={handleEdit}
|
onEdit={handleEdit}
|
||||||
onReply={handleReply}
|
onReply={handleReply}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from "@/core/api/dmApi";
|
} from "@/core/api/dmApi";
|
||||||
import type { DmEncryptedJSON, DmEnvelope, DMWebSocketMessage, EncryptedMessageJson, Message } from "@/core/types";
|
import type { DmEncryptedJSON, DmEnvelope, DMWebSocketMessage, EncryptedMessageJson, Message } from "@/core/types";
|
||||||
import type { UserState } from "@/pages/chat/state";
|
import type { UserState } from "@/pages/chat/state";
|
||||||
|
import { formatDMUsername } from "@/pages/chat/hooks/useDM";
|
||||||
|
|
||||||
export interface DMPanelData {
|
export interface DMPanelData {
|
||||||
userId: number;
|
userId: number;
|
||||||
@@ -48,8 +49,12 @@ export class DMPanel extends MessagePanel {
|
|||||||
|
|
||||||
private async parseTextPayload(env: DmEnvelope, decryptedMessages: Message[]) {
|
private async parseTextPayload(env: DmEnvelope, decryptedMessages: Message[]) {
|
||||||
const plaintext = await decryptDm(env, this.dmData!.publicKey);
|
const plaintext = await decryptDm(env, this.dmData!.publicKey);
|
||||||
const isAuthor = env.senderId !== this.dmData!.userId;
|
const username = formatDMUsername(
|
||||||
const username = isAuthor ? this.currentUser.currentUser?.username ?? "You" : this.dmData!.username;
|
env.senderId,
|
||||||
|
env.recipientId,
|
||||||
|
this.currentUser.currentUser?.id!,
|
||||||
|
this.dmData!.username
|
||||||
|
);
|
||||||
|
|
||||||
// Try parse JSON payload { type: "text", data: { content, files?, reply_to_id? } }
|
// Try parse JSON payload { type: "text", data: { content, files?, reply_to_id? } }
|
||||||
let content = plaintext;
|
let content = plaintext;
|
||||||
@@ -168,6 +173,7 @@ export class DMPanel extends MessagePanel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Handle incoming WebSocket DM messages
|
// Handle incoming WebSocket DM messages
|
||||||
async handleWebSocketMessage(response: DMWebSocketMessage): Promise<void> {
|
async handleWebSocketMessage(response: DMWebSocketMessage): Promise<void> {
|
||||||
if (response.type === "dmNew" && this.dmData) {
|
if (response.type === "dmNew" && this.dmData) {
|
||||||
|
|||||||
Reference in New Issue
Block a user