Compare commits
8
Commits
72325f48e5
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67d5764f6e | ||
|
|
786eaffb33 | ||
|
|
4940f1212f | ||
|
|
c166f1d186 | ||
|
|
71b3d2b491 | ||
|
|
a5d40f28c8 | ||
|
|
9bfc5555bc | ||
|
|
7eea8ff6d1 |
@@ -4,6 +4,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Knot.Contracts.Conversations.Application.Abstractions;
|
using Knot.Contracts.Conversations.Application.Abstractions;
|
||||||
using Knot.Contracts.Conversations.Domain;
|
using Knot.Contracts.Conversations.Domain;
|
||||||
|
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||||
using Knot.Modules.Conversations.Infrastructure.SignalR;
|
using Knot.Modules.Conversations.Infrastructure.SignalR;
|
||||||
using Knot.Shared.Kernel;
|
using Knot.Shared.Kernel;
|
||||||
using Knot.Shared.Kernel.Storage;
|
using Knot.Shared.Kernel.Storage;
|
||||||
|
|||||||
@@ -194,7 +194,8 @@ const translations = {
|
|||||||
clearChatConfirm: 'Очистить историю чата для себя? Собеседник сохранит свою историю.',
|
clearChatConfirm: 'Очистить историю чата для себя? Собеседник сохранит свою историю.',
|
||||||
clearHistory: 'Очистить историю',
|
clearHistory: 'Очистить историю',
|
||||||
clearHistoryConfirm: 'Очистить историю?',
|
clearHistoryConfirm: 'Очистить историю?',
|
||||||
deleteChatConfirm: 'Удалить чат? Это действие нельзя отменить.',
|
deleteChatConfirm: 'Удалить чат? Это действие нельзя отменить. Чат будет удалён у всех участников.',
|
||||||
|
deleteGroupChatConfirm: 'Удалить чат? Это действие нельзя отменить. Чат будет удалён у всех участников.',
|
||||||
pinChat: 'Закрепить чат',
|
pinChat: 'Закрепить чат',
|
||||||
unpinChat: 'Открепить чат',
|
unpinChat: 'Открепить чат',
|
||||||
chatCleared: 'Очищено',
|
chatCleared: 'Очищено',
|
||||||
@@ -574,7 +575,8 @@ const translations = {
|
|||||||
clearChatConfirm: 'Clear chat history for yourself? The other person will keep their history.',
|
clearChatConfirm: 'Clear chat history for yourself? The other person will keep their history.',
|
||||||
clearHistory: 'Clear history',
|
clearHistory: 'Clear history',
|
||||||
clearHistoryConfirm: 'Clear history?',
|
clearHistoryConfirm: 'Clear history?',
|
||||||
deleteChatConfirm: 'Delete this chat? This action cannot be undone.',
|
deleteChatConfirm: 'Delete this chat? This action cannot be undone. The chat will be removed for all participants.',
|
||||||
|
deleteGroupChatConfirm: 'Delete this chat? This action cannot be undone. The chat will be removed for all participants.',
|
||||||
pinChat: 'Pin chat',
|
pinChat: 'Pin chat',
|
||||||
unpinChat: 'Unpin chat',
|
unpinChat: 'Unpin chat',
|
||||||
chatCleared: 'Chat cleared',
|
chatCleared: 'Chat cleared',
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ export default function ChatPage() {
|
|||||||
const [activeTab, setActiveTab] = useState('chats');
|
const [activeTab, setActiveTab] = useState('chats');
|
||||||
const { t } = useLang();
|
const { t } = useLang();
|
||||||
|
|
||||||
|
const activeChat = useChatStore((state) => state.activeChat);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
groupCallOpenRef.current = groupCallOpen;
|
groupCallOpenRef.current = groupCallOpen;
|
||||||
groupCallChatIdRef.current = groupCallChatId;
|
groupCallChatIdRef.current = groupCallChatId;
|
||||||
@@ -335,6 +337,16 @@ export default function ChatPage() {
|
|||||||
};
|
};
|
||||||
}, [user?.id]);
|
}, [user?.id]);
|
||||||
|
|
||||||
|
// Join chat group when activeChat changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeChat) {
|
||||||
|
const socket = getSocket();
|
||||||
|
if (socket) {
|
||||||
|
socket.emit('join_chat', activeChat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [activeChat]);
|
||||||
|
|
||||||
const handleStartCall = (targetUser: UserBasic, type: 'voice' | 'video') => {
|
const handleStartCall = (targetUser: UserBasic, type: 'voice' | 'video') => {
|
||||||
setCallTarget(targetUser);
|
setCallTarget(targetUser);
|
||||||
setCallType(type);
|
setCallType(type);
|
||||||
@@ -371,8 +383,6 @@ export default function ChatPage() {
|
|||||||
setGroupCallOpen(false);
|
setGroupCallOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeChat = useChatStore((state) => state.activeChat);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
|
|||||||
@@ -98,13 +98,13 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
NotificationStore.useNotificationStore.getState().addNotification('info', (t as any)('searchingHistory') || 'Searching message in history...');
|
NotificationStore.useNotificationStore.getState().addNotification('info', (t as any)('searchingHistory') || 'Searching message in history...');
|
||||||
|
|
||||||
const chatStore = useChatStore.getState();
|
const chatStore = useChatStore.getState();
|
||||||
|
|
||||||
if (sequenceId !== undefined) {
|
if (sequenceId !== undefined) {
|
||||||
await chatStore.jumpToMessage(activeChat, sequenceId);
|
await chatStore.jumpToMessage(activeChat, sequenceId);
|
||||||
// Wait a bit for React to render
|
// Wait a bit for React to render
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!tryScroll()) {
|
if (!tryScroll()) {
|
||||||
NotificationStore.useNotificationStore.getState().addNotification('warning', (t as any)('messageNotFound') || 'Message not found');
|
NotificationStore.useNotificationStore.getState().addNotification('warning', (t as any)('messageNotFound') || 'Message not found');
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
} else {
|
} else {
|
||||||
@@ -137,25 +137,25 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chat?.isImporting || !chat?.importJobId) {
|
if (!chat?.isImporting || !chat?.importJobId) {
|
||||||
setImportStatus(null);
|
setImportStatus(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const poll = async () => {
|
const poll = async () => {
|
||||||
try {
|
try {
|
||||||
const data = await httpClient.request<any>(`/import/telegram/status/${chat.importJobId}`);
|
const data = await httpClient.request<any>(`/import/telegram/status/${chat.importJobId}`);
|
||||||
setImportStatus({ processed: data.processedMessages, total: data.totalMessages, status: data.status });
|
setImportStatus({ processed: data.processedMessages, total: data.totalMessages, status: data.status });
|
||||||
if (data.status === 'Completed' || data.status === 'Failed') {
|
if (data.status === 'Completed' || data.status === 'Failed') {
|
||||||
setImportStatus(null);
|
setImportStatus(null);
|
||||||
loadChats();
|
loadChats();
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
if (e.status === 404) {
|
|
||||||
console.warn('Import job not found');
|
|
||||||
} else {
|
|
||||||
console.error('Failed to poll status', e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.status === 404) {
|
||||||
|
console.warn('Import job not found');
|
||||||
|
} else {
|
||||||
|
console.error('Failed to poll status', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
poll();
|
poll();
|
||||||
@@ -187,12 +187,31 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
// Refs and logic for tracking session's first unread message to show the divider exactly once per load
|
// 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 });
|
const sessionUnreadRef = useRef<{ chatId: string, msgId: string | null }>({ chatId: '', msgId: null });
|
||||||
|
|
||||||
if (activeChat && activeChat !== sessionUnreadRef.current.chatId && !isLoadingMessages) {
|
// Update sessionUnreadRef when chat changes OR when messages are marked as read
|
||||||
const firstUnreadMsg = chatMessages.find(
|
useEffect(() => {
|
||||||
(m) => m.senderId !== user?.id && !m.readBy?.some((r) => r.userId === user?.id)
|
if (!activeChat || isLoadingMessages) return;
|
||||||
);
|
|
||||||
sessionUnreadRef.current = { chatId: activeChat, msgId: firstUnreadMsg ? firstUnreadMsg.id : null };
|
// Reset on chat change
|
||||||
}
|
if (activeChat !== sessionUnreadRef.current.chatId) {
|
||||||
|
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 };
|
||||||
|
} else {
|
||||||
|
// Update if the first unread message was read (msgId no longer exists in unread list)
|
||||||
|
const firstUnreadMsg = chatMessages.find(
|
||||||
|
(m) => m.senderId !== user?.id && !m.readBy?.some((r) => r.userId === user?.id)
|
||||||
|
);
|
||||||
|
if (sessionUnreadRef.current.msgId && !firstUnreadMsg) {
|
||||||
|
// All messages are now read
|
||||||
|
sessionUnreadRef.current.msgId = null;
|
||||||
|
} else if (firstUnreadMsg && sessionUnreadRef.current.msgId !== firstUnreadMsg.id) {
|
||||||
|
// First unread changed (some messages were read)
|
||||||
|
sessionUnreadRef.current.msgId = firstUnreadMsg.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [activeChat, chatMessages, user?.id, isLoadingMessages]);
|
||||||
|
|
||||||
const firstUnreadId = activeChat === sessionUnreadRef.current.chatId ? sessionUnreadRef.current.msgId : null;
|
const firstUnreadId = activeChat === sessionUnreadRef.current.chatId ? sessionUnreadRef.current.msgId : null;
|
||||||
const initialScrollChatId = useRef<string | null>(null);
|
const initialScrollChatId = useRef<string | null>(null);
|
||||||
|
|
||||||
@@ -270,19 +289,19 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
const saveScrollPosition = useCallback((targetChatId?: string) => {
|
const saveScrollPosition = useCallback((targetChatId?: string) => {
|
||||||
const container = scrollContainerRef.current;
|
const container = scrollContainerRef.current;
|
||||||
const chatId = targetChatId || activeChat;
|
const chatId = targetChatId || activeChat;
|
||||||
|
|
||||||
if (!container || !chatId || !scrollReady || isInitializingRef.current || isScrollingToBottomRef.current) return;
|
if (!container || !chatId || !scrollReady || isInitializingRef.current || isScrollingToBottomRef.current) return;
|
||||||
|
|
||||||
// Проверка: сообщения в стейте должны быть от целевого чата
|
// Проверка: сообщения в стейте должны быть от целевого чата
|
||||||
if (chatMessages.length > 0 && chatMessages[0].chatId !== chatId) return;
|
if (chatMessages.length > 0 && chatMessages[0].chatId !== chatId) return;
|
||||||
|
|
||||||
const isAtBottomNow = container.scrollHeight - container.scrollTop - container.clientHeight < 40;
|
const isAtBottomNow = container.scrollHeight - container.scrollTop - container.clientHeight < 40;
|
||||||
isAtBottomRef.current = isAtBottomNow;
|
isAtBottomRef.current = isAtBottomNow;
|
||||||
|
|
||||||
if (isAtBottomNow) {
|
if (isAtBottomNow) {
|
||||||
localStorage.setItem(`chat_at_bottom_${chatId}`, 'true');
|
localStorage.setItem(`chat_at_bottom_${chatId}`, 'true');
|
||||||
localStorage.removeItem(`chat_anchor_${chatId}`);
|
localStorage.removeItem(`chat_anchor_${chatId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageElements = container.querySelectorAll('[data-message-id]');
|
const messageElements = container.querySelectorAll('[data-message-id]');
|
||||||
@@ -304,8 +323,8 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (anchor && anchor.id) {
|
if (anchor && anchor.id) {
|
||||||
localStorage.setItem(`chat_anchor_${chatId}`, JSON.stringify(anchor));
|
localStorage.setItem(`chat_anchor_${chatId}`, JSON.stringify(anchor));
|
||||||
localStorage.removeItem(`chat_at_bottom_${chatId}`);
|
localStorage.removeItem(`chat_at_bottom_${chatId}`);
|
||||||
}
|
}
|
||||||
}, [activeChat, scrollReady, chatMessages]);
|
}, [activeChat, scrollReady, chatMessages]);
|
||||||
|
|
||||||
@@ -313,7 +332,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
const restoreScrollPosition = useCallback(() => {
|
const restoreScrollPosition = useCallback(() => {
|
||||||
const container = scrollContainerRef.current;
|
const container = scrollContainerRef.current;
|
||||||
if (!container || !activeChat) return false;
|
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;
|
if (chatMessages.length === 0) return true;
|
||||||
|
|
||||||
@@ -330,16 +349,16 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
try {
|
try {
|
||||||
const { id, offset } = JSON.parse(saved);
|
const { id, offset } = JSON.parse(saved);
|
||||||
let el = container.querySelector(`[data-message-id="${id}"]`) as HTMLElement;
|
let el = container.querySelector(`[data-message-id="${id}"]`) as HTMLElement;
|
||||||
|
|
||||||
// Поиск ближайшего, если точное сообщение еще не загружено
|
// Поиск ближайшего, если точное сообщение еще не загружено
|
||||||
if (!el) {
|
if (!el) {
|
||||||
const msgIndex = chatMessages.findIndex(m => m.id === id);
|
const msgIndex = chatMessages.findIndex(m => m.id === id);
|
||||||
if (msgIndex !== -1) {
|
if (msgIndex !== -1) {
|
||||||
for (let i = msgIndex; i < chatMessages.length; i++) {
|
for (let i = msgIndex; i < chatMessages.length; i++) {
|
||||||
const nextEl = container.querySelector(`[data-message-id="${chatMessages[i].id}"]`) as HTMLElement;
|
const nextEl = container.querySelector(`[data-message-id="${chatMessages[i].id}"]`) as HTMLElement;
|
||||||
if (nextEl) { el = nextEl; break; }
|
if (nextEl) { el = nextEl; break; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el) {
|
if (el) {
|
||||||
@@ -368,7 +387,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLoadingMessages || !scrollContainerRef.current || !activeChat) return;
|
if (isLoadingMessages || !scrollContainerRef.current || !activeChat) return;
|
||||||
const container = scrollContainerRef.current;
|
const container = scrollContainerRef.current;
|
||||||
|
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
if (isInitializingRef.current) return;
|
if (isInitializingRef.current) return;
|
||||||
|
|
||||||
@@ -405,18 +424,18 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
|
|
||||||
// Сохраняем позицию старого чата
|
// Сохраняем позицию старого чата
|
||||||
if (prevChatIdRef.current && scrollContainerRef.current && scrollReady) {
|
if (prevChatIdRef.current && scrollContainerRef.current && scrollReady) {
|
||||||
saveScrollPosition(prevChatIdRef.current);
|
saveScrollPosition(prevChatIdRef.current);
|
||||||
}
|
}
|
||||||
|
|
||||||
setScrollReady(false);
|
setScrollReady(false);
|
||||||
prevChatIdRef.current = activeChat;
|
prevChatIdRef.current = activeChat;
|
||||||
|
|
||||||
if (scrollContainerRef.current) {
|
if (scrollContainerRef.current) {
|
||||||
scrollContainerRef.current.scrollTop = 0;
|
scrollContainerRef.current.scrollTop = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
isInitializingRef.current = false;
|
isInitializingRef.current = false;
|
||||||
}, 600);
|
}, 600);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
@@ -443,7 +462,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
|
|
||||||
if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);
|
if (scrollTimeoutRef.current) clearTimeout(scrollTimeoutRef.current);
|
||||||
scrollTimeoutRef.current = setTimeout(() => saveScrollPosition(), 150);
|
scrollTimeoutRef.current = setTimeout(() => saveScrollPosition(), 150);
|
||||||
|
|
||||||
const st = container.scrollTop;
|
const st = container.scrollTop;
|
||||||
const isScrollingUp = st < lastScrollTopRef.current;
|
const isScrollingUp = st < lastScrollTopRef.current;
|
||||||
const stChanged = st !== lastScrollTopRef.current;
|
const stChanged = st !== lastScrollTopRef.current;
|
||||||
@@ -473,7 +492,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
|
|
||||||
const containerRect = container.getBoundingClientRect();
|
const containerRect = container.getBoundingClientRect();
|
||||||
const messageElements = container.querySelectorAll('[data-message-id]');
|
const messageElements = container.querySelectorAll('[data-message-id]');
|
||||||
|
|
||||||
let currentTopMsgId = null;
|
let currentTopMsgId = null;
|
||||||
for (const el of messageElements) {
|
for (const el of messageElements) {
|
||||||
const rect = el.getBoundingClientRect();
|
const rect = el.getBoundingClientRect();
|
||||||
@@ -506,15 +525,15 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
// Принудительный сброс режима (Второй Клик)
|
// Принудительный сброс режима (Второй Клик)
|
||||||
localStorage.setItem(`chat_at_bottom_${activeChat}`, 'true');
|
localStorage.setItem(`chat_at_bottom_${activeChat}`, 'true');
|
||||||
localStorage.removeItem(`chat_anchor_${activeChat}`);
|
localStorage.removeItem(`chat_anchor_${activeChat}`);
|
||||||
|
|
||||||
isScrollingToBottomRef.current = true;
|
isScrollingToBottomRef.current = true;
|
||||||
if (scrollContainerRef.current) {
|
if (scrollContainerRef.current) {
|
||||||
scrollContainerRef.current.scrollTo({
|
scrollContainerRef.current.scrollTo({
|
||||||
top: scrollContainerRef.current.scrollHeight,
|
top: scrollContainerRef.current.scrollHeight,
|
||||||
behavior: 'smooth'
|
behavior: 'smooth'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isScrollingToBottomRef.current = false;
|
isScrollingToBottomRef.current = false;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
@@ -573,6 +592,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
{
|
{
|
||||||
root: scrollContainerRef.current,
|
root: scrollContainerRef.current,
|
||||||
threshold: 0.1,
|
threshold: 0.1,
|
||||||
|
rootMargin: '0px',
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -584,7 +604,26 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
unreadElements.forEach((el: Element) => {
|
unreadElements.forEach((el: Element) => {
|
||||||
const id = el.getAttribute('data-message-id');
|
const id = el.getAttribute('data-message-id');
|
||||||
if (id && !sentReadIdsRef.current.has(id)) {
|
if (id && !sentReadIdsRef.current.has(id)) {
|
||||||
observer.observe(el);
|
// Check if element is already visible
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const containerRect = scrollContainerRef.current!.getBoundingClientRect();
|
||||||
|
const isVisible = rect.top >= containerRect.top && rect.bottom <= containerRect.bottom;
|
||||||
|
|
||||||
|
if (isVisible) {
|
||||||
|
// Mark as read immediately without waiting for intersection
|
||||||
|
const seqId = parseInt(el.getAttribute('data-sequence-id') || '0', 10);
|
||||||
|
if (seqId > 0) {
|
||||||
|
socket.emit('read_messages', {
|
||||||
|
chatId: activeChat,
|
||||||
|
lastReadMessageId: id,
|
||||||
|
lastReadSequenceId: seqId,
|
||||||
|
});
|
||||||
|
useChatStore.getState().markRead(activeChat, user.id, seqId);
|
||||||
|
sentReadIdsRef.current.add(id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
observer.observe(el);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -646,7 +685,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
<section className="flex-1 h-full flex flex-col items-center justify-center bg-[#010101] relative overflow-hidden">
|
<section className="flex-1 h-full flex flex-col items-center justify-center bg-[#010101] relative overflow-hidden">
|
||||||
{/* Background Knot Texture */}
|
{/* Background Knot Texture */}
|
||||||
<div className="absolute inset-0 opacity-[0.04] pointer-events-none flex items-center justify-center">
|
<div className="absolute inset-0 opacity-[0.04] pointer-events-none flex items-center justify-center">
|
||||||
<span className="material-symbols-outlined text-white text-[500px] select-none">cloud_download</span>
|
<span className="material-symbols-outlined text-white text-[500px] select-none">cloud_download</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chat Empty State View */}
|
{/* Chat Empty State View */}
|
||||||
@@ -1152,16 +1191,16 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between mb-2 px-1">
|
<div className="flex items-center justify-between mb-2 px-1">
|
||||||
<span className="text-xs font-black uppercase tracking-widest text-primary">
|
<span className="text-xs font-black uppercase tracking-widest text-primary">
|
||||||
{importStatus?.status === 'Processing' ? 'Обработка' :
|
{importStatus?.status === 'Processing' ? 'Обработка' :
|
||||||
importStatus?.status === 'Queued' ? 'В очереди' : 'Загрузка'}
|
importStatus?.status === 'Queued' ? 'В очереди' : 'Загрузка'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs font-black text-on-surface tabular-nums">
|
<span className="text-xs font-black text-on-surface tabular-nums">
|
||||||
{importStatus?.processed || 0} / {importStatus?.total || 0}
|
{importStatus?.processed || 0} / {importStatus?.total || 0}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-3 w-full bg-surface-container-highest rounded-full overflow-hidden border border-outline/10 p-0.5">
|
<div className="h-3 w-full bg-surface-container-highest rounded-full overflow-hidden border border-outline/10 p-0.5">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ width: 0 }}
|
initial={{ width: 0 }}
|
||||||
animate={{ width: `${Math.min(100, Math.round(((importStatus?.processed || 0) / (importStatus?.total || 1)) * 100))}%` }}
|
animate={{ width: `${Math.min(100, Math.round(((importStatus?.processed || 0) / (importStatus?.total || 1)) * 100))}%` }}
|
||||||
transition={{ type: 'spring', damping: 20 }}
|
transition={{ type: 'spring', damping: 20 }}
|
||||||
@@ -1199,9 +1238,9 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
{chatPinnedMessages.length > 1 && (
|
{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">
|
<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) => (
|
{chatPinnedMessages.map((_, idx) => (
|
||||||
<div
|
<div
|
||||||
key={idx}
|
key={idx}
|
||||||
className={`flex-1 transition-colors duration-300 ${idx === pinnedIndex % chatPinnedMessages.length ? 'bg-primary' : 'bg-primary/20'}`}
|
className={`flex-1 transition-colors duration-300 ${idx === pinnedIndex % chatPinnedMessages.length ? 'bg-primary' : 'bg-primary/20'}`}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -1226,8 +1265,8 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
|||||||
{t('pinnedMessage')} {chatPinnedMessages.length > 1 ? `#${(pinnedIndex % chatPinnedMessages.length) + 1}` : ''}
|
{t('pinnedMessage')} {chatPinnedMessages.length > 1 ? `#${(pinnedIndex % chatPinnedMessages.length) + 1}` : ''}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-zinc-300 truncate font-medium">
|
<p className="text-sm text-zinc-300 truncate font-medium">
|
||||||
{chatPinnedMessages[pinnedIndex % chatPinnedMessages.length]?.content ||
|
{chatPinnedMessages[pinnedIndex % chatPinnedMessages.length]?.content ||
|
||||||
(chatPinnedMessages[pinnedIndex % chatPinnedMessages.length]?.media?.length > 0 ? t('media') : '...')}
|
(chatPinnedMessages[pinnedIndex % chatPinnedMessages.length]?.media?.length > 0 ? t('media') : '...')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user