Заготовка опросов
This commit is contained in:
@@ -77,7 +77,58 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
const [activeGroupCallParticipants, setActiveGroupCallParticipants] = useState<string[]>([]);
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleJumpToMessage = async (msgId: string, cleanup?: () => void, targetCreatedAt?: string) => {
|
||||
cleanup?.();
|
||||
const tryScroll = () => {
|
||||
const el = document.getElementById(`msg-${msgId}`);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
el.classList.add('highlight-message');
|
||||
setTimeout(() => el.classList.remove('highlight-message'), 5000);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (tryScroll()) return;
|
||||
if (!activeChat) return;
|
||||
|
||||
const NotificationStore = await import('../../../../core/application/stores/notificationStore');
|
||||
NotificationStore.useNotificationStore.getState().addNotification('info', (t as any)('searchingHistory') || 'Searching message in history...');
|
||||
|
||||
const chatStore = useChatStore.getState();
|
||||
let found = false;
|
||||
const targetDate = targetCreatedAt ? new Date(targetCreatedAt).getTime() : 0;
|
||||
|
||||
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 (targetDate && targetDate > oldestLoaded && chatMessages.some(m => m.id === msgId)) {
|
||||
if (tryScroll()) { found = true; break; }
|
||||
}
|
||||
|
||||
if (chatStore.hasMoreMessages[activeChat] === false && oldestLoaded <= targetDate) break;
|
||||
|
||||
await chatStore.loadMessages(activeChat, false, true);
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
|
||||
if (tryScroll()) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (targetDate && oldestLoaded < (targetDate - 1000 * 60 * 60)) {
|
||||
if (i > 10) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
NotificationStore.useNotificationStore.getState().addNotification('warning', (t as any)('messageNotFound') || 'Message not found');
|
||||
}
|
||||
};
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const topMenuRef = useRef<HTMLDivElement>(null);
|
||||
const deleteMenuRef = useRef<HTMLDivElement>(null);
|
||||
@@ -87,7 +138,8 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
const allChatMessages = activeChat ? messages[activeChat] || [] : [];
|
||||
// Filter out deleted messages to prevent layout shifts
|
||||
const chatMessages = allChatMessages.filter(m => !m.isDeleted);
|
||||
const pinnedMsg = activeChat ? pinnedMessages[activeChat] : null;
|
||||
const chatPinnedMessages = activeChat ? pinnedMessages[activeChat] || [] : [];
|
||||
const [pinnedIndex, setPinnedIndex] = useState(0);
|
||||
|
||||
const [importStatus, setImportStatus] = useState<{ processed: number, total: number, status: string } | null>(null);
|
||||
const isAtBottomRef = useRef(false);
|
||||
@@ -213,8 +265,8 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
const scrollToBottom = useCallback((smooth = true) => {
|
||||
if (messagesEndRef.current) {
|
||||
messagesEndRef.current.scrollIntoView({ behavior: smooth ? 'smooth' : 'instant', block: 'end' });
|
||||
} else if (messagesContainerRef.current) {
|
||||
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
|
||||
} else if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTop = scrollContainerRef.current.scrollHeight;
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -225,7 +277,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
|
||||
// 1. СОХРАНЕНИЕ ПОЗИЦИИ (ЯКОРНОЕ ПО MESSAGE ID)
|
||||
const saveScrollPosition = useCallback((targetChatId?: string) => {
|
||||
const container = messagesContainerRef.current;
|
||||
const container = scrollContainerRef.current;
|
||||
const chatId = targetChatId || activeChat;
|
||||
|
||||
if (!container || !chatId || !scrollReady || isInitializingRef.current || isScrollingToBottomRef.current) return;
|
||||
@@ -233,7 +285,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
// Проверка: сообщения в стейте должны быть от целевого чата
|
||||
if (chatMessages.length > 0 && chatMessages[0].chatId !== chatId) return;
|
||||
|
||||
const isAtBottomNow = container.scrollHeight - container.scrollTop - container.clientHeight < 100;
|
||||
const isAtBottomNow = container.scrollHeight - container.scrollTop - container.clientHeight < 40;
|
||||
isAtBottomRef.current = isAtBottomNow;
|
||||
|
||||
if (isAtBottomNow) {
|
||||
@@ -268,7 +320,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
|
||||
// 2. ВОССТАНОВЛЕНИЕ ПОЗИЦИИ
|
||||
const restoreScrollPosition = useCallback(() => {
|
||||
const container = messagesContainerRef.current;
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container || !activeChat) return false;
|
||||
|
||||
if (chatMessages.length > 0 && chatMessages[0].chatId !== activeChat) return false;
|
||||
@@ -323,8 +375,8 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
|
||||
// 3. ОБЗЕРВЕР И УПРАВЛЕНИЕ ЖИЗНЕННЫМ ЦИКЛОМ
|
||||
useEffect(() => {
|
||||
if (isLoadingMessages || !messagesContainerRef.current || !activeChat) return;
|
||||
const container = messagesContainerRef.current;
|
||||
if (isLoadingMessages || !scrollContainerRef.current || !activeChat) return;
|
||||
const container = scrollContainerRef.current;
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
if (isInitializingRef.current) return;
|
||||
@@ -361,15 +413,15 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
isScrollingToBottomRef.current = false;
|
||||
|
||||
// Сохраняем позицию старого чата
|
||||
if (prevChatIdRef.current && messagesContainerRef.current && scrollReady) {
|
||||
if (prevChatIdRef.current && scrollContainerRef.current && scrollReady) {
|
||||
saveScrollPosition(prevChatIdRef.current);
|
||||
}
|
||||
|
||||
setScrollReady(false);
|
||||
prevChatIdRef.current = activeChat;
|
||||
|
||||
if (messagesContainerRef.current) {
|
||||
messagesContainerRef.current.scrollTop = 0;
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTop = 0;
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
@@ -381,7 +433,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
}, [activeChat, saveScrollPosition, scrollReady]);
|
||||
|
||||
const checkScrollPosition = useCallback(() => {
|
||||
const container = messagesContainerRef.current;
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container || isInitializingRef.current) return;
|
||||
const isNearBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 300;
|
||||
setShowScrollDown(!isNearBottom);
|
||||
@@ -392,10 +444,10 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
if (isInitializingRef.current || isScrollingToBottomRef.current) return;
|
||||
|
||||
checkScrollPosition();
|
||||
const container = messagesContainerRef.current;
|
||||
const container = scrollContainerRef.current;
|
||||
if (container && activeChat) {
|
||||
// Synchronous "at bottom" check to prevent ResizeObserver from fighting the user
|
||||
const isAtBottomNow = container.scrollHeight - container.scrollTop - container.clientHeight < 100;
|
||||
const isAtBottomNow = container.scrollHeight - container.scrollTop - container.clientHeight < 40;
|
||||
isAtBottomRef.current = isAtBottomNow;
|
||||
|
||||
if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);
|
||||
@@ -405,12 +457,12 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
const isScrollingUp = st < lastScrollTopRef.current;
|
||||
lastScrollTopRef.current = st;
|
||||
|
||||
// Sticky Date Header Logic - Telegram style: show when scrolling, especially up
|
||||
if (st > 100) {
|
||||
// Sticky Date Header Logic - Telegram style
|
||||
if (st > 100 && (isScrollingUp || st !== lastScrollTopRef.current)) {
|
||||
setShowStickyDate(true);
|
||||
if (stickyDateTimerRef.current) clearTimeout(stickyDateTimerRef.current);
|
||||
stickyDateTimerRef.current = setTimeout(() => setShowStickyDate(false), 2000);
|
||||
} else {
|
||||
stickyDateTimerRef.current = setTimeout(() => setShowStickyDate(false), isScrollingUp ? 1500 : 1000);
|
||||
} else if (st <= 100) {
|
||||
setShowStickyDate(false);
|
||||
}
|
||||
|
||||
@@ -451,9 +503,9 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
localStorage.removeItem(`chat_anchor_${activeChat}`);
|
||||
|
||||
isScrollingToBottomRef.current = true;
|
||||
if (messagesContainerRef.current) {
|
||||
messagesContainerRef.current.scrollTo({
|
||||
top: messagesContainerRef.current.scrollHeight,
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTo({
|
||||
top: scrollContainerRef.current.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
@@ -514,7 +566,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
}
|
||||
},
|
||||
{
|
||||
root: messagesContainerRef.current,
|
||||
root: scrollContainerRef.current,
|
||||
threshold: 0.1,
|
||||
}
|
||||
);
|
||||
@@ -522,9 +574,9 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
observerRef.current = observer;
|
||||
|
||||
const observeUnread = () => {
|
||||
if (!messagesContainerRef.current) return;
|
||||
const unreadElements = messagesContainerRef.current.querySelectorAll('.unread-detector');
|
||||
unreadElements.forEach((el) => {
|
||||
if (!scrollContainerRef.current) return;
|
||||
const unreadElements = scrollContainerRef.current.querySelectorAll('.unread-detector');
|
||||
unreadElements.forEach((el: Element) => {
|
||||
const id = el.getAttribute('data-message-id');
|
||||
if (id && !sentReadIdsRef.current.has(id)) {
|
||||
observer.observe(el);
|
||||
@@ -537,9 +589,9 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
}, [activeChat, user?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (observerRef.current && messagesContainerRef.current) {
|
||||
const unreadElements = messagesContainerRef.current.querySelectorAll('.unread-detector');
|
||||
unreadElements.forEach((el) => {
|
||||
if (observerRef.current && scrollContainerRef.current) {
|
||||
const unreadElements = scrollContainerRef.current.querySelectorAll('.unread-detector');
|
||||
unreadElements.forEach((el: Element) => {
|
||||
const id = el.getAttribute('data-message-id');
|
||||
if (id && !sentReadIdsRef.current.has(id)) {
|
||||
observerRef.current?.observe(el);
|
||||
@@ -949,6 +1001,8 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
try {
|
||||
await ChatApi.clearChat(activeChat);
|
||||
useChatStore.getState().clearMessages(activeChat);
|
||||
const NotificationStore = await import('../../../../core/application/stores/notificationStore');
|
||||
NotificationStore.useNotificationStore.getState().addNotification('success', t('chatCleared'));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
@@ -1100,37 +1154,62 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
</button>
|
||||
)}
|
||||
|
||||
{pinnedMsg && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const el = document.getElementById(`msg-${pinnedMsg.id}`);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
el.classList.add('highlight-message');
|
||||
setTimeout(() => el.classList.remove('highlight-message'), 5000);
|
||||
}
|
||||
}}
|
||||
className="flex items-center gap-3 px-4 py-2 border-b border-outline/10 bg-surface-container-high/60 hover:bg-surface-container-highest transition-colors text-left w-full flex-shrink-0"
|
||||
>
|
||||
<Pin size={16} className="text-primary flex-shrink-0 rotate-45" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs font-medium text-primary">{t('pinnedMessage')}</p>
|
||||
<p className="text-sm text-zinc-300 truncate">
|
||||
{pinnedMsg.content || (pinnedMsg.media?.length > 0 ? t('media') : '...')}
|
||||
</p>
|
||||
</div>
|
||||
<X
|
||||
size={16}
|
||||
className="text-zinc-500 hover:text-white flex-shrink-0 transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const socket = getSocket();
|
||||
if (socket && activeChat) {
|
||||
socket.emit('unpin_message', { messageId: pinnedMsg.id, chatId: activeChat });
|
||||
{chatPinnedMessages.length > 0 && (
|
||||
<div className="flex-shrink-0 flex items-center gap-0 border-b border-outline/10 bg-surface-container-high/60 hover:bg-surface-container-high transition-colors overflow-hidden h-[54px] relative">
|
||||
{/* Cycling progress indicator for multiple pins */}
|
||||
{chatPinnedMessages.length > 1 && (
|
||||
<div className="absolute left-1 top-1.5 bottom-1.5 w-0.5 rounded-full bg-white/5 flex flex-col gap-0.5 overflow-hidden">
|
||||
{chatPinnedMessages.map((_, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`flex-1 transition-colors duration-300 ${idx === pinnedIndex % chatPinnedMessages.length ? 'bg-primary' : 'bg-primary/20'}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
const currentPin = chatPinnedMessages[pinnedIndex % chatPinnedMessages.length];
|
||||
if (!currentPin) return;
|
||||
handleJumpToMessage(currentPin.id, undefined, currentPin.createdAt);
|
||||
if (chatPinnedMessages.length > 1) {
|
||||
setPinnedIndex(prev => (prev + 1) % chatPinnedMessages.length);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
className={`flex-1 flex items-center gap-3 px-4 py-2 text-left h-full ${chatPinnedMessages.length > 1 ? 'ml-1.5' : ''}`}
|
||||
>
|
||||
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
|
||||
<Pin size={14} className="text-primary rotate-45" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-[11px] font-black text-primary uppercase tracking-wider">
|
||||
{t('pinnedMessage')} {chatPinnedMessages.length > 1 ? `#${(pinnedIndex % chatPinnedMessages.length) + 1}` : ''}
|
||||
</p>
|
||||
<p className="text-sm text-zinc-300 truncate font-medium">
|
||||
{chatPinnedMessages[pinnedIndex % chatPinnedMessages.length]?.content ||
|
||||
(chatPinnedMessages[pinnedIndex % chatPinnedMessages.length]?.media?.length > 0 ? t('media') : '...')}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div className="flex items-center px-2">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const socket = getSocket();
|
||||
const currentPin = chatPinnedMessages[pinnedIndex % chatPinnedMessages.length];
|
||||
if (socket && activeChat && currentPin) {
|
||||
socket.emit('unpin_message', { messageId: currentPin.id, chatId: activeChat });
|
||||
}
|
||||
}}
|
||||
className="w-8 h-8 rounded-full flex items-center justify-center text-zinc-500 hover:text-white hover:bg-white/5 transition-all"
|
||||
title={t('unpin' as any) || 'Открепить'}
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative flex-1 flex flex-col overflow-hidden">
|
||||
@@ -1142,7 +1221,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
exit={{ opacity: 0, scale: 0.9, y: -20 }}
|
||||
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
|
||||
key="sticky-date"
|
||||
className="absolute top-6 left-1/2 -translate-x-1/2 z-[200] pointer-events-none"
|
||||
className="absolute top-6 left-1/2 -translate-x-1/2 z-[999] pointer-events-none"
|
||||
>
|
||||
<span className="px-4 py-1.5 rounded-full text-[11px] font-black uppercase tracking-widest text-white bg-black/60 backdrop-blur-xl shadow-[0_10px_30px_rgba(0,0,0,0.5)] border border-white/10 ring-2 ring-black/20 whitespace-nowrap">
|
||||
{stickyDate}
|
||||
@@ -1152,7 +1231,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
</AnimatePresence>
|
||||
|
||||
<div
|
||||
ref={messagesContainerRef}
|
||||
ref={scrollContainerRef}
|
||||
onScroll={handleScroll}
|
||||
className={`flex-1 overflow-y-auto overflow-x-hidden px-6 pt-6 pb-2 relative z-10 scroll-smooth-container ${!scrollReady && !isLoadingMessages && chatMessages.length > 0 ? 'invisible' : ''}`}
|
||||
>
|
||||
@@ -1253,7 +1332,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
<footer className="flex-shrink-0 bg-surface-container-lowest/40 backdrop-blur-xl border-t border-white/5 pb-safe">
|
||||
<footer className="flex-shrink-0 bg-surface-container-lowest/40 backdrop-blur-xl border-t border-white/5 pb-safe relative z-50">
|
||||
<MessageInput chatId={activeChat} />
|
||||
</footer>
|
||||
</>
|
||||
@@ -1266,57 +1345,6 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
)}
|
||||
|
||||
{(() => {
|
||||
const handleJumpToMessage = async (msgId: string, cleanup?: () => void, targetCreatedAt?: string) => {
|
||||
cleanup?.();
|
||||
const tryScroll = () => {
|
||||
const el = document.getElementById(`msg-${msgId}`);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
el.classList.add('highlight-message');
|
||||
setTimeout(() => el.classList.remove('highlight-message'), 5000);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (tryScroll()) return;
|
||||
if (!activeChat) return;
|
||||
|
||||
const NotificationStore = await import('../../../../core/application/stores/notificationStore');
|
||||
NotificationStore.useNotificationStore.getState().addNotification('info', (t as any)('searchingHistory') || 'Searching message in history...');
|
||||
|
||||
const chatStore = useChatStore.getState();
|
||||
let found = false;
|
||||
const targetDate = targetCreatedAt ? new Date(targetCreatedAt).getTime() : 0;
|
||||
|
||||
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 (targetDate && targetDate > oldestLoaded && chatMessages.some(m => m.id === msgId)) {
|
||||
if (tryScroll()) { found = true; break; }
|
||||
}
|
||||
|
||||
if (chatStore.hasMoreMessages[activeChat] === false && oldestLoaded <= targetDate) break;
|
||||
|
||||
await chatStore.loadMessages(activeChat, false, true);
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
|
||||
if (tryScroll()) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (targetDate && oldestLoaded < (targetDate - 1000 * 60 * 60)) {
|
||||
if (i > 10) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
NotificationStore.useNotificationStore.getState().addNotification('warning', (t as any)('messageNotFound') || 'Message not found');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence>
|
||||
|
||||
Reference in New Issue
Block a user