Conversation
- Implemented a new FastAPI endpoint for retrieving system version information at /api/v2/system/version. - Created ConversationReadCursor model to track the last read message for users in conversations. - Added version_info service to resolve server version details including git commit and build timestamp. - Introduced read_state_service_v2 to manage read cursor functionality and relay read events. - Developed typing_service_v2 to handle typing indicators and relay typing events. - Added integration tests for typing and read cursor functionalities, ensuring proper event fanout and access control. - Created database migration for conversation_read_cursors table.
There was a problem hiding this comment.
Pull request overview
This PR advances the Blackwire v0.3 wave by formalizing /api/v2 as the primary surface and adding real-time UX features (typing + read cursors + version visibility) across server, API spec, and the Qt client.
Changes:
- Add
/api/v2typing indicators and persistent read cursors with WS fanout + federation relays (plus DB migration/model). - Add
/api/v2/system/versionendpoint and client/server version display in the Qt client. - Update docs/site to
v0.3, plus significant Qt client UX/theme and attachment/markdown rendering updates.
Reviewed changes
Copilot reviewed 49 out of 49 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| spec/api.md | Updates API spec to /api/v2 primary, documents typing/read/version + federation endpoints |
| server/app/services/typing_service_v2.py | Implements typing publish + federation relay fanout |
| server/app/services/read_state_service_v2.py | Implements read-cursor persistence, fanout, federation relay |
| server/migrations/versions/20260301_0009_conversation_read_cursors.py | Adds conversation_read_cursors table + indexes |
| server/app/services/version_info.py | Resolves version metadata for /system/version |
| server/app/api_v2/conversations.py | Adds REST endpoints for typing + read cursor write/read |
| server/app/api_v2/federation.py | Adds federation relay endpoints for typing/read |
| server/app/api_v2/system.py | Adds /api/v2/system/version route |
| server/app/config.py | Enables v0.3 defaults + adds typing/read settings/rate limits |
| server/tests/integration/test_v2_typing_read_version.py | Integration coverage for typing/read/version + federation relay plumbing |
| client-cpp-gui/src/** + include/** | Adds typing/read handling, version fetch/display, attachment lifecycle, markdown/media rendering, and UI refreshes |
| client-cpp-gui/src/ui/theme.cpp + CMakeLists.txt | Theme redesign + Qt MultimediaWidgets dependency + versioning |
| client-cpp-gui/src/ui/theme.cpp.bak | Adds an unintended backup file (should be removed) |
| V0.3_Update.md / README.md / index.html | Updates release docs/site to v0.3 and describes new capabilities |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if last_read_sent_at_ms < existing.last_read_sent_at_ms: | ||
| return existing, False | ||
| if ( | ||
| last_read_sent_at_ms == existing.last_read_sent_at_ms | ||
| and last_read_message_id == existing.last_read_message_id | ||
| ): | ||
| return existing, False | ||
|
|
||
| existing.last_read_message_id = last_read_message_id | ||
| existing.last_read_sent_at_ms = last_read_sent_at_ms | ||
| existing.user_address = user_address | ||
| existing.updated_at = datetime.now(UTC) |
There was a problem hiding this comment.
_upsert_cursor treats updates with the same last_read_sent_at_ms but a different last_read_message_id as a forward move and overwrites the cursor. This violates the documented monotonicity rule (equal timestamps are not strictly monotonic) and can allow cursor “flip-flopping” if multiple messages share the same sent_at_ms (client-controlled). Consider treating last_read_sent_at_ms == existing.last_read_sent_at_ms as a no-op unless the message id is identical, or enforcing a deterministic tie-break (e.g., compare the message’s server ordering/created_at).
| QObject::connect(body, &QLabel::linkActivated, body, [](const QString& url) { | ||
| const auto answer = QMessageBox::question( | ||
| nullptr, | ||
| "Open Link", | ||
| QString("Do you want to open this link?\n\n%1").arg(url), | ||
| QMessageBox::Yes | QMessageBox::No, | ||
| QMessageBox::No); | ||
| if (answer == QMessageBox::Yes) { | ||
| QDesktopServices::openUrl(QUrl(url)); | ||
| } |
There was a problem hiding this comment.
The markdown link handler opens whatever URL Qt parses via QDesktopServices::openUrl(QUrl(url)). Even with a confirmation dialog, this can still launch potentially unsafe schemes (e.g., file:, custom handlers). Consider validating QUrl(url) and whitelisting allowed schemes (typically http/https, and possibly mailto if desired) before enabling the open action.
| if (media_kind == "image") { | ||
| QPixmap image_preview; | ||
| image_preview.loadFromData(file_bytes); | ||
| if (!image_preview.isNull()) { | ||
| auto* image_label = new QLabel(bubble); | ||
| image_label->setObjectName("messageImage"); | ||
| image_label->setPixmap( | ||
| image_preview.scaledToWidth(320, Qt::SmoothTransformation)); | ||
| image_label->setTextInteractionFlags(Qt::TextSelectableByMouse); | ||
| bubble_layout->addWidget(image_label); | ||
| } |
There was a problem hiding this comment.
Inline image preview decodes and loads attachment bytes directly into a QPixmap without any size/type guard. A malicious (or just very large) attachment could cause excessive memory use or UI hangs. Consider bounding preview generation (e.g., skip preview above a max byte size and/or validate the image dimensions before scaling).
| #include "blackwire/ui/theme.hpp" | ||
|
|
||
| #include <QApplication> | ||
| #include <QColor> | ||
| #include <QPalette> | ||
|
|
||
| namespace blackwire { | ||
|
|
||
| void ApplyAppTheme(QApplication& app) { | ||
| QPalette palette; | ||
| palette.setColor(QPalette::Window, QColor("#1e1f22")); | ||
| palette.setColor(QPalette::WindowText, QColor("#f2f3f5")); | ||
| palette.setColor(QPalette::Base, QColor("#2b2d31")); | ||
| palette.setColor(QPalette::AlternateBase, QColor("#313338")); | ||
| palette.setColor(QPalette::ToolTipBase, QColor("#313338")); | ||
| palette.setColor(QPalette::ToolTipText, QColor("#f2f3f5")); | ||
| palette.setColor(QPalette::Text, QColor("#f2f3f5")); | ||
| palette.setColor(QPalette::Button, QColor("#313338")); | ||
| palette.setColor(QPalette::ButtonText, QColor("#f2f3f5")); | ||
| palette.setColor(QPalette::BrightText, QColor("#ffffff")); |
There was a problem hiding this comment.
client-cpp-gui/src/ui/theme.cpp.bak appears to be a backup artifact committed alongside the real theme.cpp. This adds ~500 lines of duplicate code and increases repo noise/build times. It should be removed from the PR (and ideally added to .gitignore for *.bak).
| - Default: OFF (messages cleared on client restart, privacy-first) | ||
| - When enabled: encrypted plaintext cache persists across restarts | ||
| - When disabled: `local_messages` stripped at save time, runtime session intact | ||
| - Respects user privacy choice — no message history leak on shared devices |
There was a problem hiding this comment.
This update doc says the message cache toggle default is OFF, but the implementation defaults SocialPreferences::save_message_cache to true (and unit test SaveMessageCacheDefaultsToTrue asserts that default). Please align the documentation with the actual default (or change the default in code if OFF is intended).
| - Default: OFF (messages cleared on client restart, privacy-first) | |
| - When enabled: encrypted plaintext cache persists across restarts | |
| - When disabled: `local_messages` stripped at save time, runtime session intact | |
| - Respects user privacy choice — no message history leak on shared devices | |
| - Default: ON (messages persist across client restarts by default) | |
| - When enabled: encrypted plaintext cache persists across restarts | |
| - When disabled: `local_messages` stripped at save time, runtime session intact | |
| - Respects user privacy choice — users can disable local history to avoid message leaks on shared devices |
No description provided.