Импорт чатов с телеграмм, асинхронная загрузка сообщений

This commit is contained in:
Халимов Рустам
2026-03-16 23:13:20 +03:00
parent 5600067f8d
commit 97b70250de
10 changed files with 331 additions and 92 deletions
+25 -2
View File
@@ -48,6 +48,8 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
typingUsers,
pinnedMessages,
isLoadingMessages,
hasMoreMessages,
loadMessages,
setActiveChat,
} = useChatStore();
@@ -113,6 +115,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
}
const firstUnreadId = activeChat === sessionUnreadRef.current.chatId ? sessionUnreadRef.current.msgId : null;
const lastObservedMessageIdRef = useRef<string | null>(null);
const prevScrollHeightRef = useRef<number>(0);
// Load muted state
useEffect(() => {
@@ -199,7 +202,16 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
}
setScrollReady(true);
}
}, [activeChat, isLoadingMessages]);
}, [activeChat, isLoadingMessages && chatMessages.length === 0]);
useLayoutEffect(() => {
if (prevScrollHeightRef.current > 0 && messagesContainerRef.current) {
const container = messagesContainerRef.current;
const diff = container.scrollHeight - prevScrollHeightRef.current;
container.scrollTop += diff;
prevScrollHeightRef.current = 0;
}
}, [chatMessages.length]);
// Scroll on new message arrivals
useEffect(() => {
@@ -316,6 +328,12 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
const handleScroll = () => {
checkScrollPosition();
const container = messagesContainerRef.current;
if (container && container.scrollTop < 100 && activeChat && hasMoreMessages[activeChat] && !isLoadingMessages) {
prevScrollHeightRef.current = container.scrollHeight;
useChatStore.getState().loadMessages(activeChat, false);
}
};
useEffect(() => {
@@ -851,7 +869,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
onScroll={handleScroll}
className={`flex-1 overflow-y-auto px-6 pt-6 pb-2 relative z-10 ${!scrollReady && !isLoadingMessages && chatMessages.length > 0 ? 'invisible' : ''}`}
>
{isLoadingMessages ? (
{isLoadingMessages && chatMessages.length === 0 ? (
<div className="flex justify-center py-8">
<div className="w-6 h-6 border-2 border-knot-500 border-t-transparent rounded-full animate-spin" />
</div>
@@ -861,6 +879,11 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
</div>
) : (
<div className="space-y-1 max-w-3xl mx-auto">
{isLoadingMessages && (
<div className="flex justify-center py-4">
<div className="w-5 h-5 border-2 border-knot-500 border-t-transparent rounded-full animate-spin" />
</div>
)}
{chatMessages.map((msg, i) => {
const prevMsg = i > 0 ? chatMessages[i - 1] : null;
const showAvatar = !prevMsg || prevMsg.senderId !== msg.senderId;
+1 -1
View File
@@ -431,7 +431,7 @@ export default function SideMenu({ isOpen, onClose, onOpenProfile }: SideMenuPro
<div className="px-5 py-3 border-t border-border mt-2">
<h4 className="text-xs text-zinc-500 uppercase tracking-wide mb-3">Хранилище и данные</h4>
<button
onClick={() => { setShowImportModal(true); }}
onClick={() => { loadFriends(); setShowImportModal(true); }}
className="w-full flex items-center gap-4 px-3 py-3 rounded-xl bg-surface-tertiary/50 hover:bg-surface-hover transition-colors"
>
<div className="w-8 h-8 rounded-lg bg-blue-500/20 flex items-center justify-center">
@@ -189,13 +189,11 @@ export default function TelegramImportModal({ isOpen, onClose, friends }: Telegr
>
<option value="">-- Выберите пользователя --</option>
<option value={user?.id}>Это я ({user?.displayName || user?.username})</option>
<optgroup label="Мои контакты">
{friends.map(f => (
<option key={f.id} value={f.id}>
Контакт: {f.displayName || f.username}
</option>
))}
</optgroup>
{friends.map(f => (
<option key={f.id} value={f.id}>
Контакт: {f.displayName || f.username}
</option>
))}
</select>
</div>
))}
+16 -5
View File
@@ -15,13 +15,14 @@ interface ChatState {
isLoadingMessages: boolean;
searchQuery: string;
drafts: Record<string, string>;
hasMoreMessages: Record<string, boolean>;
setActiveChat: (chatId: string | null) => void;
setSearchQuery: (query: string) => void;
setDraft: (chatId: string, text: string) => void;
getDraft: (chatId: string) => string;
loadChats: () => Promise<void>;
loadMessages: (chatId: string) => Promise<void>;
loadMessages: (chatId: string, reset?: boolean) => Promise<void>;
addMessage: (message: Message) => void;
updateMessage: (message: Message) => void;
removeMessage: (messageId: string, chatId: string) => void;
@@ -56,6 +57,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
isLoadingMessages: false,
searchQuery: '',
drafts: JSON.parse(localStorage.getItem('knot_drafts') || '{}'),
hasMoreMessages: {},
setActiveChat: (chatId) => set((state) => ({
activeChat: chatId,
@@ -111,13 +113,21 @@ export const useChatStore = create<ChatState>((set, get) => ({
}
},
loadMessages: async (chatId) => {
loadMessages: async (chatId, reset = false) => {
try {
const state = get();
if (!reset && state.messages[chatId] && state.hasMoreMessages[chatId] === false) return;
if (state.isLoadingMessages) return;
set({ isLoadingMessages: true });
const fetched = await api.getMessages(chatId);
const currentMessages = state.messages[chatId] || [];
const cursor = !reset && currentMessages.length > 0 ? currentMessages[0].createdAt : undefined;
const fetched = await api.getMessages(chatId, cursor);
set((state) => {
// Merge fetched messages with any that arrived via socket during the fetch
const existing = state.messages[chatId] || [];
// Merge fetched messages with any that arrived via socket
const existing = reset ? [] : (state.messages[chatId] || []);
const fetchedIds = new Set(fetched.map(m => m.id));
const socketOnly = existing.filter(m => !fetchedIds.has(m.id));
const merged = [...fetched, ...socketOnly].sort(
@@ -125,6 +135,7 @@ export const useChatStore = create<ChatState>((set, get) => ({
);
return {
messages: { ...state.messages, [chatId]: merged },
hasMoreMessages: { ...state.hasMoreMessages, [chatId]: fetched.length === 100 },
isLoadingMessages: false,
};
});