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
+
+
+ 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
+
+
+ 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
+
+
+ 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