Импорт из телеграм

This commit is contained in:
Халимов Рустам
2026-03-16 17:51:49 +03:00
parent 33007b3c72
commit 5600067f8d
20 changed files with 1362 additions and 144 deletions

View File

@@ -102,12 +102,25 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
const typingInChat = typingUsers.filter((t) => t.chatId === activeChat && t.userId !== user?.id);
// Refs and logic for tracking session's first unread message to show the divider exactly once per load
const sessionUnreadRef = useRef<{ chatId: string, msgId: string | null }>({ chatId: '', msgId: null });
if (activeChat && activeChat !== sessionUnreadRef.current.chatId && !isLoadingMessages) {
const firstUnreadMsg = chatMessages.find(
(m) => m.senderId !== user?.id && !m.readBy?.some((r) => r.userId === user?.id)
);
sessionUnreadRef.current = { chatId: activeChat, msgId: firstUnreadMsg ? firstUnreadMsg.id : null };
}
const firstUnreadId = activeChat === sessionUnreadRef.current.chatId ? sessionUnreadRef.current.msgId : null;
const lastObservedMessageIdRef = useRef<string | null>(null);
// Load muted state
useEffect(() => {
if (activeChat) {
setMuted(isChatMuted(activeChat));
setScrollReady(false);
setActiveGroupCallParticipants([]);
lastObservedMessageIdRef.current = null;
}
}, [activeChat]);
@@ -167,7 +180,23 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
// Первичная прокрутка при открытии чата или после загрузки (layout effect — до отрисовки)
useLayoutEffect(() => {
if (!isLoadingMessages && messagesContainerRef.current) {
messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
const container = messagesContainerRef.current;
const unreadId = sessionUnreadRef.current.msgId;
if (unreadId && activeChat === sessionUnreadRef.current.chatId) {
const dividerEl = document.getElementById('unread-divider');
const unreadEl = document.getElementById(`msg-${unreadId}`);
const targetEl = dividerEl || unreadEl;
if (targetEl) {
// Вычитаем отступ сверху, чтобы начало непрочитанных было под шапкой
container.scrollTop = targetEl.offsetTop - 60;
} else {
container.scrollTop = container.scrollHeight;
}
} else {
container.scrollTop = container.scrollHeight;
}
setScrollReady(true);
}
}, [activeChat, isLoadingMessages]);
@@ -176,17 +205,25 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
useEffect(() => {
if (chatMessages.length > 0) {
const lastMsg = chatMessages[chatMessages.length - 1];
if (lastMsg.senderId === user?.id) {
setTimeout(() => scrollToBottom(true), 50);
} else {
// Если пользователь внизу — прокрутить
const container = messagesContainerRef.current;
if (container) {
const isNearBottom =
container.scrollHeight - container.scrollTop - container.clientHeight < 250;
if (isNearBottom) setTimeout(() => scrollToBottom(true), 50);
const prevId = lastObservedMessageIdRef.current;
lastObservedMessageIdRef.current = lastMsg.id;
// Scroll ONLY if a genuinely new message was added (not during initial chat load)
if (prevId && prevId !== lastMsg.id) {
if (lastMsg.senderId === user?.id) {
setTimeout(() => scrollToBottom(true), 50);
} else {
// Если пользователь внизу — прокрутить
const container = messagesContainerRef.current;
if (container) {
const isNearBottom =
container.scrollHeight - container.scrollTop - container.clientHeight < 300;
if (isNearBottom) setTimeout(() => scrollToBottom(true), 50);
}
}
}
} else {
lastObservedMessageIdRef.current = null;
}
}, [chatMessages.length, user?.id, scrollToBottom]);
@@ -273,7 +310,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
const container = messagesContainerRef.current;
if (!container) return;
const isNearBottom =
container.scrollHeight - container.scrollTop - container.clientHeight < 200;
container.scrollHeight - container.scrollTop - container.clientHeight < 300;
setShowScrollDown(!isNearBottom);
}, []);
@@ -321,29 +358,20 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
if (!activeChat || !chat) {
return (
<div className="flex-1 h-full flex items-center justify-center bg-surface-secondary/50 rounded-[2rem] overflow-hidden border border-white/5 shadow-2xl relative z-0 backdrop-blur-3xl group">
{/* Slowly pulsing purple background as requested */}
<div className="absolute inset-0 pointer-events-none overflow-hidden transition-opacity duration-[10000ms]">
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[60vw] h-[60vw] max-w-[800px] max-h-[800px] bg-accent/10 rounded-full blur-[120px] animate-[pulse_8s_ease-in-out_infinite]" />
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[40vw] h-[40vw] max-w-[500px] max-h-[500px] bg-purple-600/15 rounded-full blur-[100px] animate-[pulse_12s_ease-in-out_infinite_reverse]" />
</div>
<div className="absolute inset-0 bg-[url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGNpcmNsZSBjeD0iMSIgY3k9IjEiIHI9IjEiIGZpbGw9InJnYmEoMjU1LDI1NSwyNTUsMC4wMSkvPjwvc3ZnPg==')] [mask-image:radial-gradient(ellipse_at_center,black_40%,transparent_100%)] opacity-20 pointer-events-none" />
<div className="text-center relative z-10 w-full max-w-sm px-6 mx-auto">
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
className="w-28 h-28 mx-auto mb-8 rounded-[2rem] bg-gradient-to-br from-accent/20 to-purple-600/20 flex items-center justify-center shadow-[0_0_60px_-15px_var(--color-accent)] ring-1 ring-white/10 backdrop-blur-2xl relative"
className="w-24 h-24 mx-auto mb-6 rounded-full bg-surface-hover flex items-center justify-center border border-border/40 backdrop-blur-md relative"
>
<div className="absolute inset-0 rounded-[2rem] bg-gradient-to-br from-white/[0.05] to-transparent pointer-events-none" />
<MessageSquare className="w-16 h-16 text-accent shadow-2xl transform hover:scale-105 transition-transform" />
<MessageSquare className="w-10 h-10 text-zinc-500 transform hover:scale-105 transition-transform" />
</motion.div>
<motion.h2
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.6, delay: 0.1, ease: [0.16, 1, 0.3, 1] }}
className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-accent via-fuchsia-400 to-indigo-400 mb-4 drop-shadow-lg tracking-tight"
className="text-2xl font-semibold text-white mb-2 tracking-tight"
>
Knot Messenger
</motion.h2>
@@ -351,7 +379,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.6, delay: 0.2, ease: [0.16, 1, 0.3, 1] }}
className="text-sm font-medium text-zinc-300 bg-white/5 backdrop-blur-lg py-2.5 px-6 rounded-full inline-flex border border-white/10 shadow-lg"
className="text-sm text-zinc-400 bg-surface-hover backdrop-blur-lg py-2 px-4 rounded-full inline-flex border border-border/50"
>
{t('selectChat')}
</motion.p>
@@ -743,8 +771,8 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
const el = document.getElementById(`msg-${msg.id}`);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
el.classList.add('bg-knot-500/20');
setTimeout(() => el.classList.remove('bg-knot-500/20'), 2000);
el.classList.add('highlight-message');
setTimeout(() => el.classList.remove('highlight-message'), 3000);
}
setShowSearch(false);
setSearchText('');
@@ -790,8 +818,8 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
const el = document.getElementById(`msg-${pinnedMsg.id}`);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
el.classList.add('bg-knot-500/20');
setTimeout(() => el.classList.remove('bg-knot-500/20'), 2000);
el.classList.add('highlight-message');
setTimeout(() => el.classList.remove('highlight-message'), 3000);
}
}}
className="flex items-center gap-3 px-4 py-2 border-b border-border bg-surface-secondary/60 hover:bg-surface-hover transition-colors text-left w-full flex-shrink-0"
@@ -839,6 +867,8 @@ 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 (
<div
@@ -846,6 +876,15 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
data-message-id={msg.id}
className={`transition-colors duration-500 ${msg.senderId !== user?.id && !msg.readBy?.some(r => r.userId === user?.id) ? 'unread-detector' : ''}`}
>
{isFirstUnread && (
<div id="unread-divider" className="flex items-center justify-center my-4 opacity-80 select-none">
<div className="flex-1 h-px bg-border/50"></div>
<span className="px-4 text-[11px] font-semibold tracking-wider uppercase text-zinc-400">
{t('unreadMessages')}
</span>
<div className="flex-1 h-px bg-border/50"></div>
</div>
)}
{showDate && (
<div className="flex justify-center my-4">
<span className="px-3 py-1 rounded-full text-xs text-zinc-400 glass">
@@ -886,9 +925,9 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0, opacity: 0 }}
onClick={() => scrollToBottom()}
className="absolute bottom-24 right-6 w-11 h-11 rounded-full bg-surface-tertiary/90 backdrop-blur-md border border-border shadow-2xl flex items-center justify-center text-zinc-400 hover:text-white hover:bg-surface-hover hover:scale-105 transition-all z-10"
className="absolute bottom-24 right-5 w-12 h-12 rounded-full bg-surface-secondary/95 backdrop-blur-md border border-border shadow-xl flex items-center justify-center text-zinc-400 hover:text-white hover:bg-surface-hover hover:scale-105 transition-all z-10"
>
<ArrowDown size={20} />
<ArrowDown size={22} className="text-accent hover:text-accent-light transition-colors" />
{unreadCount > 0 && (
<motion.span
initial={{ scale: 0 }}
@@ -923,8 +962,8 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
const el = document.getElementById(`msg-${msgId}`);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
el.classList.add('bg-knot-500/20');
setTimeout(() => el.classList.remove('bg-knot-500/20'), 2000);
el.classList.add('highlight-message');
setTimeout(() => el.classList.remove('highlight-message'), 3000);
setProfileUserId(null);
}
}}
@@ -943,8 +982,8 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
const el = document.getElementById(`msg-${msgId}`);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
el.classList.add('bg-knot-500/20');
setTimeout(() => el.classList.remove('bg-knot-500/20'), 2000);
el.classList.add('highlight-message');
setTimeout(() => el.classList.remove('highlight-message'), 3000);
setShowGroupSettings(false);
}
}}