fix: send and recive message

This commit is contained in:
2026-05-09 10:16:15 +03:00
parent cbd764f76b
commit 0e7709c95e
8 changed files with 624 additions and 42 deletions
+142
View File
@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="ca182f62-efdb-4f7c-b4fa-3db0a3244740" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Dockerfile" />
<option value="Python Script" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="GitHubPullRequestSearchHistory">{
&quot;lastFilter&quot;: {
&quot;state&quot;: &quot;OPEN&quot;,
&quot;assignee&quot;: &quot;lorsanstand&quot;
}
}</component>
<component name="GithubPullRequestsUISettings">{
&quot;selectedUrlAndAccountId&quot;: {
&quot;url&quot;: &quot;https://github.com/lorsanstand/Aether.git&quot;,
&quot;accountId&quot;: &quot;7d226d82-cbdd-4f01-a2df-d6a90c41dc0d&quot;
}
}</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 6
}</component>
<component name="ProjectId" id="377jRIu8LdWxRZUqlIlKecvcH0N" />
<component name="ProjectLevelVcsManager">
<ConfirmationsSetting value="2" id="Add" />
</component>
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;Python.test.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;,
&quot;ai.playground.ignore.import.keys.banner.in.settings&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;dev&quot;,
&quot;last_opened_file_path&quot;: &quot;/home/lorsan/Projects/Aether&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$" />
<recent name="$PROJECT_DIR$/frontend" />
<recent name="$PROJECT_DIR$/backend/scripts" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$" />
<recent name="$PROJECT_DIR$/backend/app" />
<recent name="$PROJECT_DIR$/backend" />
</key>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-PY-253.30387.173" />
<option value="bundled-python-sdk-4762d8aabb82-6d6dccd035ac-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-253.30387.173" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="ca182f62-efdb-4f7c-b4fa-3db0a3244740" name="Changes" comment="" />
<created>1766260993924</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1766260993924</updated>
<workItem from="1766579069494" duration="537000" />
<workItem from="1766821371454" duration="6586000" />
<workItem from="1767182789092" duration="61000" />
<workItem from="1767436737099" duration="457000" />
<workItem from="1767469948303" duration="344000" />
<workItem from="1767515834391" duration="3673000" />
<workItem from="1767632206964" duration="1009000" />
<workItem from="1767702075617" duration="5995000" />
<workItem from="1767728758580" duration="1701000" />
<workItem from="1767777556289" duration="2238000" />
<workItem from="1767894161585" duration="3168000" />
<workItem from="1767954037387" duration="3206000" />
<workItem from="1768041344558" duration="24517000" />
<workItem from="1768145545609" duration="2734000" />
<workItem from="1768152718327" duration="3439000" />
<workItem from="1768206631384" duration="12898000" />
<workItem from="1768234411796" duration="7183000" />
<workItem from="1768724458835" duration="4856000" />
<workItem from="1768823389064" duration="12094000" />
<workItem from="1768917847698" duration="5107000" />
<workItem from="1769105143116" duration="384000" />
<workItem from="1769175659580" duration="701000" />
<workItem from="1769249378321" duration="1583000" />
<workItem from="1769255439265" duration="6053000" />
<workItem from="1769323001360" duration="8679000" />
<workItem from="1769413076919" duration="20372000" />
<workItem from="1769440193935" duration="43000" />
<workItem from="1769440272013" duration="8398000" />
<workItem from="1769526024191" duration="778000" />
<workItem from="1769612969196" duration="30000" />
<workItem from="1769614076335" duration="1957000" />
<workItem from="1769794458540" duration="138000" />
<workItem from="1769845775151" duration="151000" />
<workItem from="1769845934187" duration="9000" />
<workItem from="1770183147191" duration="612000" />
<workItem from="1770212893901" duration="4778000" />
<workItem from="1770364636251" duration="7167000" />
<workItem from="1770412285849" duration="16000" />
<workItem from="1773344252513" duration="761000" />
<workItem from="1773416003664" duration="3382000" />
<workItem from="1773515737128" duration="9276000" />
<workItem from="1773642657359" duration="2326000" />
<workItem from="1773648107686" duration="1727000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/Aether$test.coverage" NAME="test Coverage Results" MODIFIED="1767705414318" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/backend/app" />
</component>
</project>
+142
View File
@@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="ca182f62-efdb-4f7c-b4fa-3db0a3244740" name="Changes" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Dockerfile" />
<option value="Python Script" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="GitHubPullRequestSearchHistory">{
&quot;lastFilter&quot;: {
&quot;state&quot;: &quot;OPEN&quot;,
&quot;assignee&quot;: &quot;lorsanstand&quot;
}
}</component>
<component name="GithubPullRequestsUISettings">{
&quot;selectedUrlAndAccountId&quot;: {
&quot;url&quot;: &quot;https://github.com/lorsanstand/Aether.git&quot;,
&quot;accountId&quot;: &quot;7d226d82-cbdd-4f01-a2df-d6a90c41dc0d&quot;
}
}</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 6
}</component>
<component name="ProjectId" id="377jRIu8LdWxRZUqlIlKecvcH0N" />
<component name="ProjectLevelVcsManager">
<ConfirmationsSetting value="2" id="Add" />
</component>
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;Python.test.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;,
&quot;ai.playground.ignore.import.keys.banner.in.settings&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;dev&quot;,
&quot;last_opened_file_path&quot;: &quot;/home/lorsan/Projects/Aether&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$" />
<recent name="$PROJECT_DIR$/frontend" />
<recent name="$PROJECT_DIR$/backend/scripts" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$" />
<recent name="$PROJECT_DIR$/backend/app" />
<recent name="$PROJECT_DIR$/backend" />
</key>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-PY-253.30387.173" />
<option value="bundled-python-sdk-4762d8aabb82-6d6dccd035ac-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-253.30387.173" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="ca182f62-efdb-4f7c-b4fa-3db0a3244740" name="Changes" comment="" />
<created>1766260993924</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1766260993924</updated>
<workItem from="1766579069494" duration="537000" />
<workItem from="1766821371454" duration="6586000" />
<workItem from="1767182789092" duration="61000" />
<workItem from="1767436737099" duration="457000" />
<workItem from="1767469948303" duration="344000" />
<workItem from="1767515834391" duration="3673000" />
<workItem from="1767632206964" duration="1009000" />
<workItem from="1767702075617" duration="5995000" />
<workItem from="1767728758580" duration="1701000" />
<workItem from="1767777556289" duration="2238000" />
<workItem from="1767894161585" duration="3168000" />
<workItem from="1767954037387" duration="3206000" />
<workItem from="1768041344558" duration="24517000" />
<workItem from="1768145545609" duration="2734000" />
<workItem from="1768152718327" duration="3439000" />
<workItem from="1768206631384" duration="12898000" />
<workItem from="1768234411796" duration="7183000" />
<workItem from="1768724458835" duration="4856000" />
<workItem from="1768823389064" duration="12094000" />
<workItem from="1768917847698" duration="5107000" />
<workItem from="1769105143116" duration="384000" />
<workItem from="1769175659580" duration="701000" />
<workItem from="1769249378321" duration="1583000" />
<workItem from="1769255439265" duration="6053000" />
<workItem from="1769323001360" duration="8679000" />
<workItem from="1769413076919" duration="20372000" />
<workItem from="1769440193935" duration="43000" />
<workItem from="1769440272013" duration="8398000" />
<workItem from="1769526024191" duration="778000" />
<workItem from="1769612969196" duration="30000" />
<workItem from="1769614076335" duration="1957000" />
<workItem from="1769794458540" duration="138000" />
<workItem from="1769845775151" duration="151000" />
<workItem from="1769845934187" duration="9000" />
<workItem from="1770183147191" duration="612000" />
<workItem from="1770212893901" duration="4778000" />
<workItem from="1770364636251" duration="7167000" />
<workItem from="1770412285849" duration="16000" />
<workItem from="1773344252513" duration="761000" />
<workItem from="1773416003664" duration="3382000" />
<workItem from="1773515737128" duration="9276000" />
<workItem from="1773642657359" duration="2326000" />
<workItem from="1773648107686" duration="1727000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/Aether$test.coverage" NAME="test Coverage Results" MODIFIED="1767705414318" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/backend/app" />
</component>
</project>
+150
View File
@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="ca182f62-efdb-4f7c-b4fa-3db0a3244740" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/backend/app/auth/dependencies.py" beforeDir="false" afterPath="$PROJECT_DIR$/backend/app/auth/dependencies.py" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Dockerfile" />
<option value="Python Script" />
</list>
</option>
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="GitHubPullRequestSearchHistory">{
&quot;lastFilter&quot;: {
&quot;state&quot;: &quot;OPEN&quot;,
&quot;assignee&quot;: &quot;lorsanstand&quot;
}
}</component>
<component name="GithubPullRequestsUISettings">{
&quot;selectedUrlAndAccountId&quot;: {
&quot;url&quot;: &quot;https://github.com/lorsanstand/Aether.git&quot;,
&quot;accountId&quot;: &quot;7d226d82-cbdd-4f01-a2df-d6a90c41dc0d&quot;
}
}</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 6
}</component>
<component name="ProjectId" id="377jRIu8LdWxRZUqlIlKecvcH0N" />
<component name="ProjectLevelVcsManager">
<ConfirmationsSetting value="2" id="Add" />
</component>
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;Python.test.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;RunOnceActivity.typescript.service.memoryLimit.init&quot;: &quot;true&quot;,
&quot;ai.playground.ignore.import.keys.banner.in.settings&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;dev&quot;,
&quot;last_opened_file_path&quot;: &quot;/home/lorsan/Projects/Aether&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}</component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$" />
<recent name="$PROJECT_DIR$/frontend" />
<recent name="$PROJECT_DIR$/backend/scripts" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$" />
<recent name="$PROJECT_DIR$/backend/app" />
<recent name="$PROJECT_DIR$/backend" />
</key>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-PY-253.29346.142" />
<option value="bundled-python-sdk-f2b7a9f6281b-6e1f45a539f7-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-253.29346.142" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="ca182f62-efdb-4f7c-b4fa-3db0a3244740" name="Changes" comment="" />
<created>1766260993924</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1766260993924</updated>
<workItem from="1766579069494" duration="537000" />
<workItem from="1766821371454" duration="6586000" />
<workItem from="1767182789092" duration="61000" />
<workItem from="1767436737099" duration="457000" />
<workItem from="1767469948303" duration="344000" />
<workItem from="1767515834391" duration="3673000" />
<workItem from="1767632206964" duration="1009000" />
<workItem from="1767702075617" duration="5995000" />
<workItem from="1767728758580" duration="1701000" />
<workItem from="1767777556289" duration="2238000" />
<workItem from="1767894161585" duration="3168000" />
<workItem from="1767954037387" duration="3206000" />
<workItem from="1768041344558" duration="24517000" />
<workItem from="1768145545609" duration="2734000" />
<workItem from="1768152718327" duration="3439000" />
<workItem from="1768206631384" duration="12898000" />
<workItem from="1768234411796" duration="7183000" />
<workItem from="1768724458835" duration="4856000" />
<workItem from="1768823389064" duration="12094000" />
<workItem from="1768917847698" duration="5107000" />
<workItem from="1769105143116" duration="384000" />
<workItem from="1769175659580" duration="701000" />
<workItem from="1769249378321" duration="1583000" />
<workItem from="1769255439265" duration="6053000" />
<workItem from="1769323001360" duration="8679000" />
<workItem from="1769413076919" duration="20372000" />
<workItem from="1769440193935" duration="43000" />
<workItem from="1769440272013" duration="8398000" />
<workItem from="1769526024191" duration="778000" />
<workItem from="1769612969196" duration="30000" />
<workItem from="1769614076335" duration="1957000" />
<workItem from="1769794458540" duration="138000" />
<workItem from="1769845775151" duration="151000" />
<workItem from="1769845934187" duration="9000" />
<workItem from="1770183147191" duration="612000" />
<workItem from="1770212893901" duration="4778000" />
<workItem from="1770364636251" duration="7167000" />
<workItem from="1770412285849" duration="16000" />
<workItem from="1773344252513" duration="761000" />
<workItem from="1773416003664" duration="3382000" />
<workItem from="1773515737128" duration="14371000" />
<workItem from="1773683889945" duration="3252000" />
<workItem from="1774037786590" duration="99000" />
<workItem from="1774086898413" duration="1732000" />
<workItem from="1774170820260" duration="64000" />
<workItem from="1774170890907" duration="3675000" />
<workItem from="1774246130820" duration="1316000" />
<workItem from="1774247842084" duration="3728000" />
<workItem from="1774623135340" duration="10000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/Aether$test.coverage" NAME="test Coverage Results" MODIFIED="1767705414318" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/backend/app" />
</component>
</project>
+110
View File
@@ -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 тип совпадает со схемой
+2 -2
View File
@@ -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
+11 -5
View File
@@ -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})
+9 -6
View File
@@ -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)
+58 -29
View File
@@ -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