Фиксы по историям
This commit is contained in:
@@ -191,25 +191,22 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
const scrollTimeoutRef = useRef<any>(null);
|
||||
const prevChatIdRef = useRef<string | null>(activeChat);
|
||||
|
||||
// 1. СОХРАНЕНИЕ ПОЗИЦИИ (СТРОГО ПО ID)
|
||||
// 1. СОХРАНЕНИЕ ПОЗИЦИИ (ЯКОРНОЕ ПО MESSAGE ID)
|
||||
const saveScrollPosition = useCallback((targetChatId?: string) => {
|
||||
const container = messagesContainerRef.current;
|
||||
const chatId = targetChatId || activeChat;
|
||||
|
||||
// НЕ сохраняем, если чат ещё не восстановил свою позицию или в процессе загрузки
|
||||
if (!container || !chatId || !scrollReady || isInitializingRef.current || isScrollingToBottomRef.current) return;
|
||||
|
||||
// ГАРАНТИЯ: Если сообщения в стейте НЕ от этого чата - не пишем в память мусор
|
||||
// Проверка: сообщения в стейте должны быть от целевого чата
|
||||
if (chatMessages.length > 0 && chatMessages[0].chatId !== chatId) return;
|
||||
|
||||
// КРИТИЧНО: Если этот чат уже помечен как находящийся внизу,
|
||||
// не позволяем автоматике перезаписать это якорем (защита "второго клика")
|
||||
if (localStorage.getItem(`chat_at_bottom_${chatId}`) === 'true') {
|
||||
const isNearBottomNow = container.scrollHeight - container.scrollTop - container.clientHeight < 150;
|
||||
if (isNearBottomNow) {
|
||||
localStorage.removeItem(`chat_anchor_${chatId}`);
|
||||
return;
|
||||
}
|
||||
const isAtBottomNow = container.scrollHeight - container.scrollTop - container.clientHeight < 150;
|
||||
|
||||
if (isAtBottomNow) {
|
||||
localStorage.setItem(`chat_at_bottom_${chatId}`, 'true');
|
||||
localStorage.removeItem(`chat_anchor_${chatId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const messageElements = container.querySelectorAll('[data-message-id]');
|
||||
@@ -218,9 +215,10 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
let anchor = null;
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
|
||||
// Находим первое сообщение, которое пересекает верхнюю границу видимости
|
||||
for (const el of messageElements) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (rect.top >= containerRect.top) {
|
||||
if (rect.bottom > containerRect.top) {
|
||||
anchor = {
|
||||
id: el.getAttribute('data-message-id'),
|
||||
offset: rect.top - containerRect.top
|
||||
@@ -231,13 +229,6 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
|
||||
if (anchor && anchor.id) {
|
||||
localStorage.setItem(`chat_anchor_${chatId}`, JSON.stringify(anchor));
|
||||
}
|
||||
|
||||
const isAtBottomNow = container.scrollHeight - container.scrollTop - container.clientHeight < 150;
|
||||
if (isAtBottomNow) {
|
||||
localStorage.setItem(`chat_at_bottom_${chatId}`, 'true');
|
||||
localStorage.removeItem(`chat_anchor_${chatId}`);
|
||||
} else {
|
||||
localStorage.removeItem(`chat_at_bottom_${chatId}`);
|
||||
}
|
||||
}, [activeChat, scrollReady, chatMessages]);
|
||||
@@ -247,32 +238,23 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
const container = messagesContainerRef.current;
|
||||
if (!container || !activeChat) return false;
|
||||
|
||||
// КРИТИЧНО: Ждем, пока в хранилище сообщений появятся данные именно от активного чата
|
||||
if (chatMessages.length > 0 && chatMessages[0].chatId !== activeChat) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (chatMessages.length > 0 && chatMessages[0].chatId !== activeChat) return false;
|
||||
if (chatMessages.length === 0) return true;
|
||||
|
||||
// Сначала проверяем, был ли пользователь внизу (защита "второго клика")
|
||||
// ПРИОРИТЕТ 1: Если чат внизу (после второго клика или скролла)
|
||||
if (localStorage.getItem(`chat_at_bottom_${activeChat}`) === 'true') {
|
||||
scrollToBottom(false);
|
||||
|
||||
// Агрессивные повторы, если контент еще догружается (картинки и т.д.)
|
||||
for (const delay of [100, 300, 600, 1000]) {
|
||||
setTimeout(() => {
|
||||
if (activeChat === prevChatIdRef.current) scrollToBottom(false);
|
||||
}, delay);
|
||||
}
|
||||
container.scrollTop = container.scrollHeight;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Восстановление по якорю сообщения
|
||||
// ПРИОРИТЕТ 2: Восстановление по якорю сообщения
|
||||
const saved = localStorage.getItem(`chat_anchor_${activeChat}`);
|
||||
if (saved) {
|
||||
try {
|
||||
const { id, offset } = JSON.parse(saved);
|
||||
let el = container.querySelector(`[data-message-id="${id}"]`) as HTMLElement;
|
||||
|
||||
// Поиск ближайшего, если точное сообщение еще не загружено
|
||||
if (!el) {
|
||||
const msgIndex = chatMessages.findIndex(m => m.id === id);
|
||||
if (msgIndex !== -1) {
|
||||
@@ -282,15 +264,15 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (el) {
|
||||
container.scrollTop = el.offsetTop - offset;
|
||||
return true;
|
||||
}
|
||||
if (chatMessages.some(m => m.id === id)) return false;
|
||||
} catch (e) { console.error(e); }
|
||||
} catch (e) { console.error('Anchor restoration failed', e); }
|
||||
}
|
||||
|
||||
// Если ничего нет - к непрочитанным или вниз
|
||||
// ПРИОРИТЕТ 3: Непрочитанные или низ
|
||||
const firstUnread = chatMessages.find(m => m.senderId !== user?.id && !m.readBy?.some(r => r.userId === user?.id));
|
||||
if (firstUnread) {
|
||||
const el = document.getElementById(`msg-${firstUnread.id}`) || document.getElementById('unread-divider');
|
||||
@@ -299,17 +281,17 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
return true;
|
||||
}
|
||||
}
|
||||
scrollToBottom(false);
|
||||
return true;
|
||||
}, [activeChat, chatMessages, scrollToBottom, user?.id]);
|
||||
|
||||
// 3. ОБЗЕРВЕР И ИНИЦИАЛИЗАЦИЯ
|
||||
container.scrollTop = container.scrollHeight;
|
||||
return true;
|
||||
}, [activeChat, chatMessages, user?.id]);
|
||||
|
||||
// 3. ОБЗЕРВЕР И УПРАВЛЕНИЕ ЖИЗНЕННЫМ ЦИКЛОМ
|
||||
useEffect(() => {
|
||||
if (isLoadingMessages || !messagesContainerRef.current || !activeChat) return;
|
||||
const container = messagesContainerRef.current;
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
// Игнорируем замеры, пока мы не отпозиционировали чат изначально
|
||||
if (isInitializingRef.current) return;
|
||||
|
||||
if (!scrollReady) {
|
||||
@@ -318,17 +300,15 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
isInitializingRef.current = false;
|
||||
}
|
||||
} else if (localStorage.getItem(`chat_at_bottom_${activeChat}`) === 'true') {
|
||||
const isNearBottomNow = container.scrollHeight - container.scrollTop - container.clientHeight < 300;
|
||||
if (isNearBottomNow) {
|
||||
scrollToBottom(true);
|
||||
}
|
||||
// Удержание внизу при росте контента
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
});
|
||||
|
||||
const messagesDiv = container.querySelector('.space-y-1');
|
||||
if (messagesDiv) observer.observe(messagesDiv);
|
||||
|
||||
// Принудительно пробуем восстановить, если сообщения ПРАВИЛЬНЫЕ
|
||||
// Попытка восстановления при появлении правильных сообщений
|
||||
if (chatMessages.length > 0 && chatMessages[0].chatId === activeChat && !scrollReady) {
|
||||
if (restoreScrollPosition()) {
|
||||
setScrollReady(true);
|
||||
@@ -337,30 +317,29 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [activeChat, isLoadingMessages, chatMessages, restoreScrollPosition, scrollReady, scrollToBottom]);
|
||||
}, [activeChat, isLoadingMessages, chatMessages, restoreScrollPosition, scrollReady]);
|
||||
|
||||
// 4. ПЕРЕКЛЮЧЕНИЕ ЧАТОВ (ФИКС RACE CONDITION)
|
||||
// 4. ПЕРЕКЛЮЧЕНИЕ ЧАТОВ (СИНХРОННОЕ)
|
||||
useLayoutEffect(() => {
|
||||
if (activeChat !== prevChatIdRef.current) {
|
||||
// СРАЗУ блокируем сохранение, чтобы handleScroll ничего не записал при изменении высоты
|
||||
isInitializingRef.current = true;
|
||||
isScrollingToBottomRef.current = false;
|
||||
|
||||
// Сохраняем позицию ПРЕДЫДУЩЕГО чата ПЕРЕД установкой новых данных
|
||||
// Сохраняем позицию старого чата
|
||||
if (prevChatIdRef.current && messagesContainerRef.current && scrollReady) {
|
||||
saveScrollPosition(prevChatIdRef.current);
|
||||
saveScrollPosition(prevChatIdRef.current);
|
||||
}
|
||||
|
||||
setScrollReady(false);
|
||||
prevChatIdRef.current = activeChat;
|
||||
|
||||
if (messagesContainerRef.current) {
|
||||
messagesContainerRef.current.scrollTop = 0;
|
||||
messagesContainerRef.current.scrollTop = 0;
|
||||
}
|
||||
|
||||
// Таймер защиты на случай медленного рендера
|
||||
const timer = setTimeout(() => {
|
||||
isInitializingRef.current = false;
|
||||
}, 1000);
|
||||
isInitializingRef.current = false;
|
||||
}, 600);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
@@ -374,17 +353,13 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
}, []);
|
||||
|
||||
const handleScroll = () => {
|
||||
// В период инициализации (1.2с) или принудительного скролла вниз - игнорируем любые события.
|
||||
if (isInitializingRef.current || isScrollingToBottomRef.current) return;
|
||||
|
||||
checkScrollPosition();
|
||||
const container = messagesContainerRef.current;
|
||||
if (container && activeChat) {
|
||||
const isNearBottomNow = container.scrollHeight - container.scrollTop - container.clientHeight < 120;
|
||||
if (!isNearBottomNow) localStorage.removeItem(`chat_at_bottom_${activeChat}`);
|
||||
|
||||
if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);
|
||||
scrollTimeoutRef.current = setTimeout(() => saveScrollPosition(), 200);
|
||||
scrollTimeoutRef.current = setTimeout(() => saveScrollPosition(), 150);
|
||||
|
||||
if (container.scrollTop < 100 && hasMoreMessages[activeChat] && !isLoadingMessages) {
|
||||
useChatStore.getState().loadMessages(activeChat, false, true);
|
||||
@@ -395,18 +370,26 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
useEffect(() => {
|
||||
const handleScrollEvent = (e: any) => {
|
||||
if (e.detail?.chatId === activeChat) {
|
||||
isScrollingToBottomRef.current = true;
|
||||
// Принудительный сброс режима (Второй Клик)
|
||||
localStorage.setItem(`chat_at_bottom_${activeChat}`, 'true');
|
||||
localStorage.removeItem(`chat_anchor_${activeChat}`);
|
||||
scrollToBottom(true);
|
||||
|
||||
isScrollingToBottomRef.current = true;
|
||||
if (messagesContainerRef.current) {
|
||||
messagesContainerRef.current.scrollTo({
|
||||
top: messagesContainerRef.current.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
isScrollingToBottomRef.current = false;
|
||||
}, 500);
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
window.addEventListener('CHAT_SCROLL_TO_BOTTOM', handleScrollEvent);
|
||||
return () => window.removeEventListener('CHAT_SCROLL_TO_BOTTOM', handleScrollEvent);
|
||||
}, [activeChat, scrollToBottom]);
|
||||
}, [activeChat]);
|
||||
|
||||
// Read receipts using IntersectionObserver
|
||||
const sentReadIdsRef = useRef<Set<string>>(new Set());
|
||||
@@ -426,7 +409,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
let highestSequenceId = -1;
|
||||
let highestMsgId = '';
|
||||
const newlyReadIds: string[] = [];
|
||||
|
||||
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
const msgId = entry.target.getAttribute('data-message-id');
|
||||
@@ -435,7 +418,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
newlyReadIds.push(msgId);
|
||||
sentReadIdsRef.current.add(msgId);
|
||||
observer.unobserve(entry.target);
|
||||
|
||||
|
||||
const seqId = parseInt(seqIdAttr, 10);
|
||||
if (seqId > highestSequenceId) {
|
||||
highestSequenceId = seqId;
|
||||
@@ -568,16 +551,16 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
forum
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
|
||||
<h2 className="text-3xl font-black text-[#e5e2e1] tracking-tight mb-2 leading-tight">
|
||||
{t('selectChatTitle')}
|
||||
</h2>
|
||||
|
||||
|
||||
<p className="text-sm font-medium text-[#c1c6d7] max-w-sm leading-relaxed opacity-40 px-4">
|
||||
{t('selectChatSubtext')}
|
||||
</p>
|
||||
|
||||
<motion.button
|
||||
<motion.button
|
||||
initial={{ y: 20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
@@ -1078,7 +1061,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
const showDate =
|
||||
!prevMsg ||
|
||||
new Date(msg.createdAt).toDateString() !== new Date(prevMsg.createdAt).toDateString();
|
||||
|
||||
|
||||
const isFirstUnread = firstUnreadId === msg.id;
|
||||
|
||||
return (
|
||||
@@ -1198,21 +1181,21 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const chatMessages = chatStore.messages[activeChat] || [];
|
||||
const oldestLoaded = chatMessages.length > 0 ? new Date(chatMessages[0].createdAt).getTime() : Date.now();
|
||||
|
||||
|
||||
// If target is newer than oldest loaded, and not found, maybe it's in a gap or we need to keep loading?
|
||||
// Actually target is almost always older if not found.
|
||||
// If we don't have targetCreatedAt, we guess (up to 100 attempts)
|
||||
if (targetDate && targetDate > oldestLoaded && chatMessages.some(m => m.id === msgId)) {
|
||||
// Should have been found by tryScroll, but lets try one last time
|
||||
if (tryScroll()) { found = true; break; }
|
||||
// Should have been found by tryScroll, but lets try one last time
|
||||
if (tryScroll()) { found = true; break; }
|
||||
}
|
||||
|
||||
if (chatStore.hasMoreMessages[activeChat] === false && oldestLoaded <= targetDate) break;
|
||||
|
||||
|
||||
await chatStore.loadMessages(activeChat, false, true);
|
||||
// Give React 150ms to render the new messages
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
|
||||
|
||||
if (tryScroll()) {
|
||||
found = true;
|
||||
break;
|
||||
@@ -1220,11 +1203,11 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
|
||||
// Stop if we have gone way past the target date
|
||||
if (targetDate && oldestLoaded < (targetDate - 1000 * 60 * 60)) {
|
||||
// We are 1 hour before the message and still haven't found it? might be deleted
|
||||
if (i > 10) break;
|
||||
// We are 1 hour before the message and still haven't found it? might be deleted
|
||||
if (i > 10) break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!found) {
|
||||
NotificationStore.useNotificationStore.getState().addNotification('warning', lang === 'ru' ? 'Сообщение не найдено' : 'Message not found');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user