Skip to content

Latest commit

 

History

History
302 lines (252 loc) · 17.4 KB

File metadata and controls

302 lines (252 loc) · 17.4 KB

The Three Laws

  1. Don't be stupid
  2. Don't be evil
  3. RAM is expensive - think twice before increasing its usage

TeleMatrix — Agent Guide

TeleMatrix is a Matrix messenger that wears Telegram Desktop's skin: a C++20 / Qt 6 Widgets front end (a partial, pixel-oriented clone of the tdesktop UI) over a Rust back end built on matrix-rust-sdk, linked in as a static library across a small C FFI.

Guiding principle: when implementing any UI task, port the Telegram Desktop implementation as-is. Don't reinvent painting logic, layout math, or style values — copy them. Only approximate when a direct port hits a hard wall (Qt6 API drift, lib_ui abstractions we don't have). See Porting from tdesktop.

macOS is the primary, only-actively-tested platform. Windows (MSVC/NSIS) and Linux (DEB/RPM) build + packaging paths exist and must stay alive and portable — do not treat the non-macOS #[cfg]/#ifdef branches as dead code.


Read this first — hard constraints

These trip up every new agent. Internalize them before touching anything.

  • Compile-check Rust with cargo build --profile dev-ffi (from rust/) — the same profile CMake's Debug build uses, so it shares a warm, already-built target tree instead of compiling a separate one like a bare cargo build would.
  • Comments are terse. Comment only genuinely non-obvious / hard-won logic. No obvious or narrating comments. Match the density of the surrounding code.
  • Commit only when asked. Don't commit or push unless the user requests it.
  • Background / network features need an explicit opt-out setting.
  • Use st:: constants (in src/styles/style_constants.h) for every color, size, and font. Never hardcode visual values.
  • clangd errors about QHash/QImage/QString templates are include-path noise — ignore them; trust the actual cmake --build result.

Architecture

C++ Qt 6 app (src/)
├── app/       AppController (intro↔main transition), AppMainWindow, AppMainWidget (2-panel)
├── intro/     Login flow: IntroWidget → IntroStart → IntroLogin (+ verification, recovery-key)
├── dialogs/   Chat list: DialogsWidget → DialogsInner → DialogsRow + dialogs_layout (custom paint);
│              folders management boxes
├── history/   Timeline: HistoryWidget → ScrollArea → HistoryList (custom paint) + HistoryInput
│              (composer); history_message.cpp = bubble paint helpers; media/ = viewers,
│              inline video; reactions/pinned/jump affordances
├── media/     Media pipeline glue (thumbnails, video streaming client)
├── protocol/  ProtocolBridge (QObject wrapping the Rust FFI) + flat C++ structs
├── settings/  Settings pages (appearance, notifications, sessions, …)
├── storage/   Local app storage glue
├── styles/    style_constants.h (all st:: colors/fonts/sizes), style_*.h per component
├── theme/     Light/dark theme parsing + palette
├── ui/        TeleMatrix's own Qt widget layer (RpWidget, ScrollArea, FlatLabel, InputField,
│              layers/, text/, widgets/, toast, qr_code_image). Ported/adapted from tdesktop's
│              lib_ui but on plain Qt signals/slots — NO rpl streams.
└── window/    Top-level window plumbing

Rust back end (rust/src/) — service-oriented, ~60 modules. Highlights:
├── ffi.rs, protocol.rs, matrix.rs, types.rs   FFI surface, ProtocolClient, SDK wiring, shared structs
├── lib.rs                                      crate root / runtime
├── Sync & session   sliding_sync_service, sync_loop_service, session_lifecycle_service,
│                     session_storage_service, session_task_service, auth_service, account_service
├── Rooms list       room_summary_service, room_list_service, unread_count_service,
│                     folder_service / room_folders, presence_typing_service
├── Timeline         timeline_service, timeline_window(_service), timeline_cache_service,
│                     timeline_update_service, timeline_conversion_service, timeline_navigation_service
├── Media            media_stream/ (loopback streaming proxy), media_transfer_service,
│                     media_cache_service, media_blob_store, video_thumbnail_service,
│                     upload_* (limit/progress/tasks)
├── Messaging        message_action_service, room_action_service, room_creation_service,
│                     room_invite_service, room_member_service, recent_emoji(_service)
├── E2EE & secrets   encryption_service, verification_service, keychain.rs, secret_vault.rs,
│                     encrypted_sqlite, store_guard, session_storage_service
├── Search           search_index, search_service, search_backfill (local encrypted FTS)
├── Notifications    notification_service, notification_settings_service
└── Previews         preview_service/_store/_fetch_signal, link_preview_rules

