Починка импорта, плеер для аудио
This commit is contained in:
@@ -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} />
|
||||
|
||||
Reference in New Issue
Block a user