mirror of
https://github.com/fromchat-messenger/web.git
synced 2026-09-22 19:15:08 +03:00
Simplify chats list
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user