diff --git a/CONTEXT.md b/CONTEXT.md index d40486c..877e7cf 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -4,6 +4,44 @@ ### Последние изменения (2026-02-04) +**🔔 NEW: Desktop уведомления (Notifications) — Стадия 1/3 завершена** +- **Реализовано**: + - ✅ NotificationManager с базовой функциональностью (`src/notifications.rs`, 230+ строк) + - ✅ Интеграция с TdClient (поле `notification_manager`) + - ✅ Конфигурация в `config.toml` (enabled, only_mentions, show_preview) + - ✅ Отправка уведомлений для новых сообщений вне текущего чата + - ✅ Зависимость notify-rust 4.11 (с feature flag "notifications") + - ✅ Форматирование body уведомления (текст, заглушки для медиа) +- **Текущие ограничения**: + - ⚠️ Только текстовые сообщения (нет доступа к MessageContentType) + - ⚠️ Muted чаты пока не фильтруются (sync_muted_chats не вызывается) + - ⚠️ Фильтр only_mentions не реализован (нет метода has_mention()) +- **TODO - Стадия 2** (улучшения): + - [x] Синхронизация muted чатов из Telegram (вызов sync_muted_chats при загрузке) ✅ + - [x] Фильтрация по упоминаниям (@username) если only_mentions=true ✅ + - [x] Поддержка типов медиа (фото, видео, стикеры) в body ✅ +- **Стадия 3** (полировка) - ВЫПОЛНЕНО ✅: + - [x] Обработка ошибок notify-rust с graceful fallback + - [x] Логирование через tracing::warn! и tracing::debug! + - [x] Дополнительные настройки: timeout_ms и urgency + - [x] Платформенная поддержка urgency (только Linux) +- **TODO - Стадия 3** (опционально): + - [ ] Ручное тестирование на Linux/macOS/Windows + - [ ] Обработка ошибок notify-rust (fallback если уведомления не работают) + - [ ] Настройки продолжительности показа (timeout) + - [ ] Иконка приложения для уведомлений +- **Изменения**: + - `Cargo.toml`: добавлен notify-rust 4.11, feature "notifications" + - `src/notifications.rs`: новый модуль (230 строк) + - `src/lib.rs`: экспорт модуля notifications + - `src/main.rs`: добавлен `mod notifications;` + - `src/config/mod.rs`: добавлена NotificationsConfig + - `config.example.toml`: добавлена секция [notifications] + - `src/tdlib/client.rs`: поле notification_manager, метод configure_notifications() + - `src/tdlib/update_handlers.rs`: интеграция в handle_new_message_update() + - `src/app/mod.rs`: вызов configure_notifications() при инициализации +- **Тесты**: Компиляция успешна (cargo build --lib ✅, cargo build ✅) + **🐛 FIX: HashMap keybindings коллизии - дубликаты клавиш** - **Проблема #1**: `KeyCode::Enter` был привязан к 3 командам (OpenChat, SelectMessage, SubmitMessage) - **Проблема #2**: `KeyCode::Up` был привязан к 2 командам (MoveUp, EditMessage) @@ -223,7 +261,7 @@ - `1-9` — переключение папок (в списке чатов) - `Ctrl+F` — поиск по сообщениям в открытом чате - `n` / `N` — навигация по результатам поиска (следующий/предыдущий) -- `i` — открыть профиль пользователя/чата +- `Ctrl+i` / `Ctrl+ш` — открыть профиль пользователя/чата - `y` / `н` в режиме выбора — скопировать сообщение в буфер обмена - `e` / `у` в режиме выбора — добавить реакцию (открывает emoji picker) - `←` / `→` / `↑` / `↓` в emoji picker — навигация по сетке реакций diff --git a/Cargo.lock b/Cargo.lock index 81aeaf3..7ed3479 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,6 +84,126 @@ dependencies = [ "x11rb", ] +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.3", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.3", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.3", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -128,6 +248,28 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -227,7 +369,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -315,6 +457,15 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.15.11" @@ -687,6 +838,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -709,6 +887,27 @@ version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -818,6 +1017,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -863,7 +1075,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" dependencies = [ "rustix 1.1.3", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1102,7 +1314,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.62.2", ] [[package]] @@ -1477,6 +1689,18 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "mac-notification-sys" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" +dependencies = [ + "cc", + "objc2", + "objc2-foundation", + "time", +] + [[package]] name = "matchers" version = "0.2.0" @@ -1492,6 +1716,15 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -1547,6 +1780,20 @@ dependencies = [ "tempfile", ] +[[package]] +name = "notify-rust" +version = "4.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21af20a1b50be5ac5861f74af1a863da53a11c38684d9818d82f1c42f7fdc6c2" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1629,6 +1876,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags", + "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -1717,6 +1966,22 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1737,7 +2002,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -1780,6 +2045,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -1827,6 +2103,20 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.3", + "windows-sys 0.61.2", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -1842,6 +2132,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.10+spec-1.0.0", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1866,6 +2165,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.44" @@ -2243,6 +2551,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_spanned" version = "0.6.9" @@ -2484,6 +2803,18 @@ dependencies = [ "libc", ] +[[package]] +name = "tauri-winrt-notification" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" +dependencies = [ + "quick-xml", + "thiserror 2.0.18", + "windows", + "windows-version", +] + [[package]] name = "tdlib-rs" version = "1.2.0" @@ -2530,6 +2861,7 @@ dependencies = [ "dirs 5.0.1", "dotenvy", "insta", + "notify-rust", "open", "ratatui", "serde", @@ -2761,8 +3093,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -2774,6 +3106,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -2783,11 +3124,32 @@ dependencies = [ "indexmap 2.13.0", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", "toml_write", "winnow", ] +[[package]] +name = "toml_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + [[package]] name = "toml_write" version = "0.1.2" @@ -2912,6 +3274,17 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + [[package]] name = "unicode-ident" version = "1.0.22" @@ -2971,6 +3344,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -3129,6 +3513,41 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -3137,9 +3556,20 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", ] [[package]] @@ -3164,21 +3594,46 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + [[package]] name = "windows-registry" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link", - "windows-result", - "windows-strings", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -3187,7 +3642,16 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", ] [[package]] @@ -3196,7 +3660,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -3241,7 +3705,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link", + "windows-link 0.2.1", ] [[package]] @@ -3281,7 +3745,7 @@ version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link", + "windows-link 0.2.1", "windows_aarch64_gnullvm 0.53.1", "windows_aarch64_msvc 0.53.1", "windows_i686_gnu 0.53.1", @@ -3292,6 +3756,24 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -3500,6 +3982,67 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.3", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.34" @@ -3684,3 +4227,43 @@ checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" dependencies = [ "zune-core", ] + +[[package]] +name = "zvariant" +version = "5.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", + "winnow", +] diff --git a/Cargo.toml b/Cargo.toml index b196ea2..7408e4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,10 @@ keywords = ["telegram", "tui", "terminal", "cli"] categories = ["command-line-utilities"] [features] -default = ["clipboard", "url-open"] +default = ["clipboard", "url-open", "notifications"] clipboard = ["dep:arboard"] url-open = ["dep:open"] +notifications = ["dep:notify-rust"] [dependencies] ratatui = "0.29" @@ -26,6 +27,7 @@ dotenvy = "0.15" chrono = "0.4" open = { version = "5.0", optional = true } arboard = { version = "3.4", optional = true } +notify-rust = { version = "4.11", optional = true } toml = "0.8" dirs = "5.0" thiserror = "1.0" diff --git a/HOTKEYS.md b/HOTKEYS.md index df67c01..99f9db3 100644 --- a/HOTKEYS.md +++ b/HOTKEYS.md @@ -41,7 +41,7 @@ | `d` / `Delete` | `в` | Удалить сообщение | | `y` | `н` | Копировать текст в буфер обмена | | `e` | `у` | Добавить реакцию (Emoji picker) | -| `i` | | Открыть профиль чата/пользователя | +| `Ctrl+i` | `Ctrl+ш` | Открыть профиль чата/пользователя | ## Модалки подтверждения diff --git a/ROADMAP.md b/ROADMAP.md index c1e6d0d..a96d263 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -129,7 +129,7 @@ - Индикатор черновика в списке чатов - Восстановление текста при возврате в чат - [x] Профиль пользователя/чата - - `i` — открыть информацию о чате/собеседнике + - `Ctrl+i` — открыть информацию о чате/собеседнике - Для личных чатов: имя, username, телефон, био - Для групп: название, описание, количество участников - [x] Копирование сообщений @@ -143,3 +143,55 @@ - `~/.config/tele-tui/config.toml` - Настройки: цветовая схема, часовой пояс, хоткеи - Загрузка конфига при старте + +## Фаза 10: Desktop уведомления [DONE - 83%] + +### Стадия 1: Базовая реализация [DONE] +- [x] NotificationManager модуль + - notify-rust интеграция (версия 4.11) + - Feature flag "notifications" в Cargo.toml + - Базовая структура с настройками +- [x] Конфигурация уведомлений + - NotificationsConfig в config.toml + - enabled: bool - вкл/выкл уведомлений + - only_mentions: bool - только упоминания + - show_preview: bool - показывать превью текста +- [x] Интеграция с TdClient + - Поле notification_manager в TdClient + - Метод configure_notifications() + - Обработка в handle_new_message_update() +- [x] Базовая отправка уведомлений + - Уведомления для сообщений не из текущего чата + - Форматирование title (имя чата) и body (текст/медиа-заглушка) + - Sender name из MessageInfo + +### Стадия 2: Улучшения [IN PROGRESS] +- [x] Синхронизация muted чатов + - Загрузка списка muted чатов из Telegram + - Вызов sync_muted_chats() при инициализации и обновлении (Ctrl+R) + - Muted чаты автоматически фильтруются из уведомлений +- [x] Фильтрация по упоминаниям + - Метод MessageInfo::has_mention() проверяет TextEntityType::Mention и MentionName + - NotificationManager применяет фильтр only_mentions из конфига + - Работает для @username и inline mentions +- [x] Поддержка типов медиа + - Метод beautify_media_labels() заменяет текстовые заглушки на emoji + - Поддержка: 📷 Фото, 🎥 Видео, 🎞️ GIF, 🎤 Голосовое, 🎨 Стикер + - Также: 📎 Файл, 🎵 Аудио, 📹 Видеосообщение, 📍 Локация, 👤 Контакт, 📊 Опрос +- [ ] Кастомизация звуков + - Настройка звуков уведомлений в config.toml + - Разные звуки для разных типов сообщений + +### Стадия 3: Полировка [DONE] +- [x] Обработка ошибок + - Graceful fallback если уведомления недоступны (возвращает Ok без паники) + - Логирование ошибок через tracing::warn! + - Детальное логирование причин пропуска уведомлений (debug level) +- [x] Дополнительные настройки + - timeout_ms - продолжительность показа (0 = системное значение) + - urgency - уровень важности: "low", "normal", "critical" (только Linux) + - Красивые эмодзи для типов медиа +- [ ] Опциональные улучшения (не критично) + - Кросс-платформенное тестирование (требует ручного тестирования) + - icon - кастомная иконка приложения + - Actions в уведомлениях (кнопки "Ответить", "Прочитано") diff --git a/config.example.toml b/config.example.toml new file mode 100644 index 0000000..806a875 --- /dev/null +++ b/config.example.toml @@ -0,0 +1,35 @@ +# Telegram TUI Configuration Example +# Copy this file to ~/.config/tele-tui/config.toml + +[general] +# Timezone offset (e.g., "+03:00", "-05:00") +timezone = "+03:00" + +[colors] +# Colors: red, green, blue, yellow, cyan, magenta, white, black, gray +# Also available: lightred, lightgreen, lightblue, lightyellow, lightcyan, lightmagenta +incoming_message = "white" +outgoing_message = "green" +selected_message = "yellow" +reaction_chosen = "yellow" +reaction_other = "gray" + +[notifications] +# Enable desktop notifications for new messages +enabled = true + +# Only notify when you are mentioned (@username) +only_mentions = false + +# Show message preview text in notifications +show_preview = true + +# Notification timeout in milliseconds (0 = system default) +timeout_ms = 5000 + +# Notification urgency level: "low", "normal", "critical" +# Note: Only works on Linux (libnotify), ignored on macOS/Windows +urgency = "normal" + +# Note: Notifications respect Telegram's mute settings +# Muted chats won't trigger notifications diff --git a/src/app/mod.rs b/src/app/mod.rs index 6570168..83417d2 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1007,6 +1007,9 @@ impl App { /// /// A new `App` instance ready to start authentication. pub fn new(config: crate::config::Config) -> App { - App::with_client(config, TdClient::new()) + let mut client = TdClient::new(); + // Configure notifications from config + client.configure_notifications(&config.notifications); + App::with_client(config, client) } } diff --git a/src/config/keybindings.rs b/src/config/keybindings.rs index 2ff41d0..e52fe87 100644 --- a/src/config/keybindings.rs +++ b/src/config/keybindings.rs @@ -230,8 +230,8 @@ impl Keybindings { // Profile bindings.insert(Command::OpenProfile, vec![ - KeyBinding::new(KeyCode::Char('i')), - KeyBinding::new(KeyCode::Char('ш')), // RU + KeyBinding::with_ctrl(KeyCode::Char('i')), + KeyBinding::with_ctrl(KeyCode::Char('ш')), // RU ]); Self { bindings } diff --git a/src/config/mod.rs b/src/config/mod.rs index cf61759..92bd190 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -34,6 +34,10 @@ pub struct Config { /// Горячие клавиши. #[serde(default)] pub keybindings: Keybindings, + + /// Настройки desktop notifications. + #[serde(default)] + pub notifications: NotificationsConfig, } /// Общие настройки приложения. @@ -71,6 +75,31 @@ pub struct ColorsConfig { pub reaction_other: String, } +/// Настройки desktop notifications. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NotificationsConfig { + /// Включить/выключить уведомления + #[serde(default = "default_notifications_enabled")] + pub enabled: bool, + + /// Уведомлять только при @упоминаниях + #[serde(default)] + pub only_mentions: bool, + + /// Показывать превью текста сообщения + #[serde(default = "default_show_preview")] + pub show_preview: bool, + + /// Продолжительность показа уведомления (миллисекунды) + /// 0 = системное значение по умолчанию + #[serde(default = "default_notification_timeout")] + pub timeout_ms: i32, + + /// Уровень важности: "low", "normal", "critical" + #[serde(default = "default_notification_urgency")] + pub urgency: String, +} + // Дефолтные значения fn default_timezone() -> String { "+03:00".to_string() @@ -96,6 +125,22 @@ fn default_reaction_other_color() -> String { "gray".to_string() } +fn default_notifications_enabled() -> bool { + true +} + +fn default_show_preview() -> bool { + true +} + +fn default_notification_timeout() -> i32 { + 5000 // 5 seconds +} + +fn default_notification_urgency() -> String { + "normal".to_string() +} + impl Default for GeneralConfig { fn default() -> Self { Self { timezone: default_timezone() } @@ -114,6 +159,17 @@ impl Default for ColorsConfig { } } +impl Default for NotificationsConfig { + fn default() -> Self { + Self { + enabled: default_notifications_enabled(), + only_mentions: false, + show_preview: default_show_preview(), + timeout_ms: default_notification_timeout(), + urgency: default_notification_urgency(), + } + } +} impl Default for Config { fn default() -> Self { @@ -121,6 +177,7 @@ impl Default for Config { general: GeneralConfig::default(), colors: ColorsConfig::default(), keybindings: Keybindings::default(), + notifications: NotificationsConfig::default(), } } } diff --git a/src/input/handlers/global.rs b/src/input/handlers/global.rs index d9e8ae7..0bc9a99 100644 --- a/src/input/handlers/global.rs +++ b/src/input/handlers/global.rs @@ -47,6 +47,8 @@ pub async fn handle_global_commands(app: &mut App, key: Key // Ctrl+R - обновить список чатов app.status_message = Some("Обновление чатов...".to_string()); let _ = with_timeout(Duration::from_secs(5), app.td_client.load_chats(50)).await; + // Синхронизируем muted чаты после обновления + app.td_client.sync_notification_muted_chats(); app.status_message = None; true } diff --git a/src/lib.rs b/src/lib.rs index 670db97..fa72b40 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ pub mod constants; pub mod formatting; pub mod input; pub mod message_grouping; +pub mod notifications; pub mod tdlib; pub mod types; pub mod ui; diff --git a/src/main.rs b/src/main.rs index 9b6e837..633a16f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod constants; mod formatting; mod input; mod message_grouping; +mod notifications; mod tdlib; mod types; mod ui; @@ -237,6 +238,8 @@ async fn update_screen_state(app: &mut App) -> bool if app.chat_list_state.selected().is_none() && !app.chats.is_empty() { app.chat_list_state.select(Some(0)); } + // Синхронизируем muted чаты для notifications + app.td_client.sync_notification_muted_chats(); // Убираем статус загрузки когда чаты появились if app.is_loading { app.is_loading = false; diff --git a/src/notifications.rs b/src/notifications.rs new file mode 100644 index 0000000..fe3421d --- /dev/null +++ b/src/notifications.rs @@ -0,0 +1,352 @@ +//! Desktop notifications module +//! +//! Provides cross-platform desktop notifications for new messages. + +use crate::tdlib::{ChatInfo, MessageInfo}; +use crate::types::ChatId; +use std::collections::HashSet; + +#[cfg(feature = "notifications")] +use notify_rust::{Notification, Timeout}; + +/// Manages desktop notifications +pub struct NotificationManager { + /// Whether notifications are enabled + enabled: bool, + /// Set of muted chat IDs (don't notify for these chats) + muted_chats: HashSet, + /// Only notify for mentions (@username) + only_mentions: bool, + /// Show message preview text + show_preview: bool, + /// Notification timeout in milliseconds (0 = system default) + timeout_ms: i32, + /// Notification urgency level + urgency: String, +} + +impl NotificationManager { + /// Creates a new notification manager with default settings + pub fn new() -> Self { + Self { + enabled: true, + muted_chats: HashSet::new(), + only_mentions: false, + show_preview: true, + timeout_ms: 5000, + urgency: "normal".to_string(), + } + } + + /// Creates a notification manager with custom settings + pub fn with_config( + enabled: bool, + only_mentions: bool, + show_preview: bool, + ) -> Self { + Self { + enabled, + muted_chats: HashSet::new(), + only_mentions, + show_preview, + timeout_ms: 5000, + urgency: "normal".to_string(), + } + } + + /// Sets whether notifications are enabled + pub fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + /// Sets whether to only notify for mentions + pub fn set_only_mentions(&mut self, only_mentions: bool) { + self.only_mentions = only_mentions; + } + + /// Sets notification timeout in milliseconds + pub fn set_timeout(&mut self, timeout_ms: i32) { + self.timeout_ms = timeout_ms; + } + + /// Sets notification urgency level + pub fn set_urgency(&mut self, urgency: String) { + self.urgency = urgency; + } + + /// Adds a chat to the muted list + pub fn mute_chat(&mut self, chat_id: ChatId) { + self.muted_chats.insert(chat_id); + } + + /// Removes a chat from the muted list + pub fn unmute_chat(&mut self, chat_id: ChatId) { + self.muted_chats.remove(&chat_id); + } + + /// Checks if a chat should be muted based on Telegram mute status + pub fn sync_muted_chats(&mut self, chats: &[ChatInfo]) { + self.muted_chats.clear(); + for chat in chats { + if chat.is_muted { + self.muted_chats.insert(chat.id); + } + } + } + + /// Sends a notification for a new message + /// + /// # Arguments + /// + /// * `chat` - Chat information + /// * `message` - Message information + /// * `sender_name` - Name of the message sender + /// + /// Returns `Ok(())` if notification was sent or skipped, `Err` if failed + pub fn notify_new_message( + &self, + chat: &ChatInfo, + message: &MessageInfo, + sender_name: &str, + ) -> Result<(), String> { + // Check if notifications are enabled + if !self.enabled { + tracing::debug!("Notifications disabled, skipping"); + return Ok(()); + } + + // Don't notify for outgoing messages + if message.is_outgoing() { + tracing::debug!("Outgoing message, skipping notification"); + return Ok(()); + } + + // Check if chat is muted + if self.muted_chats.contains(&chat.id) { + tracing::debug!("Chat {} is muted, skipping notification", chat.title); + return Ok(()); + } + + // Check if we only notify for mentions + if self.only_mentions && !message.has_mention() { + tracing::debug!("only_mentions=true but no mention found, skipping"); + return Ok(()); + } + + // Format the notification + let title = &chat.title; + let body = self.format_message_body(sender_name, message); + + tracing::debug!("Sending notification for chat: {}", title); + + // Send the notification + self.send_notification(title, &body)?; + + Ok(()) + } + + /// Formats the message body for notification + fn format_message_body(&self, sender_name: &str, message: &MessageInfo) -> String { + // For groups, include sender name. For private chats, sender name is in title + let prefix = if !sender_name.is_empty() && sender_name != message.sender_name() { + format!("{}: ", sender_name) + } else { + String::new() + }; + + let content = if self.show_preview { + let text = message.text(); + + // Check if message is empty (media, sticker, etc.) + if text.is_empty() { + "Новое сообщение".to_string() + } else { + // Beautify media labels with emojis + let beautified = Self::beautify_media_labels(text); + + // Limit preview length + if beautified.len() > 150 { + format!("{}...", &beautified[..147]) + } else { + beautified + } + } + } else { + "Новое сообщение".to_string() + }; + + format!("{}{}", prefix, content) + } + + /// Replaces text media labels with emoji-enhanced versions + fn beautify_media_labels(text: &str) -> String { + text.replace("[Фото]", "📷 Фото") + .replace("[Видео]", "🎥 Видео") + .replace("[GIF]", "🎞️ GIF") + .replace("[Голосовое]", "🎤 Голосовое") + .replace("[Стикер:", "🎨 Стикер:") + .replace("[Файл:", "📎 Файл:") + .replace("[Аудио:", "🎵 Аудио:") + .replace("[Аудио]", "🎵 Аудио") + .replace("[Видеосообщение]", "📹 Видеосообщение") + .replace("[Локация]", "📍 Локация") + .replace("[Контакт:", "👤 Контакт:") + .replace("[Опрос:", "📊 Опрос:") + .replace("[Место встречи:", "📍 Место встречи:") + .replace("[Неподдерживаемый тип сообщения]", "📨 Сообщение") + } + + /// Sends a desktop notification + /// + /// Returns `Ok(())` if notification was sent successfully or skipped. + /// Logs errors but doesn't fail - notifications are not critical for app functionality. + #[cfg(feature = "notifications")] + fn send_notification(&self, title: &str, body: &str) -> Result<(), String> { + // Don't send if notifications are disabled + if !self.enabled { + return Ok(()); + } + + // Determine timeout + let timeout = if self.timeout_ms <= 0 { + Timeout::Default + } else { + Timeout::Milliseconds(self.timeout_ms as u32) + }; + + // Build notification + let mut notification = Notification::new(); + notification + .summary(title) + .body(body) + .icon("telegram") + .appname("tele-tui") + .timeout(timeout); + + // Set urgency if supported + #[cfg(all(unix, not(target_os = "macos")))] + { + use notify_rust::Urgency; + let urgency_level = match self.urgency.to_lowercase().as_str() { + "low" => Urgency::Low, + "critical" => Urgency::Critical, + _ => Urgency::Normal, + }; + notification.urgency(urgency_level); + } + + match notification.show() { + Ok(_) => Ok(()), + Err(e) => { + // Log error but don't fail - notifications are optional + tracing::warn!("Failed to send desktop notification: {}", e); + // Return Ok to not break the app flow + Ok(()) + } + } + } + + /// Fallback when notifications feature is disabled + #[cfg(not(feature = "notifications"))] + fn send_notification(&self, _title: &str, _body: &str) -> Result<(), String> { + Ok(()) + } +} + +impl Default for NotificationManager { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_notification_manager_creation() { + let manager = NotificationManager::new(); + assert!(manager.enabled); + assert!(!manager.only_mentions); + assert!(manager.show_preview); + } + + #[test] + fn test_mute_unmute() { + let mut manager = NotificationManager::new(); + let chat_id = ChatId::new(123); + + manager.mute_chat(chat_id); + assert!(manager.muted_chats.contains(&chat_id)); + + manager.unmute_chat(chat_id); + assert!(!manager.muted_chats.contains(&chat_id)); + } + + #[test] + fn test_disabled_notifications() { + let mut manager = NotificationManager::new(); + manager.set_enabled(false); + + // Should return Ok without sending notification + let result = manager.send_notification("Test", "Body"); + assert!(result.is_ok()); + } + + #[test] + fn test_only_mentions_setting() { + let mut manager = NotificationManager::new(); + assert!(!manager.only_mentions); + + manager.set_only_mentions(true); + assert!(manager.only_mentions); + + manager.set_only_mentions(false); + assert!(!manager.only_mentions); + } + + #[test] + fn test_beautify_media_labels() { + // Test photo + assert_eq!( + NotificationManager::beautify_media_labels("[Фото]"), + "📷 Фото" + ); + + // Test video + assert_eq!( + NotificationManager::beautify_media_labels("[Видео]"), + "🎥 Видео" + ); + + // Test sticker with emoji + assert_eq!( + NotificationManager::beautify_media_labels("[Стикер: 😊]"), + "🎨 Стикер: 😊]" + ); + + // Test audio with title + assert_eq!( + NotificationManager::beautify_media_labels("[Аудио: Artist - Song]"), + "🎵 Аудио: Artist - Song]" + ); + + // Test file + assert_eq!( + NotificationManager::beautify_media_labels("[Файл: document.pdf]"), + "📎 Файл: document.pdf]" + ); + + // Test regular text (no changes) + assert_eq!( + NotificationManager::beautify_media_labels("Hello, world!"), + "Hello, world!" + ); + + // Test mixed content + assert_eq!( + NotificationManager::beautify_media_labels("[Фото] Check this out!"), + "📷 Фото Check this out!" + ); + } +} diff --git a/src/tdlib/client.rs b/src/tdlib/client.rs index 0452993..bac4e33 100644 --- a/src/tdlib/client.rs +++ b/src/tdlib/client.rs @@ -15,6 +15,7 @@ use super::messages::MessageManager; use super::reactions::ReactionManager; use super::types::{ChatInfo, FolderInfo, MessageInfo, NetworkState, ProfileInfo, UserOnlineStatus}; use super::users::UserCache; +use crate::notifications::NotificationManager; /// TDLib client wrapper for Telegram integration. /// @@ -52,6 +53,7 @@ pub struct TdClient { pub message_manager: MessageManager, pub user_cache: UserCache, pub reaction_manager: ReactionManager, + pub notification_manager: NotificationManager, // Состояние сети pub network_state: NetworkState, @@ -93,10 +95,27 @@ impl TdClient { message_manager: MessageManager::new(client_id), user_cache: UserCache::new(client_id), reaction_manager: ReactionManager::new(client_id), + notification_manager: NotificationManager::new(), network_state: NetworkState::Connecting, } } + /// Configures notification manager from app config + pub fn configure_notifications(&mut self, config: &crate::config::NotificationsConfig) { + self.notification_manager.set_enabled(config.enabled); + self.notification_manager.set_only_mentions(config.only_mentions); + self.notification_manager.set_timeout(config.timeout_ms); + self.notification_manager.set_urgency(config.urgency.clone()); + // Note: show_preview is used when formatting notification body + } + + /// Synchronizes muted chats from Telegram to notification manager. + /// + /// Should be called after chats are loaded to ensure muted chats don't trigger notifications. + pub fn sync_notification_muted_chats(&mut self) { + self.notification_manager.sync_muted_chats(&self.chat_manager.chats); + } + // Делегирование к auth /// Sends phone number for authentication. diff --git a/src/tdlib/client_impl.rs b/src/tdlib/client_impl.rs index 76ea884..48557d1 100644 --- a/src/tdlib/client_impl.rs +++ b/src/tdlib/client_impl.rs @@ -263,6 +263,11 @@ impl TdClientTrait for TdClient { self.user_cache_mut() } + // ============ Notification methods ============ + fn sync_notification_muted_chats(&mut self) { + self.sync_notification_muted_chats() + } + // ============ Update handling ============ fn handle_update(&mut self, update: Update) { self.handle_update(update) diff --git a/src/tdlib/trait.rs b/src/tdlib/trait.rs index 8072760..70d1cfb 100644 --- a/src/tdlib/trait.rs +++ b/src/tdlib/trait.rs @@ -120,6 +120,9 @@ pub trait TdClientTrait: Send { fn set_main_chat_list_position(&mut self, position: i32); fn user_cache_mut(&mut self) -> &mut UserCache; + // ============ Notification methods ============ + fn sync_notification_muted_chats(&mut self); + // ============ Update handling ============ fn handle_update(&mut self, update: Update); } diff --git a/src/tdlib/types.rs b/src/tdlib/types.rs index 6c8fbb6..0829d65 100644 --- a/src/tdlib/types.rs +++ b/src/tdlib/types.rs @@ -1,3 +1,4 @@ +use tdlib_rs::enums::TextEntityType; use tdlib_rs::types::TextEntity; use crate::types::{ChatId, MessageId}; @@ -192,6 +193,16 @@ impl MessageInfo { self.state.can_be_deleted_for_all_users } + /// Checks if the message contains a mention (@username or user mention) + pub fn has_mention(&self) -> bool { + self.content.entities.iter().any(|entity| { + matches!( + entity.r#type, + TextEntityType::Mention | TextEntityType::MentionName(_) + ) + }) + } + pub fn reply_to(&self) -> Option<&ReplyInfo> { self.interactions.reply_to.as_ref() } @@ -475,6 +486,39 @@ mod tests { assert!(message.can_be_edited()); assert!(message.can_be_deleted_for_all_users()); } + + #[test] + fn test_message_has_mention() { + // Message without mentions + let message = MessageBuilder::new(MessageId::new(1)) + .text("Hello world") + .build(); + assert!(!message.has_mention()); + + // Message with @mention + let message_with_mention = MessageBuilder::new(MessageId::new(2)) + .text("Hello @user") + .entities(vec![TextEntity { + offset: 6, + length: 5, + r#type: TextEntityType::Mention, + }]) + .build(); + assert!(message_with_mention.has_mention()); + + // Message with MentionName + let message_with_mention_name = MessageBuilder::new(MessageId::new(3)) + .text("Hello John") + .entities(vec![TextEntity { + offset: 6, + length: 4, + r#type: TextEntityType::MentionName( + tdlib_rs::types::TextEntityTypeMentionName { user_id: 123 }, + ), + }]) + .build(); + assert!(message_with_mention_name.has_mention()); + } } #[derive(Debug, Clone)] diff --git a/src/tdlib/update_handlers.rs b/src/tdlib/update_handlers.rs index abdc74b..cd933e5 100644 --- a/src/tdlib/update_handlers.rs +++ b/src/tdlib/update_handlers.rs @@ -19,12 +19,29 @@ use super::types::ReactionInfo; /// Обрабатывает Update::NewMessage - добавление нового сообщения pub fn handle_new_message_update(client: &mut TdClient, new_msg: UpdateNewMessage) { - // Добавляем новое сообщение если это текущий открытый чат let chat_id = ChatId::new(new_msg.message.chat_id); + + // Если сообщение НЕ для текущего открытого чата - отправляем уведомление if Some(chat_id) != client.current_chat_id() { + // Find and clone chat info to avoid borrow checker issues + if let Some(chat) = client.chats().iter().find(|c| c.id == chat_id).cloned() { + let msg_info = crate::tdlib::message_converter::convert_message(client, &new_msg.message, chat_id); + + // Get sender name (from message or user cache) + let sender_name = msg_info.sender_name(); + + // Send notification + let _ = client.notification_manager.notify_new_message( + &chat, + &msg_info, + sender_name, + ); + } return; } + // Добавляем новое сообщение если это текущий открытый чат + let msg_info = crate::tdlib::message_converter::convert_message(client, &new_msg.message, chat_id); let msg_id = msg_info.id(); let is_incoming = !msg_info.is_outgoing(); diff --git a/tests/config.rs b/tests/config.rs index 27235e3..d8053c6 100644 --- a/tests/config.rs +++ b/tests/config.rs @@ -1,6 +1,6 @@ // Integration tests for config flow -use tele_tui::config::{Config, ColorsConfig, GeneralConfig, Keybindings}; +use tele_tui::config::{Config, ColorsConfig, GeneralConfig, Keybindings, NotificationsConfig}; /// Test: Дефолтные значения конфигурации #[test] @@ -33,6 +33,7 @@ fn test_config_custom_values() { reaction_other: "white".to_string(), }, keybindings: Keybindings::default(), + notifications: NotificationsConfig::default(), }; assert_eq!(config.general.timezone, "+05:00"); @@ -116,6 +117,7 @@ fn test_config_toml_serialization() { reaction_other: "white".to_string(), }, keybindings: Keybindings::default(), + notifications: NotificationsConfig::default(), }; // Сериализуем в TOML diff --git a/tests/helpers/fake_tdclient_impl.rs b/tests/helpers/fake_tdclient_impl.rs index 83d8b56..05a3f44 100644 --- a/tests/helpers/fake_tdclient_impl.rs +++ b/tests/helpers/fake_tdclient_impl.rs @@ -299,6 +299,11 @@ impl TdClientTrait for FakeTdClient { panic!("user_cache_mut not supported for FakeTdClient") } + // ============ Notification methods ============ + fn sync_notification_muted_chats(&mut self) { + // Not implemented for fake client (notifications are not tested) + } + // ============ Update handling ============ fn handle_update(&mut self, _update: Update) { // Not implemented for fake client