FFI header: generated by the standalone `rust/cbindgen_runner` crate (CMake runs it via
`cargo run`, always regenerating) into build/generated/protocol/protocol_ffi.h. It is
deliberately NOT a build.rs job (that would skip regeneration whenever the crate is
up to date), and there is no committed shadow header. `rust/build.rs` exists but does
one unrelated thing: on macOS it emits an rpath to Qt's libav for cargo-linked test
binaries.

Data flow. Rust services run on a tokio runtime. Results cross the C ABI as flat Ffi* structs; ProtocolBridge converts them to Qt types and emits Qt signals on the main thread. C++ never calls the SDK directly — everything goes through tm_* functions.

Custom painting everywhere. Chat-list rows and message bubbles paint with QPainter directly (no per-item QML/widgets), mirroring tdesktop's paint-level approach.


Build & run

Prerequisites and exact versions live in BUILDING.md. Common pins: Qt 6.10.1, rustc 1.96.0 (pinned in rust-toolchain.toml), matrix-sdk 0.18. The Rust toolchain channel MUST stay in sync with the dtolnay/rust-toolchain refs in .github/workflows/.

There is exactly ONE build tree: build/. On macOS it must resolve official Qt, not Homebrew's — Homebrew ships no libffmpegmediaplugin.dylib, so video cannot stream. Configure hard-fails otherwise.

