Правка уведомления
This commit is contained in:
@@ -194,7 +194,8 @@ const translations = {
|
||||
clearChatConfirm: 'Очистить историю чата для себя? Собеседник сохранит свою историю.',
|
||||
clearHistory: 'Очистить историю',
|
||||
clearHistoryConfirm: 'Очистить историю?',
|
||||
deleteChatConfirm: 'Удалить чат? Это действие нельзя отменить.',
|
||||
deleteChatConfirm: 'Удалить чат? Это действие нельзя отменить. Чат будет удалён у всех участников.',
|
||||
deleteGroupChatConfirm: 'Удалить чат? Это действие нельзя отменить. Чат будет удалён у всех участников.',
|
||||
pinChat: 'Закрепить чат',
|
||||
unpinChat: 'Открепить чат',
|
||||
chatCleared: 'Очищено',
|
||||
@@ -575,6 +576,7 @@ const translations = {
|
||||
clearHistory: 'Clear history',
|
||||
clearHistoryConfirm: 'Clear history?',
|
||||
deleteChatConfirm: 'Delete this chat? This action cannot be undone.',
|
||||
deleteGroupChatConfirm: 'Delete this chat? This action cannot be undone. The chat will be removed for all participants.',
|
||||
pinChat: 'Pin chat',
|
||||
unpinChat: 'Unpin chat',
|
||||
chatCleared: 'Chat cleared',
|
||||
|
||||
@@ -283,7 +283,11 @@ function ChatListItem({ chat, isActive }: ChatListItemProps) {
|
||||
|
||||
<ConfirmModal
|
||||
open={showDeleteConfirm}
|
||||
message={isFavorites ? t('clearHistoryConfirm') : t('deleteChatConfirm')}
|
||||
message={
|
||||
isFavorites ? t('clearHistoryConfirm') :
|
||||
chat.type === 'group' ? t('deleteGroupChatConfirm') :
|
||||
t('deleteChatConfirm')
|
||||
}
|
||||
onConfirm={confirmDelete}
|
||||
onCancel={() => setShowDeleteConfirm(false)}
|
||||
/>
|
||||
|
||||
@@ -98,13 +98,13 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
NotificationStore.useNotificationStore.getState().addNotification('info', (t as any)('searchingHistory') || 'Searching message in history...');
|
||||
|
||||
const chatStore = useChatStore.getState();
|
||||
|
||||
|
||||
if (sequenceId !== undefined) {
|
||||
await chatStore.jumpToMessage(activeChat, sequenceId);
|
||||
// Wait a bit for React to render
|
||||
setTimeout(() => {
|
||||
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);
|
||||
} else {
|
||||
@@ -137,25 +137,25 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
|
||||
useEffect(() => {
|
||||
if (!chat?.isImporting || !chat?.importJobId) {
|
||||
setImportStatus(null);
|
||||
return;
|
||||
setImportStatus(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const poll = async () => {
|
||||
try {
|
||||
const data = await httpClient.request<any>(`/import/telegram/status/${chat.importJobId}`);
|
||||
setImportStatus({ processed: data.processedMessages, total: data.totalMessages, status: data.status });
|
||||
if (data.status === 'Completed' || data.status === 'Failed') {
|
||||
setImportStatus(null);
|
||||
loadChats();
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.status === 404) {
|
||||
console.warn('Import job not found');
|
||||
} else {
|
||||
console.error('Failed to poll status', e);
|
||||
}
|
||||
try {
|
||||
const data = await httpClient.request<any>(`/import/telegram/status/${chat.importJobId}`);
|
||||
setImportStatus({ processed: data.processedMessages, total: data.totalMessages, status: data.status });
|
||||
if (data.status === 'Completed' || data.status === 'Failed') {
|
||||
setImportStatus(null);
|
||||
loadChats();
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.status === 404) {
|
||||
console.warn('Import job not found');
|
||||
} else {
|
||||
console.error('Failed to poll status', e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
poll();
|
||||
@@ -270,19 +270,19 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
const saveScrollPosition = useCallback((targetChatId?: string) => {
|
||||
const container = scrollContainerRef.current;
|
||||
const chatId = targetChatId || activeChat;
|
||||
|
||||
|
||||
if (!container || !chatId || !scrollReady || isInitializingRef.current || isScrollingToBottomRef.current) return;
|
||||
|
||||
// Проверка: сообщения в стейте должны быть от целевого чата
|
||||
if (chatMessages.length > 0 && chatMessages[0].chatId !== chatId) return;
|
||||
|
||||
|
||||
const isAtBottomNow = container.scrollHeight - container.scrollTop - container.clientHeight < 40;
|
||||
isAtBottomRef.current = isAtBottomNow;
|
||||
|
||||
|
||||
if (isAtBottomNow) {
|
||||
localStorage.setItem(`chat_at_bottom_${chatId}`, 'true');
|
||||
localStorage.removeItem(`chat_anchor_${chatId}`);
|
||||
return;
|
||||
localStorage.setItem(`chat_at_bottom_${chatId}`, 'true');
|
||||
localStorage.removeItem(`chat_anchor_${chatId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const messageElements = container.querySelectorAll('[data-message-id]');
|
||||
@@ -304,8 +304,8 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
}
|
||||
|
||||
if (anchor && anchor.id) {
|
||||
localStorage.setItem(`chat_anchor_${chatId}`, JSON.stringify(anchor));
|
||||
localStorage.removeItem(`chat_at_bottom_${chatId}`);
|
||||
localStorage.setItem(`chat_anchor_${chatId}`, JSON.stringify(anchor));
|
||||
localStorage.removeItem(`chat_at_bottom_${chatId}`);
|
||||
}
|
||||
}, [activeChat, scrollReady, chatMessages]);
|
||||
|
||||
@@ -313,7 +313,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
const restoreScrollPosition = useCallback(() => {
|
||||
const container = scrollContainerRef.current;
|
||||
if (!container || !activeChat) return false;
|
||||
|
||||
|
||||
if (chatMessages.length > 0 && chatMessages[0].chatId !== activeChat) return false;
|
||||
if (chatMessages.length === 0) return true;
|
||||
|
||||
@@ -330,16 +330,16 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
try {
|
||||
const { id, offset } = JSON.parse(saved);
|
||||
let el = container.querySelector(`[data-message-id="${id}"]`) as HTMLElement;
|
||||
|
||||
|
||||
// Поиск ближайшего, если точное сообщение еще не загружено
|
||||
if (!el) {
|
||||
const msgIndex = chatMessages.findIndex(m => m.id === id);
|
||||
if (msgIndex !== -1) {
|
||||
for (let i = msgIndex; i < chatMessages.length; i++) {
|
||||
const nextEl = container.querySelector(`[data-message-id="${chatMessages[i].id}"]`) as HTMLElement;
|
||||
if (nextEl) { el = nextEl; break; }
|
||||
}
|
||||
}
|
||||
const msgIndex = chatMessages.findIndex(m => m.id === id);
|
||||
if (msgIndex !== -1) {
|
||||
for (let i = msgIndex; i < chatMessages.length; i++) {
|
||||
const nextEl = container.querySelector(`[data-message-id="${chatMessages[i].id}"]`) as HTMLElement;
|
||||
if (nextEl) { el = nextEl; break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (el) {
|
||||
@@ -368,7 +368,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
useEffect(() => {
|
||||
if (isLoadingMessages || !scrollContainerRef.current || !activeChat) return;
|
||||
const container = scrollContainerRef.current;
|
||||
|
||||
|
||||
const observer = new ResizeObserver(() => {
|
||||
if (isInitializingRef.current) return;
|
||||
|
||||
@@ -405,18 +405,18 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
|
||||
// Сохраняем позицию старого чата
|
||||
if (prevChatIdRef.current && scrollContainerRef.current && scrollReady) {
|
||||
saveScrollPosition(prevChatIdRef.current);
|
||||
saveScrollPosition(prevChatIdRef.current);
|
||||
}
|
||||
|
||||
|
||||
setScrollReady(false);
|
||||
prevChatIdRef.current = activeChat;
|
||||
|
||||
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTop = 0;
|
||||
scrollContainerRef.current.scrollTop = 0;
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
isInitializingRef.current = false;
|
||||
isInitializingRef.current = false;
|
||||
}, 600);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
@@ -443,7 +443,7 @@ 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;
|
||||
const stChanged = st !== lastScrollTopRef.current;
|
||||
@@ -473,7 +473,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const messageElements = container.querySelectorAll('[data-message-id]');
|
||||
|
||||
|
||||
let currentTopMsgId = null;
|
||||
for (const el of messageElements) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
@@ -506,15 +506,15 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
// Принудительный сброс режима (Второй Клик)
|
||||
localStorage.setItem(`chat_at_bottom_${activeChat}`, 'true');
|
||||
localStorage.removeItem(`chat_anchor_${activeChat}`);
|
||||
|
||||
|
||||
isScrollingToBottomRef.current = true;
|
||||
if (scrollContainerRef.current) {
|
||||
scrollContainerRef.current.scrollTo({
|
||||
top: scrollContainerRef.current.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
scrollContainerRef.current.scrollTo({
|
||||
top: scrollContainerRef.current.scrollHeight,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
setTimeout(() => {
|
||||
isScrollingToBottomRef.current = false;
|
||||
}, 1000);
|
||||
@@ -646,7 +646,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">
|
||||
{/* Background Knot Texture */}
|
||||
<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>
|
||||
|
||||
{/* Chat Empty State View */}
|
||||
@@ -1060,7 +1060,7 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
setShowTopMenu(false);
|
||||
if (activeChat) {
|
||||
setConfirmAction({
|
||||
message: t('deleteChatConfirm'),
|
||||
message: chat.type === 'group' ? t('deleteGroupChatConfirm') : t('deleteChatConfirm'),
|
||||
action: async () => {
|
||||
try {
|
||||
await ChatApi.deleteChat(activeChat);
|
||||
@@ -1152,16 +1152,16 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between mb-2 px-1">
|
||||
<span className="text-xs font-black uppercase tracking-widest text-primary">
|
||||
{importStatus?.status === 'Processing' ? 'Обработка' :
|
||||
importStatus?.status === 'Queued' ? 'В очереди' : 'Загрузка'}
|
||||
{importStatus?.status === 'Processing' ? 'Обработка' :
|
||||
importStatus?.status === 'Queued' ? 'В очереди' : 'Загрузка'}
|
||||
</span>
|
||||
<span className="text-xs font-black text-on-surface tabular-nums">
|
||||
{importStatus?.processed || 0} / {importStatus?.total || 0}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<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 }}
|
||||
animate={{ width: `${Math.min(100, Math.round(((importStatus?.processed || 0) / (importStatus?.total || 1)) * 100))}%` }}
|
||||
transition={{ type: 'spring', damping: 20 }}
|
||||
@@ -1199,9 +1199,9 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
{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
|
||||
key={idx}
|
||||
className={`flex-1 transition-colors duration-300 ${idx === pinnedIndex % chatPinnedMessages.length ? 'bg-primary' : 'bg-primary/20'}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -1226,8 +1226,8 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
{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') : '...')}
|
||||
{chatPinnedMessages[pinnedIndex % chatPinnedMessages.length]?.content ||
|
||||
(chatPinnedMessages[pinnedIndex % chatPinnedMessages.length]?.media?.length > 0 ? t('media') : '...')}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user