Simplify chats list

This commit is contained in:
2025-11-07 16:58:39 +03:00
Unverified
parent af530abf6c
commit 370b57f2c9
4 changed files with 107 additions and 215 deletions
@@ -74,45 +74,11 @@
} }
} }
.chatTabs { .unifiedChatsList {
margin-top: 5px;
width: 100%;
height: calc(100% - 80px);
display: flex;
flex-direction: column;
min-height: 0; // prevent flex collapse when inner overflows
--mdui-color-surface: $color-dark-surface-container;
--mdui-color-surface-variant: transparent;
img {
width: 45px;
height: 45px;
border-radius: 20%;
object-fit: cover;
}
mdui-tabs {
height: 100%;
display: flex;
flex-direction: column;
min-height: 0; // enable inner panel to scroll
}
mdui-tab-panel[active] {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0; // critical to avoid collapsing
overflow: auto;
}
mdui-list {
flex: 1; flex: 1;
min-height: 0; // allow scroll area to size correctly min-height: 0; // allow scroll area to size correctly
overflow-y: auto; overflow-y: auto;
padding: 0; margin-top: 10px;
margin: 0;
}
} }
// Search container // Search container
@@ -1,33 +0,0 @@
import { useAppState, type ChatTabs } from "@/pages/chat/state";
import { UnifiedChatsList } from "./UnifiedChatsList";
import { MaterialTab, MaterialTabPanel, MaterialTabs } from "@/utils/material";
import styles from "@/pages/chat/css/left-panel.module.scss";
export function ChatTabs() {
const { chat, setActiveTab } = useAppState();
return (
<div className={styles.chatTabs}>
<MaterialTabs
value={chat.activeTab}
full-width
onChange={(e) => setActiveTab(e.target.value as ChatTabs)}>
<MaterialTab value="chats">
Чаты
</MaterialTab>
<MaterialTab value="channels">
Каналы
</MaterialTab>
<MaterialTab value="contacts">
Контакты
</MaterialTab>
<MaterialTabPanel slot="panel" value="chats">
<UnifiedChatsList />
</MaterialTabPanel>
<MaterialTabPanel slot="panel" value="channels">Скоро будет...</MaterialTabPanel>
<MaterialTabPanel slot="panel" value="contacts">Скоро будет...</MaterialTabPanel>
</MaterialTabs>
</div>
);
}
@@ -2,7 +2,7 @@ import { useAppState } from "@/pages/chat/state";
import { useState } from "react"; import { useState } from "react";
import { SettingsDialog } from "./settings/SettingsDialog"; import { SettingsDialog } from "./settings/SettingsDialog";
import { UsernameSearch } from "./UsernameSearch"; import { UsernameSearch } from "./UsernameSearch";
import { ChatTabs } from "./ChatTabs"; import { UnifiedChatsList } from "./UnifiedChatsList";
import { ChatHeader } from "./ChatHeader"; import { ChatHeader } from "./ChatHeader";
import { MaterialBottomAppBar, MaterialFab, MaterialIconButton } from "@/utils/material"; import { MaterialBottomAppBar, MaterialFab, MaterialIconButton } from "@/utils/material";
import styles from "@/pages/chat/css/left-panel.module.scss"; import styles from "@/pages/chat/css/left-panel.module.scss";
@@ -35,7 +35,7 @@ export function LeftPanel() {
<div className={styles.searchContainer}> <div className={styles.searchContainer}>
<UsernameSearch /> <UsernameSearch />
</div> </div>
<ChatTabs /> <UnifiedChatsList />
<BottomAppBar /> <BottomAppBar />
</div> </div>
); );
@@ -1,4 +1,4 @@
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback, useMemo } from "react";
import { useAppState } from "@/pages/chat/state"; import { useAppState } from "@/pages/chat/state";
import { useDM, type DMUser } from "@/pages/chat/hooks/useDM"; import { useDM, type DMUser } from "@/pages/chat/hooks/useDM";
import { API_BASE_URL } from "@/core/config"; import { API_BASE_URL } from "@/core/config";
@@ -36,18 +36,17 @@ interface DMConversation {
type ChatItem = PublicChat | DMConversation; type ChatItem = PublicChat | DMConversation;
const PUBLIC_CHAT: PublicChat = {
id: "general",
name: "Общий чат",
type: "public"
};
export function UnifiedChatsList() { export function UnifiedChatsList() {
const { user, switchToPublicChat, switchToDM, chat } = useAppState(); const { user, switchToPublicChat, switchToDM, chat } = useAppState();
const { dmUsers, isLoadingUsers, loadUsers } = useDM(); 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 [lastMessages, setLastMessages] = useState<Record<string, Message | undefined>>({});
const [allChats, setAllChats] = useState<ChatItem[]>([]);
// Load public chat last messages
const loadLastMessages = useCallback(async () => { const loadLastMessages = useCallback(async () => {
if (!user.authToken) return; if (!user.authToken) return;
@@ -58,13 +57,9 @@ export function UnifiedChatsList() {
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
if (data.messages && data.messages.length > 0) { if (data.messages?.length > 0) {
const lastMessage = data.messages[data.messages.length - 1]; const lastMessage = data.messages[data.messages.length - 1];
setLastMessages({ general: lastMessage });
setLastMessages({
general: lastMessage,
general2: lastMessage
});
} }
} }
} catch (error) { } catch (error) {
@@ -72,7 +67,6 @@ export function UnifiedChatsList() {
} }
}, [user.authToken]); }, [user.authToken]);
// Load DM users when chats tab is active
useEffect(() => { useEffect(() => {
if (chat.activeTab === "chats") { if (chat.activeTab === "chats") {
loadUsers(); loadUsers();
@@ -80,79 +74,56 @@ export function UnifiedChatsList() {
} }
}, [chat.activeTab, loadUsers, loadLastMessages]); }, [chat.activeTab, loadUsers, loadLastMessages]);
// Combine public chats and DMs into one list const allChats = useMemo<ChatItem[]>(() => {
useEffect(() => { return [
const publicChatItems: ChatItem[] = publicChats.map(chat => ({ ...dmUsers.map((user: DMUser) => ({
...chat, ...user,
lastMessage: lastMessages[chat.id] userId: user.id,
})); type: "dm" as const
})),
{
...PUBLIC_CHAT,
lastMessage: lastMessages[PUBLIC_CHAT.id]
}
];
}, [lastMessages, dmUsers]);
const dmChatItems: ChatItem[] = dmUsers.map((user: DMUser) => ({
id: user.id,
userId: user.id, // Add userId field
username: user.username,
display_name: user.display_name,
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(() => { useEffect(() => {
if (!websocket) return; if (!websocket) return;
const handleWebSocketMessage = (e: MessageEvent) => { function handleWebSocketMessage(e: MessageEvent) {
try { try {
const msg = JSON.parse(e.data); const msg = JSON.parse(e.data);
if (msg.type === "newMessage") { if (msg.type === "newMessage") {
const newMessage = msg.data as Message; const newMessage = msg.data as Message;
// Update all public chats with the new message setLastMessages(prev => ({
setLastMessages(prev => { ...prev,
const updated = { ...prev }; [PUBLIC_CHAT.id]: newMessage
publicChats.forEach(chat => { }));
updated[chat.id] = newMessage;
});
return updated;
});
} else if (msg.type === "messageEdited") { } else if (msg.type === "messageEdited") {
const editedMessage = msg.data as Message; const editedMessage = msg.data as Message;
// Update only if the edited message is the current last message
setLastMessages(prev => { setLastMessages(prev => {
const updated = { ...prev }; if (prev[PUBLIC_CHAT.id]?.id === editedMessage.id) {
publicChats.forEach(chat => { return {
if (updated[chat.id]?.id === editedMessage.id) { ...prev,
updated[chat.id] = editedMessage; [PUBLIC_CHAT.id]: editedMessage
};
} }
}); return prev;
return updated;
}); });
} else if (msg.type === "messageDeleted") { } else if (msg.type === "messageDeleted") {
const deletedMessageId = msg.data?.message_id; const deletedMessageId = msg.data?.message_id;
let needsReload = false;
setLastMessages(prev => { setLastMessages(prev => {
const updated = { ...prev }; if (prev[PUBLIC_CHAT.id]?.id === deletedMessageId) {
publicChats.forEach(chat => {
if (updated[chat.id]?.id === deletedMessageId) {
updated[chat.id] = undefined;
needsReload = true;
}
});
return updated;
});
if (needsReload) {
loadLastMessages(); loadLastMessages();
return {
...prev,
[PUBLIC_CHAT.id]: undefined
};
} }
return prev;
});
} }
} catch (error) { } catch (error) {
console.error("Failed to handle WebSocket message in UnifiedChatsList:", error); console.error("Failed to handle WebSocket message in UnifiedChatsList:", error);
@@ -161,45 +132,33 @@ export function UnifiedChatsList() {
websocket.addEventListener("message", handleWebSocketMessage); websocket.addEventListener("message", handleWebSocketMessage);
return () => websocket.removeEventListener("message", handleWebSocketMessage); return () => websocket.removeEventListener("message", handleWebSocketMessage);
}, [publicChats, loadLastMessages]); }, [loadLastMessages]);
// Subscribe to online status for all DM users
useEffect(() => { useEffect(() => {
const dmUsers = allChats.filter(chat => chat.type === "dm") as DMConversation[];
// Subscribe to all DM users
dmUsers.forEach(dmUser => { dmUsers.forEach(dmUser => {
onlineStatusManager.subscribe(dmUser.id); onlineStatusManager.subscribe(dmUser.id);
}); });
// Cleanup function to unsubscribe from all users
return () => { return () => {
dmUsers.forEach(dmUser => { dmUsers.forEach(dmUser => {
onlineStatusManager.unsubscribe(dmUser.id); onlineStatusManager.unsubscribe(dmUser.id);
}); });
}; };
}, [allChats]); }, [dmUsers]);
function formatPublicChatMessage(chatId: string): string { function formatPublicChatMessage(chatId: string): string {
const lastMessage = lastMessages[chatId]; const lastMessage = lastMessages[chatId];
if (!lastMessage) { if (!lastMessage) return "";
return "";
}
const isCurrentUser = lastMessage.user_id === user.currentUser?.id; const isCurrentUser = lastMessage.user_id === user.currentUser?.id;
const prefix = isCurrentUser ? "Вы: " : `${lastMessage.username}: `; const prefix = isCurrentUser ? "Вы: " : `${lastMessage.username}: `;
const maxLength = 50 - prefix.length;
const maxContentLength = 50 - prefix.length; const content = lastMessage.content.length > maxLength
const content = lastMessage.content.length > maxContentLength ? lastMessage.content.substring(0, maxLength) + "..."
? lastMessage.content.substring(0, maxContentLength) + "..."
: lastMessage.content; : lastMessage.content;
return prefix + content; return prefix + content;
} };
async function handlePublicChatClick(chatName: string) {
await switchToPublicChat(chatName);
}
async function handleDMClick(dmConversation: DMConversation) { async function handleDMClick(dmConversation: DMConversation) {
if (!dmConversation.publicKey) { if (!dmConversation.publicKey) {
@@ -207,12 +166,11 @@ export function UnifiedChatsList() {
if (!authToken) return; if (!authToken) return;
const publicKey = await fetchUserPublicKey(dmConversation.id, authToken); const publicKey = await fetchUserPublicKey(dmConversation.id, authToken);
if (publicKey) { if (!publicKey) {
dmConversation.publicKey = publicKey;
} else {
console.error("Failed to get public key for user:", dmConversation.id); console.error("Failed to get public key for user:", dmConversation.id);
return; return;
} }
dmConversation.publicKey = publicKey;
} }
await switchToDM({ await switchToDM({
@@ -222,26 +180,27 @@ export function UnifiedChatsList() {
profilePicture: dmConversation.profile_picture, profilePicture: dmConversation.profile_picture,
online: dmConversation.online || false online: dmConversation.online || false
}); });
} };
if (isLoadingUsers) { if (isLoadingUsers) {
return <MaterialCircularProgress />; return <MaterialCircularProgress />;
} }
return ( return (
<MaterialList> <MaterialList className={styles.unifiedChatsList}>
{allChats.map((chat) => { {allChats.map((chat) => {
if (chat.type === "public") { if (chat.type === "public") {
const formattedMessage = formatPublicChatMessage(chat.id);
return ( return (
<MaterialListItem <MaterialListItem
key={`public-${chat.id}`} key={`public-${chat.id}`}
headline={chat.name} headline={chat.name}
onClick={() => handlePublicChatClick(chat.name)} onClick={() => switchToPublicChat(chat.name)}
style={{ cursor: "pointer" }} style={{ cursor: "pointer" }}
> >
{formatPublicChatMessage(chat.id) && ( {formattedMessage && (
<span slot="description" className={styles.listDescription}> <span slot="description" className={styles.listDescription}>
{formatPublicChatMessage(chat.id)} {formattedMessage}
</span> </span>
)} )}
<img <img
@@ -257,7 +216,8 @@ export function UnifiedChatsList() {
/> />
</MaterialListItem> </MaterialListItem>
); );
} else { }
return ( return (
<MaterialListItem <MaterialListItem
key={`dm-${chat.id}`} key={`dm-${chat.id}`}
@@ -300,7 +260,6 @@ export function UnifiedChatsList() {
)} )}
</MaterialListItem> </MaterialListItem>
); );
}
})} })}
</MaterialList> </MaterialList>
); );