# Configure (also builds the Rust lib + FFI header). The Qt prefix is needed only
# on the first configure of a fresh tree; CMake caches it.
cmake -B build -DCMAKE_PREFIX_PATH="$HOME/Qt/6.10.1/macos"
cmake --build build --target TeleMatrix -j      # build the app
open build/TeleMatrix.app                        # run (macOS)
  • CMake drives the Rust build: Debugcargo build --profile dev-ffi; Release (and unspecified) → cargo build --release. Both set panic = "abort" so a Rust panic can never unwind across the FFI boundary (that's UB). dev-ffi is a debuggable dev build that also aborts.
  • macOS universal builds compile per-arch and lipo the staticlibs together. macOS libav is Qt's (universal, LGPL), not Homebrew's — headers vendored in third_party/ffmpeg-7.1, .pc files generated by CMake, sonames asserted at configure time.
  • To compile-check just the Rust side without CMake: cd rust && cargo build --profile dev-ffi. On macOS export PKG_CONFIG_PATH="$PWD/../build/ffmpeg-pkgconfig" first (see BUILDING.md).

Testing

Two suites, both real. The app and the test runners share one telematrix_core OBJECT library (so tests link the exact objects the app ships).

  • C++ (Qt Test): one tst_*.cpp per unit under tests/, wired in tests/CMakeLists.txt.
    cmake --build build -j && ctest --test-dir build --output-on-failure
  • Rust: in-crate #[cfg(test)] unit tests plus integration tests in rust/src/integration_tests/ (services are pub(crate)). These drive a mock homeserver via matrix-sdk's testing utilities (MatrixMockServer + wiremock), enabled only as a dev-dependency — the shipped staticlib never compiles testing. cargo test uses the plain unwinding dev profile.

CI (.github/workflows/) runs both. rustfmt/clippy are advisory (the tree isn't warning-clean). Keep new code warning-free regardless.


Conventions

  • C++20, tdesktop style: tabs to indent, camelCase methods, _member private fields, kCamelCase constants. Files follow tdesktop names (dialogs_layout.cpp, history_message.cpp).
  • Prefer auto (auto, const auto, const auto &) over spelling out deduced types; rely on clear names, matching upstream style.
  • Rust: standard rustfmt. Keep the service-per-file structure; don't dump new logic into matrix.rs.
  • No new dependencies unless strictly necessary (both Cargo and CMake).
  • Terse comments (see hard constraints). Explain why, only when non-obvious.
  • Commits: one logical change each. Prefixes in use: perf, feat, fix, and area scopes like (rust), (media). Commit only when asked.
  • Localization: UI strings are Qt .ts/.qm. en is the source fallback (its .qm has zero translations → shows the tr() literal); es is the only real target. Add strings via tr() and regenerate with the lupdate/lrelease targets.

Porting from Telegram Desktop

Telegram Desktop is the primary reference for all UI work. Read the upstream source and copy paint/layout/style directly rather than inventing it. When upstream uses lib_ui APIs we don't have, adapt to plain Qt:

  • rpl::… streams → QObject::connect signals/slots
  • scrollTopValue()QScrollBar::valueChanged
  • moveToRight(x, y)move(parent->width() - x - width(), y)

References (not vendored in this repo — consult the upstream repos):

  • Telegram Desktophttps://github.com/telegramdesktop/tdesktop, under Telegram/SourceFiles/.
  • desktop-app libs — vendored as submodules in lib/ (lib_ui, lib_base, lib_rpl, lib_crl), used for select resources (e.g. lib_ui/fonts) and as the port source for src/ui/. Ports like the connecting-state radial animation come from lib_ui.

Key upstream files by component (paths under Telegram/SourceFiles/):

Component tdesktop path
Chat-list painting dialogs/ui/dialogs_layout.cpp
Chat-list styles dialogs/dialogs.style
Message bubbles history/view/history_view_message.cpp
Message list history/view/history_view_list_widget.cpp
Chat styles ui/chat/chat.style
Corner / jump-down buttons history/view/history_view_corner_buttons.cpp, ui/controls/jump_down_button.cpp
Login screens intro/intro_widget.cpp
Color palette ui/colors.palette
Icons Telegram/Resources/icons/

Reading tdesktop source — systems to translate

Upstream is written against three systems we don't use. Recognize each and map it to our equivalent while porting; don't carry the upstream form into our code.

  • RPL (reactive streams) → Qt signals/slots. A rpl::producer<T> is a stream of T over time. You'll see pipelines built with transforms (rpl::map, rpl::filter), combiners (rpl::combine — the lambda receives the values unpacked; rpl::merge — same type), and sources (rpl::single, rpl::duplicate to reuse a producer). They're consumed by a starterrpl::on_next (also on_error / on_done) — bound to an rpl::lifetime that owns the subscription. To port: a subscription becomes a QObject::connect to a signal, the transform/filter becomes logic in the slot, and lifetime becomes object ownership (the connection dies with the receiver). Common concrete mappings: scrollTopValue()QScrollBar::valueChanged; a value-producing getter → a getter plus a somethingChanged() signal. src/ui/ is deliberately plain Qt — never introduce rpl.
  • tr:: localization → Qt tr(). Upstream keys live in lang.strings and are read as tr::lng_key(...): pass tr::now as the first arg for an immediate QString (tr::lng_key(tr::now, lt_tag, value)), or omit it for a reactive rpl::producer<QString>. Plurals use {count} with | tr::to_count(); rich text uses projectors (tr::marked, tr::rich for **bold**/__italic__, tr::bold, tr::italic, tr::link, tr::url). To port: add a Qt tr() literal to the .ts/.qm catalog (en fallback, es target) and regenerate with the lupdate/lrelease targets; do placeholders and formatting the Qt way. Ignore the reactive (rpl) variants — resolve the string where it's used.
  • .style files → st:: constants. Upstream defines visuals in .style files (e.g. dialogs/dialogs.style, ui/chat/chat.style, palette in ui/colors.palette) with typed fields: pixels (10px), color, icon{{ "path/stem", color }} (multi-part icons layer bottom-up), margins(t,r,b,l), size, point, align, font(14px semibold), and inheritance Foo: Bar(base) { … }. The reason it's data, not code: px values auto-scale at non-100% interface scale — raw integer literals in code do not. To port: move every such value into st:: constants in src/styles/style_constants.h and reference st::…; never bake a dimension, color, or font into a .cpp.

Gotchas & hard-won lessons

Distilled from past regressions — re-learning these costs hours.

  • E2EE decryption is fragile and SDK-reactive.
    • client.event_cache().subscribe() at session start is mandatory — without it, decryption is silently dead on every backend. Don't remove it.
    • Decryption is driven by the SDK's own retry (R2D2 / timeline retry). Do not reintroduce a custom redecryption loop — a previous one fought the SDK and caused UTDs. The decryption glow is per-item from UtdCause; backup access is the precondition.
    • Scrutinize crypto changes in "refactor" commits — past refactors silently regressed decryption (OneShot backup strategy, generation-gated redecryption).
  • All secrets flow through keychain.rs. It's the single choke point behind a SecretBackend enum: platform keychain (macOS Keychain / Windows Cred Manager / Linux Secret Service) or a master-password vault (secret_vault.rs, Argon2id + XChaCha20) — a user-selectable choice on every platform (first-run intro step + Settings), no longer a Linux-only fallback. Optional biometric unlock (Touch ID / Windows Hello) wraps the vault key into a vault_biometric.bin sidecar via src/app/platform/biometric_auth.*; that sidecar is dropped whenever the vault key rotates or is cleared. Don't reach around it. A constructor that gates on an unreachable keychain must not wipe the session.
  • FFI panic safety. Release + dev-ffi abort on panic by design. tm_destroy must drain num_alive_tasks() == 0 before shutdown_timeout — otherwise a deadpool JoinError::Cancelled panics across the boundary (fatal under panic=abort). Don't "simplify" that drain away. One-shot FFI callbacks tied to a data struct are deferred to avoid a teardown UAF.
  • Rooms-list previews: extract_event_body (in room_summary_service.rs) is ALSO the notification gate — making an event type "previewable" there makes it notify. Room-list-only preview logic belongs in extract_last_event. Perf LRU can evict the fallback timeline cache, so blank previews are stickied (preserve_preview_if_blank / merge_sticky_previews).
  • Perf posture: resident timeline windows are LRU-capped; per-room state is released on room close; paint invalidation is region-narrowed. Hover affordances (reply/reaction pills) hang outside the row rect — narrow repaint regions must include affordanceDirtyRegion() or the pills ghost.
  • Notifications are per-event (Rust → FFI → C++ signal, gated on origin() == Sync + push-rule eval). Room invites are a separate path. Mute uses raw push-rule PUTs (the SDK API 413s on DELETE) and must re-enable the rule explicitly.
  • Forwarding must reuse the original MediaSource (file/key/iv) — rebuilding ::plain from the mxc string breaks encrypted media.
  • Video streams through a Rust 127.0.0.1 loopback proxy (HTTP Range + on-the-fly seekable AES-CTR decrypt), not full-download; it stops on logout. Transient cold-cache read timeouts look like corruption but aren't — they auto-retry.
  • Logout renames stores into .trash/ (atomic) then deletes off-path; a startup sweep reclaims leftovers. The _loggingOut latch must reset on login/restore.

Adding files

  • C++: create .h/.cpp under the right src/ subdir and add both to the telematrix_core source list in CMakeLists.txt (the app + tests both link it). Q_OBJECT types are moc'd automatically by CMAKE_AUTOMOC. Testable helpers belong in telematrix_core, not the app-only source list, so a tst_*.cpp can link them.
  • Rust: add a new service.rs module (one responsibility per file) and mod it in lib.rs; expose new capability across the FFI via a tm_* function in ffi.rs.

What NOT to do

  • Don't hardcode colors/sizes/fonts — use st:: constants.
  • Don't use raw Qt widgets where a src/ui/ equivalent exists.
  • Don't reinvent painting/layout — port from tdesktop.
  • Don't break compilation to "clean up later."
  • Don't add Telegram-specific concepts that Matrix doesn't have (MTProto, channels, bots, premium, stories).
  • Don't modify lib/ submodule files.
  • Don't reintroduce a custom E2EE redecryption loop, or remove the event-cache subscribe.