diff --git a/backend/bots/mainBot.js b/backend/bots/mainBot.js
index 7c528d5..ddd73e4 100644
--- a/backend/bots/mainBot.js
+++ b/backend/bots/mainBot.js
@@ -197,9 +197,20 @@ const pollUpdates = async () => {
// Продолжить опрос
setTimeout(poll, 1000);
} catch (error) {
- logError('Ошибка опроса Telegram для основного бота', error);
- // Переподключиться через 5 секунд
- setTimeout(poll, 5000);
+ const errorData = error.response?.data || {};
+ const errorCode = errorData.error_code;
+ const errorDescription = errorData.description || error.message;
+
+ // Обработка конфликта 409 - другой экземпляр бота уже опрашивает
+ if (errorCode === 409) {
+ // Не логируем 409 - это ожидаемая ситуация при конфликте экземпляров
+ // Подождать дольше перед повторной попыткой
+ setTimeout(poll, 10000);
+ } else {
+ logError('Ошибка опроса Telegram для основного бота', error);
+ // Переподключиться через 5 секунд
+ setTimeout(poll, 5000);
+ }
}
};
diff --git a/backend/bots/serverMonitor.js b/backend/bots/serverMonitor.js
index 1b25c18..9569788 100644
--- a/backend/bots/serverMonitor.js
+++ b/backend/bots/serverMonitor.js
@@ -310,12 +310,17 @@ const processUpdate = async (update) => {
const pollUpdates = async () => {
if (!TELEGRAM_API) return;
- while (isPolling) {
+ const poll = async () => {
+ if (!isPolling) {
+ return;
+ }
+
try {
const response = await axios.get(`${TELEGRAM_API}/getUpdates`, {
params: {
timeout: 25,
- offset
+ offset,
+ allowed_updates: ['message']
}
});
@@ -324,13 +329,35 @@ const pollUpdates = async () => {
offset = update.update_id + 1;
await processUpdate(update);
}
+
+ // Продолжить опрос
+ setTimeout(poll, 100);
} catch (error) {
- log('error', 'Ошибка опроса Telegram для модераторского бота', {
- error: error.response?.data || error.message
- });
- await new Promise((resolve) => setTimeout(resolve, 5000));
+ const errorData = error.response?.data || {};
+ const errorCode = errorData.error_code;
+ const errorDescription = errorData.description || error.message;
+
+ // Обработка конфликта 409 - другой экземпляр бота уже опрашивает
+ if (errorCode === 409) {
+ // Не логируем 409 - это ожидаемая ситуация при конфликте экземпляров
+ // Подождать дольше перед повторной попыткой
+ await new Promise((resolve) => setTimeout(resolve, 10000));
+ } else {
+ log('error', 'Ошибка опроса Telegram для модераторского бота', {
+ error: errorData || error.message
+ });
+ // Обычная задержка при других ошибках
+ await new Promise((resolve) => setTimeout(resolve, 5000));
+ }
+
+ // Продолжить опрос только если isPolling все еще true
+ if (isPolling) {
+ setTimeout(poll, 0);
+ }
}
- }
+ };
+
+ poll();
};
const startServerMonitorBot = () => {
@@ -340,13 +367,40 @@ const startServerMonitorBot = () => {
}
if (isPolling) {
+ log('warn', 'Модераторский бот уже запущен, пропускаем повторный запуск');
return;
}
+ // Инициализировать offset перед началом опроса
+ const initializeOffset = async () => {
+ try {
+ const response = await axios.get(`${TELEGRAM_API}/getUpdates`, {
+ params: {
+ timeout: 1,
+ allowed_updates: ['message']
+ }
+ });
+
+ const updates = response.data?.result || [];
+ if (updates.length > 0) {
+ offset = updates[updates.length - 1].update_id + 1;
+ log('info', `Модераторский бот: пропущено ${updates.length} старых обновлений, offset установлен на ${offset}`);
+ }
+ } catch (error) {
+ log('warn', 'Не удалось инициализировать offset для модераторского бота, начнем с 0', {
+ error: error.response?.data || error.message
+ });
+ }
+ };
+
isPolling = true;
log('info', 'Модераторский Telegram бот запущен');
- pollUpdates().catch((error) => {
+
+ initializeOffset().then(() => {
+ pollUpdates();
+ }).catch((error) => {
log('error', 'Не удалось запустить модераторский бот', { error: error.message });
+ isPolling = false;
});
};
diff --git a/frontend/src/components/CommentsModal.jsx b/frontend/src/components/CommentsModal.jsx
index 9fa295a..f61b12f 100644
--- a/frontend/src/components/CommentsModal.jsx
+++ b/frontend/src/components/CommentsModal.jsx
@@ -7,6 +7,7 @@ import { decodeHtmlEntities } from '../utils/htmlEntities'
import './CommentsModal.css'
export default function CommentsModal({ post, onClose, onUpdate }) {
+ // ВСЕ хуки должны вызываться всегда, до любых условных возвратов
const [comment, setComment] = useState('')
const [loading, setLoading] = useState(false)
const [comments, setComments] = useState([])
@@ -14,59 +15,47 @@ export default function CommentsModal({ post, onClose, onUpdate }) {
const [loadingPost, setLoadingPost] = useState(false)
// Загрузить полные данные поста с комментариями
- const loadFullPost = useCallback(async (postId) => {
- if (!postId) {
+ useEffect(() => {
+ if (!post || !post._id) {
return
}
- try {
- setLoadingPost(true)
- // Загрузить посты с фильтром по автору поста для оптимизации
- // Если это не помогает, загружаем больше постов
- const authorId = post?.author?._id || post?.author
- const response = authorId
- ? await getPosts({ userId: authorId, limit: 100 })
- : await getPosts({ limit: 200 })
-
- const foundPost = response.posts?.find(p => p._id === postId)
- if (foundPost) {
- // Проверяем, что комментарии populate'ены с авторами
- const commentsWithAuthors = (foundPost.comments || []).filter(c => {
- return c && c.author && (typeof c.author === 'object')
- })
- setComments(commentsWithAuthors)
- setFullPost(foundPost)
- } else {
- // Если не нашли, используем переданные данные
- const commentsWithAuthors = (post.comments || []).filter(c => {
- return c && c.author && (typeof c.author === 'object')
- })
- setComments(commentsWithAuthors)
+ // Сначала установим переданные данные
+ setFullPost(post)
+ const initialComments = (post.comments || []).filter(c => {
+ return c && c.author && (typeof c.author === 'object')
+ })
+ setComments(initialComments)
+
+ // Затем загрузим полные данные для обновления
+ const loadFullPost = async () => {
+ try {
+ setLoadingPost(true)
+ // Загрузить посты с фильтром по автору поста для оптимизации
+ const authorId = post?.author?._id || post?.author
+ const response = authorId
+ ? await getPosts({ userId: authorId, limit: 100 })
+ : await getPosts({ limit: 200 })
+
+ const foundPost = response.posts?.find(p => p._id === post._id)
+ if (foundPost) {
+ // Проверяем, что комментарии populate'ены с авторами
+ const commentsWithAuthors = (foundPost.comments || []).filter(c => {
+ return c && c.author && (typeof c.author === 'object')
+ })
+ setComments(commentsWithAuthors)
+ setFullPost(foundPost)
+ }
+ } catch (error) {
+ console.error('[CommentsModal] Ошибка загрузки поста:', error)
+ // Оставляем переданные данные
+ } finally {
+ setLoadingPost(false)
}
- } catch (error) {
- console.error('[CommentsModal] Ошибка загрузки поста:', error)
- // Fallback на переданные данные
- const commentsWithAuthors = (post.comments || []).filter(c => {
- return c && c.author && (typeof c.author === 'object')
- })
- setComments(commentsWithAuthors)
- } finally {
- setLoadingPost(false)
}
- }, [post?.author])
-
- useEffect(() => {
- if (post && post._id) {
- // Сначала установим переданные данные
- setFullPost(post)
- const initialComments = (post.comments || []).filter(c => {
- return c && c.author && (typeof c.author === 'object')
- })
- setComments(initialComments)
- // Затем загрузим полные данные для обновления
- loadFullPost(post._id)
- }
- }, [post?._id, loadFullPost])
+
+ loadFullPost()
+ }, [post?._id]) // Только ID поста в зависимостях
// Проверка на существование поста ПОСЛЕ хуков
if (!post) {
@@ -102,9 +91,8 @@ export default function CommentsModal({ post, onClose, onUpdate }) {
hapticFeedback('success')
// Обновить данные поста для синхронизации (но не блокируем UI)
- loadFullPost(post._id).catch(err => {
- console.error('[CommentsModal] Ошибка при обновлении после добавления:', err)
- })
+ // Перезагружаем через useEffect, который сработает при изменении post._id
+ // Но так как post._id не меняется, просто обновим локально
if (onUpdate) {
onUpdate()
@@ -112,17 +100,6 @@ export default function CommentsModal({ post, onClose, onUpdate }) {
} else {
console.error('[CommentsModal] Неожиданный формат ответа:', result)
hapticFeedback('error')
- // Попробуем перезагрузить комментарии
- await loadFullPost(post._id)
- }
- } catch (error) {
- console.error('[CommentsModal] Ошибка добавления комментария:', error)
- hapticFeedback('error')
- // Попробуем перезагрузить комментарии в случае ошибки
- try {
- await loadFullPost(post._id)
- } catch (reloadError) {
- console.error('[CommentsModal] Ошибка при перезагрузке:', reloadError)
}
} finally {
setLoading(false)
diff --git a/frontend/src/components/FollowListModal.css b/frontend/src/components/FollowListModal.css
index 11f79fa..b9b1bf3 100644
--- a/frontend/src/components/FollowListModal.css
+++ b/frontend/src/components/FollowListModal.css
@@ -103,17 +103,17 @@
}
.user-item-wrapper {
- padding: 4px 12px;
+ padding: 3px 10px;
}
.user-item {
display: flex;
align-items: center;
- gap: 10px;
+ gap: 8px;
cursor: pointer;
transition: background 0.2s;
- padding: 6px;
- border-radius: 8px;
+ padding: 5px;
+ border-radius: 6px;
position: relative;
}
@@ -122,8 +122,8 @@
}
.user-avatar {
- width: 32px;
- height: 32px;
+ width: 26px;
+ height: 26px;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
@@ -138,7 +138,7 @@
}
.user-name {
- font-size: 14px;
+ font-size: 12px;
font-weight: 600;
color: var(--text-primary);
line-height: 1.3;
@@ -148,7 +148,7 @@
}
.user-username {
- font-size: 12px;
+ font-size: 15px;
color: var(--text-secondary);
line-height: 1.3;
white-space: nowrap;
@@ -158,8 +158,8 @@
/* Follow Button Icon */
.follow-btn-icon {
- width: 32px;
- height: 32px;
+ width: 26px;
+ height: 26px;
border-radius: 50%;
background: var(--bg-primary);
color: var(--text-primary);
diff --git a/frontend/src/components/FollowListModal.jsx b/frontend/src/components/FollowListModal.jsx
index e91bcba..1dab6eb 100644
--- a/frontend/src/components/FollowListModal.jsx
+++ b/frontend/src/components/FollowListModal.jsx
@@ -112,9 +112,9 @@ export default function FollowListModal({ users, title, onClose, currentUser })
onClick={(e) => handleFollowToggle(user._id, e)}
>
{isFollowing ? (
-
+
) : (
-
+
)}
)}