Починка импорта, плеер для аудио

This commit is contained in:
Халимов Рустам
2026-04-06 23:35:45 +03:00
parent d96e4ec7d4
commit 32c9bc43cf
10 changed files with 375 additions and 196 deletions
@@ -63,6 +63,9 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
const [profileUserId, setProfileUserId] = useState<string | null>(null);
const [showGroupSettings, setShowGroupSettings] = useState(false);
const [showScrollDown, setShowScrollDown] = useState(false);
const [stickyDate, setStickyDate] = useState<string | null>(null);
const [showStickyDate, setShowStickyDate] = useState(false);
const stickyDateTimerRef = useRef<any>(null);
const [muted, setMuted] = useState(false);
const [selectionMode, setSelectionMode] = useState(false);
@@ -380,6 +383,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
setShowScrollDown(!isNearBottom);
}, []);
const lastScrollTopRef = useRef(0);
const handleScroll = () => {
if (isInitializingRef.current || isScrollingToBottomRef.current) return;
@@ -389,6 +393,42 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);
scrollTimeoutRef.current = setTimeout(() => saveScrollPosition(), 150);
const st = container.scrollTop;
const isScrollingUp = st < lastScrollTopRef.current;
lastScrollTopRef.current = st;
// Sticky Date Header Logic - Telegram style: show when scrolling, especially up
if (st > 100) {
setShowStickyDate(true);
if (stickyDateTimerRef.current) clearTimeout(stickyDateTimerRef.current);
stickyDateTimerRef.current = setTimeout(() => setShowStickyDate(false), 2000);
} else {
setShowStickyDate(false);
}
const containerRect = container.getBoundingClientRect();
const messageElements = container.querySelectorAll('[data-message-id]');
let currentTopMsgId = null;
for (const el of messageElements) {
const rect = el.getBoundingClientRect();
if (rect.top >= containerRect.top) {
currentTopMsgId = el.getAttribute('data-message-id');
break;
}
}
if (currentTopMsgId) {
const msg = chatMessages.find(m => m.id === currentTopMsgId);
if (msg) {
const dateStr = new Date(msg.createdAt).toLocaleDateString(lang === 'ru' ? 'ru' : 'en', {
day: 'numeric',
month: 'long',
});
if (dateStr !== stickyDate) setStickyDate(dateStr);
}
}
if (container.scrollTop < 100 && hasMoreMessages[activeChat] && !isLoadingMessages) {
useChatStore.getState().loadMessages(activeChat, false, true);
}
@@ -1085,106 +1125,125 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
</button>
)}
<div
ref={messagesContainerRef}
onScroll={handleScroll}
className={`flex-1 overflow-y-auto overflow-x-hidden px-6 pt-6 pb-2 relative z-10 ${!scrollReady && !isLoadingMessages && chatMessages.length > 0 ? 'invisible' : ''}`}
>
{isLoadingMessages && chatMessages.length === 0 ? (
<div className="flex justify-center py-8">
<div className="w-6 h-6 border-2 border-primary border-t-transparent rounded-full animate-spin" />
</div>
) : chatMessages.length === 0 ? (
<div className="flex items-center justify-center h-full">
<div className="flex flex-col items-center gap-4 opacity-30 select-none">
<MessagesSquare size={64} className="text-on-surface-variant" />
<p className="text-sm font-bold uppercase tracking-widest text-on-surface-variant">{t('noMessages')}</p>
</div>
</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-primary 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;
const showDate =
!prevMsg ||
new Date(msg.createdAt).toDateString() !== new Date(prevMsg.createdAt).toDateString();
const isFirstUnread = firstUnreadId === msg.id;
return (
<div
key={msg.id}
data-message-id={msg.id}
data-sequence-id={msg.sequenceId}
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-outline/20"></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-outline/20"></div>
</div>
)}
{showDate && (
<div className="flex justify-center my-4">
<span className="px-3 py-1 rounded-full text-xs text-zinc-400 glass-effect">
{new Date(msg.createdAt).toLocaleDateString(lang === 'ru' ? 'ru' : 'en', {
day: 'numeric',
month: 'long',
})}
</span>
</div>
)}
<MessageBubble
message={msg}
isMine={msg.senderId === user?.id}
showAvatar={showAvatar}
onViewProfile={(userId) => setProfileUserId(userId)}
selectionMode={selectionMode}
isSelected={selectedMessages.has(msg.id)}
onToggleSelect={handleToggleSelect}
onStartSelectionMode={handleStartSelection}
onForward={(id) => {
setSelectedMessages(new Set([id]));
setShowForwardModal(true);
}}
/>
</div>
);
})}
<div ref={messagesEndRef} className="h-4" />
</div>
)}
</div>
<AnimatePresence>
{showScrollDown && (
<motion.button
initial={{ scale: 0.5, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.5, opacity: 0, y: 20 }}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={() => scrollToBottom(true)}
className="absolute bottom-24 right-8 w-14 h-14 rounded-2xl bg-primary text-on-primary shadow-2xl flex items-center justify-center z-20 hover:shadow-primary/20 transition-all"
>
<ArrowDown size={28} />
{unreadCount > 0 && (
<span className="absolute -top-2 -right-2 min-w-[24px] h-6 px-1.5 rounded-full bg-error text-on-error text-[12px] font-black flex items-center justify-center shadow-lg border-2 border-surface-container-lowest">
{unreadCount > 99 ? '99+' : unreadCount}
<div className="relative flex-1 flex flex-col overflow-hidden">
<AnimatePresence mode="wait">
{showStickyDate && stickyDate && (
<motion.div
initial={{ opacity: 0, scale: 0.9, y: -20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
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"
>
<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}
</span>
)}
</motion.button>
)}
</AnimatePresence>
</motion.div>
)}
</AnimatePresence>
<div
ref={messagesContainerRef}
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' : ''}`}
>
{isLoadingMessages && chatMessages.length === 0 ? (
<div className="flex justify-center py-8">
<div className="w-6 h-6 border-2 border-primary border-t-transparent rounded-full animate-spin" />
</div>
) : chatMessages.length === 0 ? (
<div className="flex items-center justify-center h-full">
<div className="flex flex-col items-center gap-4 opacity-30 select-none">
<MessagesSquare size={64} className="text-on-surface-variant" />
<p className="text-sm font-bold uppercase tracking-widest text-on-surface-variant">{t('noMessages')}</p>
</div>
</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-primary 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;
const showDate =
!prevMsg ||
new Date(msg.createdAt).toDateString() !== new Date(prevMsg.createdAt).toDateString();
const isFirstUnread = firstUnreadId === msg.id;
return (
<div
key={msg.id}
data-message-id={msg.id}
data-sequence-id={msg.sequenceId}
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-outline/20"></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-outline/20"></div>
</div>
)}
{showDate && (
<div className="flex justify-center my-4">
<span className="px-3 py-1 rounded-full text-xs text-zinc-400 glass-effect">
{new Date(msg.createdAt).toLocaleDateString(lang === 'ru' ? 'ru' : 'en', {
day: 'numeric',
month: 'long',
})}
</span>
</div>
)}
<MessageBubble
message={msg}
isMine={msg.senderId === user?.id}
showAvatar={showAvatar}
onViewProfile={(userId) => setProfileUserId(userId)}
selectionMode={selectionMode}
isSelected={selectedMessages.has(msg.id)}
onToggleSelect={handleToggleSelect}
onStartSelectionMode={handleStartSelection}
onForward={(id) => {
setSelectedMessages(new Set([id]));
setShowForwardModal(true);
}}
/>
</div>
);
})}
<div ref={messagesEndRef} className="h-4" />
</div>
)}
</div>
<AnimatePresence>
{showScrollDown && (
<motion.button
initial={{ scale: 0.5, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.5, opacity: 0, y: 20 }}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
onClick={() => scrollToBottom(true)}
className="absolute bottom-8 right-8 w-14 h-14 rounded-2xl bg-primary text-on-primary shadow-2xl flex items-center justify-center z-20 hover:shadow-primary/20 transition-all"
>
<ArrowDown size={28} />
{unreadCount > 0 && (
<span className="absolute -top-2 -right-2 min-w-[24px] h-6 px-1.5 rounded-full bg-error text-on-error text-[12px] font-black flex items-center justify-center shadow-lg border-2 border-surface-container-lowest">
{unreadCount > 99 ? '99+' : unreadCount}
</span>
)}
</motion.button>
)}
</AnimatePresence>
</div>
<footer className="flex-shrink-0 bg-surface-container-lowest/40 backdrop-blur-xl border-t border-white/5 pb-safe">
<MessageInput chatId={activeChat} />