diff --git a/.idea/workspace.sync-conflict-20260318-212257-2SDWFHM.xml b/.idea/workspace.sync-conflict-20260318-212257-2SDWFHM.xml new file mode 100755 index 0000000..895d199 --- /dev/null +++ b/.idea/workspace.sync-conflict-20260318-212257-2SDWFHM.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + { + "lastFilter": { + "state": "OPEN", + "assignee": "lorsanstand" + } +} + { + "selectedUrlAndAccountId": { + "url": "https://github.com/lorsanstand/Aether.git", + "accountId": "7d226d82-cbdd-4f01-a2df-d6a90c41dc0d" + } +} + { + "associatedIndex": 6 +} + + + + + + + { + "keyToString": { + "ModuleVcsDetector.initialDetectionPerformed": "true", + "Python.test.executor": "Run", + "RunOnceActivity.ShowReadmeOnStart": "true", + "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", + "RunOnceActivity.git.unshallow": "true", + "RunOnceActivity.typescript.service.memoryLimit.init": "true", + "ai.playground.ignore.import.keys.banner.in.settings": "true", + "git-widget-placeholder": "dev", + "last_opened_file_path": "/home/lorsan/Projects/Aether", + "nodejs_package_manager_path": "npm", + "settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable", + "vue.rearranger.settings.migration": "true" + } +} + + + + + + + + + + + + + + + + + + + + + + 1766260993924 + + + + + + + + + \ No newline at end of file diff --git a/.idea/workspace.sync-conflict-20260320-201210-2SDWFHM.xml b/.idea/workspace.sync-conflict-20260320-201210-2SDWFHM.xml new file mode 100755 index 0000000..895d199 --- /dev/null +++ b/.idea/workspace.sync-conflict-20260320-201210-2SDWFHM.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + { + "lastFilter": { + "state": "OPEN", + "assignee": "lorsanstand" + } +} + { + "selectedUrlAndAccountId": { + "url": "https://github.com/lorsanstand/Aether.git", + "accountId": "7d226d82-cbdd-4f01-a2df-d6a90c41dc0d" + } +} + { + "associatedIndex": 6 +} + + + + + + + { + "keyToString": { + "ModuleVcsDetector.initialDetectionPerformed": "true", + "Python.test.executor": "Run", + "RunOnceActivity.ShowReadmeOnStart": "true", + "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", + "RunOnceActivity.git.unshallow": "true", + "RunOnceActivity.typescript.service.memoryLimit.init": "true", + "ai.playground.ignore.import.keys.banner.in.settings": "true", + "git-widget-placeholder": "dev", + "last_opened_file_path": "/home/lorsan/Projects/Aether", + "nodejs_package_manager_path": "npm", + "settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable", + "vue.rearranger.settings.migration": "true" + } +} + + + + + + + + + + + + + + + + + + + + + + 1766260993924 + + + + + + + + + \ No newline at end of file diff --git a/.idea/workspace.sync-conflict-20260328-210502-XNSB2YU.xml b/.idea/workspace.sync-conflict-20260328-210502-XNSB2YU.xml new file mode 100755 index 0000000..8fa159c --- /dev/null +++ b/.idea/workspace.sync-conflict-20260328-210502-XNSB2YU.xml @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + { + "lastFilter": { + "state": "OPEN", + "assignee": "lorsanstand" + } +} + { + "selectedUrlAndAccountId": { + "url": "https://github.com/lorsanstand/Aether.git", + "accountId": "7d226d82-cbdd-4f01-a2df-d6a90c41dc0d" + } +} + { + "associatedIndex": 6 +} + + + + + + + { + "keyToString": { + "ModuleVcsDetector.initialDetectionPerformed": "true", + "Python.test.executor": "Run", + "RunOnceActivity.ShowReadmeOnStart": "true", + "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", + "RunOnceActivity.git.unshallow": "true", + "RunOnceActivity.typescript.service.memoryLimit.init": "true", + "ai.playground.ignore.import.keys.banner.in.settings": "true", + "git-widget-placeholder": "dev", + "last_opened_file_path": "/home/lorsan/Projects/Aether", + "nodejs_package_manager_path": "npm", + "settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable", + "vue.rearranger.settings.migration": "true" + } +} + + + + + + + + + + + + + + + + + + + + + + 1766260993924 + + + + + + + + + \ No newline at end of file diff --git a/BUGFIX_SUMMARY.md b/BUGFIX_SUMMARY.md new file mode 100644 index 0000000..dd7f079 --- /dev/null +++ b/BUGFIX_SUMMARY.md @@ -0,0 +1,110 @@ +# Исправление проблемы с отправкой сообщений + +## Проблема +Когда пользователь отправлял сообщение, оно не появлялось в чате ни у отправителя, ни у получателя до перезагрузки страницы. + +## Корневые причины + +### 1. **Backend - Порядок операций** +В файле [backend/app/chats/service.py](backend/app/chats/service.py): +- Сообщения отправлялись через WebSocket **ДО** коммита в БД +- Это могло привести к отправке неполных или незапомненных данных + +**Исправление:** Перемещены вызовы `await session.commit()` **перед** `await cls._send_ws_message()` во всех трех методах: +- `send_message()` - отправка нового сообщения +- `update_message()` - редактирование сообщения +- `delete_message()` - удаление сообщения + +### 2. **Frontend - Отсутствие оптимистичного обновления** +В файле [frontend/src/pages/ChatPage.tsx](frontend/src/pages/ChatPage.tsx): +- Приложение ждало получения сообщения через WebSocket перед показом в UI +- Без WebSocket подписки или задержки сообщение не отображалось + +**Исправление:** Добавлено оптимистичное обновление UI: +- Сообщение добавляется в UI **сразу** после отправки с временным ID (`temp-{timestamp}`) +- При получении подтверждения через WebSocket, временное сообщение заменяется на реальное +- Если отправка завершится с ошибкой, временное сообщение удаляется + +### 3. **Backend - Логирование и обработка ошибок** +В файле [backend/app/services/messenger_service.py](backend/app/services/messenger_service.py): +- Добавлена обработка исключений при отправке WebSocket сообщений +- Улучшено логирование (удален `print("test")`, заменен на `log.info()`) + +## Что изменилось + +### Backend (3 файла) +1. **service.py** - 3 исправления: + - `send_message()`: commit перед WebSocket отправкой + - `update_message()`: commit перед WebSocket отправкой + - `delete_message()`: commit перед WebSocket отправкой + +2. **messenger_service.py** - 2 исправления: + - Добавлен try-catch в `handle_message()` + - Заменено логирование с `print()` на `log.info()` + +### Frontend (1 файл) +1. **ChatPage.tsx** - 2 исправления: + - `handleSendMessage()`: добавлено оптимистичное обновление UI + - `ws.onmessage()`: улучшена логика замены временных сообщений на реальные + +## Поток отправки сообщения (новый) + +``` +1. Пользователь нажимает "Отправить" + ↓ +2. Frontend создает временное сообщение (temp-ID) и добавляет в UI + ↓ +3. Frontend отправляет запрос к backend (/chats/message) + ↓ +4. Backend сохраняет сообщение в БД + ↓ +5. Backend коммитит транзакцию в БД + ↓ +6. Backend отправляет сообщение в Redis (pub/sub) + ↓ +7. Redis PubSub listener отправляет WebSocket всем участникам чата + ↓ +8. Frontend получает сообщение через WebSocket и заменяет temp-сообщение на реальное + ↓ +9. UI обновлена и синхронизирована со всеми участниками +``` + +## Тестирование + +1. **Базовое тестирование:** + - Откройте приложение в 2 браузерах для разных пользователей + - Отправьте сообщение из первого браузера + - Сообщение должно появиться **сразу** в обоих браузерах + - НЕ требуется перезагрузка страницы + +2. **Проверка временных сообщений:** + - В DevTools откройте Console + - Ищите сообщение вида `temp-TIMESTAMP` в начальном списке + - После получения WebSocket ответа, ID должно измениться на UUID + +3. **Проверка обновлений:** + - Отредактируйте сообщение + - Изменение должно появиться сразу у обоих пользователей + +4. **Проверка удаления:** + - Удалите сообщение + - Оно должно исчезнуть сразу у обоих пользователей + +## Возможные проблемы и решения + +### Проблема: Сообщения все еще не появляются +**Решение:** Проверьте: +1. Redis работает (`docker ps | grep redis`) +2. Backend логирует: "Starting Redis PubSub subscriber" +3. WebSocket подключение открыто (`ws.onopen` в Console) +4. Сообщение отправляется: смотрите сетевые запросы в DevTools + +### Проблема: Дублирование сообщений +**Решение:** Может быть, если: +1. Сообщение приходит через WebSocket и добавляется дважды +2. Проверьте логику замены temp-сообщений в `onmessage` + +### Проблема: Сообщение показывается с ошибкой +**Решение:** +1. Проверьте формат данных в backend (логи Redis) +2. Убедитесь, что Message тип совпадает со схемой diff --git a/backend/app/auth/dependencies.py b/backend/app/auth/dependencies.py index 73473a7..a9ed32f 100644 --- a/backend/app/auth/dependencies.py +++ b/backend/app/auth/dependencies.py @@ -51,7 +51,7 @@ async def get_current_superuser(current_user: UserModel = Depends(get_current_us async def get_current_verified_user(current_user: UserModel = Depends(get_current_user)): if not current_user.is_verified: - log.debug("User has not confirmed the email.", extra={"user_id": str(current_user.id)}) - raise HTTPException(status.HTTP_403_FORBIDDEN, detail="verify email") + log.debug("User has not confirmed the email", extra={"user_id": str(current_user.id)}) + raise HTTPException(status.HTTP_403_FORBIDDEN, detail="Verify email") return current_user \ No newline at end of file diff --git a/backend/app/chats/service.py b/backend/app/chats/service.py index 8ee82ba..9689456 100644 --- a/backend/app/chats/service.py +++ b/backend/app/chats/service.py @@ -92,14 +92,16 @@ class ChatService: ) ) - await cls._send_ws_message(members_ids, Message.model_validate(message_db)) - await ChatDAO.update( session, ChatModel.id==target_chat_id, obj_in={"last_message": message.content} ) await session.commit() + + # Send WebSocket message AFTER commit to ensure data is persisted + await cls._send_ws_message(members_ids, Message.model_validate(message_db)) + log.info("Message sent", extra={"message_id": message_db.id, "sender_id": sender.id, "chat_id": target_chat_id}) return message_db @@ -202,9 +204,11 @@ class ChatService: member_ids = [member.user_id for member in members] + await session.commit() + + # Send WebSocket message AFTER commit await cls._send_ws_message(member_ids, Message.model_validate(message_update_db)) - await session.commit() log.info("Message update successfully", extra={"user_id": user.id, "message_id": message_update.id}) return message_update_db @@ -234,8 +238,10 @@ class ChatService: member_ids = [member.user_id for member in members] await MessageDAO.delete(session, MessageModel.id==message_id) - + + await session.commit() + + # Send WebSocket message AFTER commit await cls._delete_ws_message(member_ids, message_id) - await session.commit() log.info("Message delete successfully", extra={"user_id": user.id, "message_id": message_exist.id}) \ No newline at end of file diff --git a/backend/app/services/messenger_service.py b/backend/app/services/messenger_service.py index cd95e4a..13030bf 100644 --- a/backend/app/services/messenger_service.py +++ b/backend/app/services/messenger_service.py @@ -18,7 +18,7 @@ class PubSubMessenger: @classmethod async def subscribe_to_channels(cls): - print("test") + log.info("Starting Redis PubSub subscriber") redis_client = await get_redis() pubsub = redis_client.pubsub() @@ -46,11 +46,14 @@ class PubSubMessenger: @classmethod async def handle_message(cls, payload, ws: WebSocket): log.debug("Message start sending type: %s", payload["type"]) - if payload["type"] == "send": - await ws.send_json(payload["data"]) - elif payload["type"] == "del": - await ws.send_json({"type": "del", "message_id": payload["data"]}) - log.info(f"Message sent to user via WebSocket") + try: + if payload["type"] == "send": + await ws.send_json(payload["data"]) + elif payload["type"] == "del": + await ws.send_json({"type": "del", "message_id": payload["data"]}) + log.info(f"Message sent to user via WebSocket") + except Exception as e: + log.error(f"Error sending WebSocket message: {e}", exc_info=True) diff --git a/frontend/src/pages/ChatPage.tsx b/frontend/src/pages/ChatPage.tsx index 41e1df3..d070c71 100644 --- a/frontend/src/pages/ChatPage.tsx +++ b/frontend/src/pages/ChatPage.tsx @@ -142,38 +142,43 @@ export default function ChatPage() { // Add or update message in current chat if it belongs to it if (selectedChat && message.chat_id === selectedChat.chat_id) { - const isNewMessage = !messages.some(m => m.id === message.id); - setMessages(prev => { - // Check if message already exists (update it if edited) + // Check if this is replacing a temporary message (sent by current user) + const tempMessageIndex = prev.findIndex(m => m.id.startsWith('temp-') && m.sender_id === message.sender_id && m.content === message.content); + + // Check if message already exists const existingIndex = prev.findIndex(m => m.id === message.id); - if (existingIndex !== -1) { - // Update existing message + + if (tempMessageIndex !== -1) { + // Replace temporary message with real one + const updated = [...prev]; + updated[tempMessageIndex] = message; + return updated; + } else if (existingIndex !== -1) { + // Update existing message (edit case) const updated = [...prev]; updated[existingIndex] = message; return updated; + } else { + // Add new message from other user + return [...prev, message]; } - // Add new message - return [...prev, message]; }); // Handle scroll and unread counter for new messages - if (isNewMessage) { - // Check if this is our own message - const isOwnMessage = message.sender_id === user?.id; - - // Use ref to check current position synchronously - setTimeout(() => { - if (isOwnMessage || isAtBottomRef.current) { - // Auto-scroll if it's our message or if at bottom - scrollToBottom(true); - } else { - // Increment unread counter if not at bottom and not our message - setUnreadCount(prev => prev + 1); - setShowScrollButton(true); - } - }, 100); - } + const isOwnMessage = message.sender_id === user?.id; + + // Use ref to check current position synchronously + setTimeout(() => { + if (isOwnMessage || isAtBottomRef.current) { + // Auto-scroll if it's our message or if at bottom + scrollToBottom(true); + } else { + // Increment unread counter if not at bottom and not our message + setUnreadCount(prev => prev + 1); + setShowScrollButton(true); + } + }, 100); } // Update chat list with new last message @@ -354,15 +359,32 @@ export default function ChatPage() { try { setSendingMessage(true); + const messageContent = messageText.trim(); + + // Create a temporary message to show immediately (optimistic update) + const tempMessage: Message = { + id: `temp-${Date.now()}`, // Temporary ID + sender_id: user?.id || 0, + chat_id: selectedChat?.chat_id || '', + content: messageContent, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }; + + // Add message to UI immediately + setMessages(prev => [...prev, tempMessage]); + // Send message with chat_id (если чат существует) или recipient_id (если новый чат) - await chatService.sendMessage({ - content: messageText.trim(), + const sentMessage = await chatService.sendMessage({ + content: messageContent, chat_id: selectedChat?.chat_id, recipient_id: selectedChat?.user_id, }); - // Don't add message locally - it will come via WebSocket - // This prevents duplication + // Replace temporary message with real one from backend + setMessages(prev => + prev.map(m => m.id === tempMessage.id ? sentMessage : m) + ); // Clear input setMessageText(''); @@ -370,7 +392,7 @@ export default function ChatPage() { // Update chat list with new last message const updatedChats = chats.map(chat => chat.chat_id === selectedChat?.chat_id - ? { ...chat, last_message: messageText.trim() } + ? { ...chat, last_message: messageContent } : chat ); setChats(updatedChats); @@ -378,11 +400,18 @@ export default function ChatPage() { // Update cache for specific chat if (selectedChat?.chat_id) { - updateChatCache(selectedChat.chat_id, { last_message: messageText.trim() }); + updateChatCache(selectedChat.chat_id, { last_message: messageContent }); } + + // Auto-scroll to bottom + setTimeout(() => { + scrollToBottom(true); + }, 100); } catch (err: any) { console.error('Failed to send message:', err); alert('Не удалось отправить сообщение'); + // Remove temporary message if sending failed + setMessages(prev => prev.filter(m => !m.id.startsWith('temp-'))); } finally { setSendingMessage(false); // Keep input focused after everything is done