Implemented Phase 10 (Desktop Notifications) with three stages: notify-rust integration, smart filtering, and production polish. Stage 1 - Base Implementation: - Add NotificationManager module (src/notifications.rs, 350+ lines) - Integrate notify-rust 4.11 with feature flag "notifications" - Implement NotificationsConfig in config.toml (enabled, only_mentions, show_preview) - Add notification_manager field to TdClient - Create configure_notifications() method for config integration - Hook into handle_new_message_update() to send notifications - Send notifications for messages outside current chat - Format notification body with sender name and message preview Stage 2 - Smart Filtering: - Sync muted chats from Telegram (sync_muted_chats method) - Filter muted chats from notifications automatically - Add MessageInfo::has_mention() to detect @username mentions - Implement only_mentions filter (notify only when mentioned) - Beautify media labels with emojis (📷 📹 🎤 🎨 📎 etc.) - Support 10+ media types in notification preview Stage 3 - Production Polish: - Add graceful error handling (no panics on notification failure) - Implement comprehensive logging (tracing::debug!/warn!) - Add timeout_ms configuration (0 = system default) - Add urgency configuration (low/normal/critical, Linux only) - Platform-specific #[cfg] for urgency support - Log all notification skip reasons at debug level Hotkey Change: - Move profile view from 'i' to Ctrl+i to avoid conflicts Technical Details: - Cross-platform support (macOS, Linux, Windows) - Feature flag for optional notifications support - Graceful fallback when notifications unavailable - LRU-friendly muted chats sync - Test coverage for all core notification logic - All 75 tests passing Files Changed: - NEW: src/notifications.rs - Complete NotificationManager - NEW: config.example.toml - Example configuration with notifications - Modified: Cargo.toml - Add notify-rust 4.11 dependency - Modified: src/config/mod.rs - Add NotificationsConfig struct - Modified: src/tdlib/types.rs - Add has_mention() method - Modified: src/tdlib/client.rs - Add notification integration - Modified: src/tdlib/update_handlers.rs - Hook notifications - Modified: src/config/keybindings.rs - Change profile to Ctrl+i - Modified: tests/* - Add notification config to tests Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
312 lines
10 KiB
Rust
312 lines
10 KiB
Rust
//! Implementation of TdClientTrait for FakeTdClient
|
|
|
|
use super::fake_tdclient::FakeTdClient;
|
|
use async_trait::async_trait;
|
|
use tdlib_rs::enums::{ChatAction, Update};
|
|
use tele_tui::tdlib::{AuthState, ChatInfo, FolderInfo, MessageInfo, ProfileInfo, ReplyInfo, UserCache, UserOnlineStatus};
|
|
use tele_tui::tdlib::TdClientTrait;
|
|
use tele_tui::types::{ChatId, MessageId, UserId};
|
|
|
|
#[async_trait]
|
|
impl TdClientTrait for FakeTdClient {
|
|
// ============ Auth methods (not implemented for fake) ============
|
|
async fn send_phone_number(&self, _phone: String) -> Result<(), String> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn send_code(&self, _code: String) -> Result<(), String> {
|
|
Ok(())
|
|
}
|
|
|
|
async fn send_password(&self, _password: String) -> Result<(), String> {
|
|
Ok(())
|
|
}
|
|
|
|
// ============ Chat methods ============
|
|
async fn load_chats(&mut self, limit: i32) -> Result<(), String> {
|
|
// FakeTdClient loads chats but returns void
|
|
let _ = FakeTdClient::load_chats(self, limit as usize).await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn load_folder_chats(&mut self, folder_id: i32, limit: i32) -> Result<(), String> {
|
|
FakeTdClient::load_folder_chats(self, folder_id, limit as usize).await
|
|
}
|
|
|
|
async fn leave_chat(&self, _chat_id: ChatId) -> Result<(), String> {
|
|
// Not implemented for fake client
|
|
Ok(())
|
|
}
|
|
|
|
async fn get_profile_info(&self, chat_id: ChatId) -> Result<ProfileInfo, String> {
|
|
FakeTdClient::get_profile_info(self, chat_id).await
|
|
}
|
|
|
|
// ============ Chat actions ============
|
|
async fn send_chat_action(&self, chat_id: ChatId, action: ChatAction) {
|
|
let action_str = format!("{:?}", action);
|
|
FakeTdClient::send_chat_action(self, chat_id, action_str).await;
|
|
}
|
|
|
|
fn clear_stale_typing_status(&mut self) -> bool {
|
|
// Not implemented for fake
|
|
false
|
|
}
|
|
|
|
// ============ Message methods ============
|
|
async fn get_chat_history(&mut self, chat_id: ChatId, limit: i32) -> Result<Vec<MessageInfo>, String> {
|
|
FakeTdClient::get_chat_history(self, chat_id, limit).await
|
|
}
|
|
|
|
async fn load_older_messages(&mut self, chat_id: ChatId, from_message_id: MessageId) -> Result<Vec<MessageInfo>, String> {
|
|
FakeTdClient::load_older_messages(self, chat_id, from_message_id).await
|
|
}
|
|
|
|
async fn get_pinned_messages(&mut self, _chat_id: ChatId) -> Result<Vec<MessageInfo>, String> {
|
|
// Not implemented for fake
|
|
Ok(vec![])
|
|
}
|
|
|
|
async fn load_current_pinned_message(&mut self, _chat_id: ChatId) {
|
|
// Not implemented for fake
|
|
}
|
|
|
|
async fn search_messages(&self, chat_id: ChatId, query: &str) -> Result<Vec<MessageInfo>, String> {
|
|
FakeTdClient::search_messages(self, chat_id, query).await
|
|
}
|
|
|
|
async fn send_message(
|
|
&mut self,
|
|
chat_id: ChatId,
|
|
text: String,
|
|
reply_to_message_id: Option<MessageId>,
|
|
reply_info: Option<ReplyInfo>,
|
|
) -> Result<MessageInfo, String> {
|
|
FakeTdClient::send_message(self, chat_id, text, reply_to_message_id, reply_info).await
|
|
}
|
|
|
|
async fn edit_message(
|
|
&mut self,
|
|
chat_id: ChatId,
|
|
message_id: MessageId,
|
|
new_text: String,
|
|
) -> Result<MessageInfo, String> {
|
|
FakeTdClient::edit_message(self, chat_id, message_id, new_text).await
|
|
}
|
|
|
|
async fn delete_messages(
|
|
&mut self,
|
|
chat_id: ChatId,
|
|
message_ids: Vec<MessageId>,
|
|
revoke: bool,
|
|
) -> Result<(), String> {
|
|
FakeTdClient::delete_messages(self, chat_id, message_ids, revoke).await
|
|
}
|
|
|
|
async fn forward_messages(
|
|
&mut self,
|
|
to_chat_id: ChatId,
|
|
from_chat_id: ChatId,
|
|
message_ids: Vec<MessageId>,
|
|
) -> Result<(), String> {
|
|
FakeTdClient::forward_messages(self, from_chat_id, to_chat_id, message_ids).await
|
|
}
|
|
|
|
async fn set_draft_message(&self, chat_id: ChatId, text: String) -> Result<(), String> {
|
|
FakeTdClient::set_draft_message(self, chat_id, text).await
|
|
}
|
|
|
|
fn push_message(&mut self, _msg: MessageInfo) {
|
|
// Not used in fake client
|
|
}
|
|
|
|
async fn fetch_missing_reply_info(&mut self) {
|
|
// Not used in fake client
|
|
}
|
|
|
|
async fn process_pending_view_messages(&mut self) {
|
|
// Перемещаем pending в viewed для проверки в тестах
|
|
let mut pending = self.pending_view_messages.lock().unwrap();
|
|
for (chat_id, message_ids) in pending.drain(..) {
|
|
let ids: Vec<i64> = message_ids.iter().map(|id| id.as_i64()).collect();
|
|
self.viewed_messages.lock().unwrap().push((chat_id.as_i64(), ids));
|
|
}
|
|
}
|
|
|
|
// ============ User methods ============
|
|
fn get_user_status_by_chat_id(&self, _chat_id: ChatId) -> Option<&UserOnlineStatus> {
|
|
// Not implemented for fake
|
|
None
|
|
}
|
|
|
|
async fn process_pending_user_ids(&mut self) {
|
|
// Not used in fake client
|
|
}
|
|
|
|
// ============ Reaction methods ============
|
|
async fn get_message_available_reactions(
|
|
&self,
|
|
chat_id: ChatId,
|
|
message_id: MessageId,
|
|
) -> Result<Vec<String>, String> {
|
|
FakeTdClient::get_message_available_reactions(self, chat_id, message_id).await
|
|
}
|
|
|
|
async fn toggle_reaction(
|
|
&self,
|
|
chat_id: ChatId,
|
|
message_id: MessageId,
|
|
reaction: String,
|
|
) -> Result<(), String> {
|
|
FakeTdClient::toggle_reaction(self, chat_id, message_id, reaction).await
|
|
}
|
|
|
|
// ============ Getters (immutable) ============
|
|
fn client_id(&self) -> i32 {
|
|
0 // Fake client ID
|
|
}
|
|
|
|
async fn get_me(&self) -> Result<i64, String> {
|
|
Ok(12345) // Fake user ID
|
|
}
|
|
|
|
fn auth_state(&self) -> &AuthState {
|
|
// Can't return reference from Arc<Mutex>, need to use a different approach
|
|
// For now, return a static reference based on the current state
|
|
use std::sync::OnceLock;
|
|
static AUTH_STATE_READY: AuthState = AuthState::Ready;
|
|
static AUTH_STATE_WAIT_PHONE: OnceLock<AuthState> = OnceLock::new();
|
|
static AUTH_STATE_WAIT_CODE: OnceLock<AuthState> = OnceLock::new();
|
|
static AUTH_STATE_WAIT_PASSWORD: OnceLock<AuthState> = OnceLock::new();
|
|
|
|
let current = self.auth_state.lock().unwrap();
|
|
match *current {
|
|
AuthState::Ready => &AUTH_STATE_READY,
|
|
AuthState::WaitPhoneNumber => AUTH_STATE_WAIT_PHONE.get_or_init(|| AuthState::WaitPhoneNumber),
|
|
AuthState::WaitCode => AUTH_STATE_WAIT_CODE.get_or_init(|| AuthState::WaitCode),
|
|
AuthState::WaitPassword => AUTH_STATE_WAIT_PASSWORD.get_or_init(|| AuthState::WaitPassword),
|
|
_ => &AUTH_STATE_READY,
|
|
}
|
|
}
|
|
|
|
fn chats(&self) -> &[ChatInfo] {
|
|
// FakeTdClient uses Arc<Mutex>, can't return direct reference
|
|
// This is a limitation - we'll need to work around it
|
|
&[]
|
|
}
|
|
|
|
fn folders(&self) -> &[FolderInfo] {
|
|
&[]
|
|
}
|
|
|
|
fn current_chat_messages(&self) -> Vec<MessageInfo> {
|
|
if let Some(chat_id) = *self.current_chat_id.lock().unwrap() {
|
|
return self.get_messages(chat_id);
|
|
}
|
|
Vec::new()
|
|
}
|
|
|
|
fn current_chat_id(&self) -> Option<ChatId> {
|
|
self.get_current_chat_id().map(ChatId::new)
|
|
}
|
|
|
|
fn current_pinned_message(&self) -> Option<MessageInfo> {
|
|
self.current_pinned_message.lock().unwrap().clone()
|
|
}
|
|
|
|
fn typing_status(&self) -> Option<&(UserId, String, std::time::Instant)> {
|
|
None
|
|
}
|
|
|
|
fn pending_view_messages(&self) -> &[(ChatId, Vec<MessageId>)] {
|
|
&[]
|
|
}
|
|
|
|
fn pending_user_ids(&self) -> &[UserId] {
|
|
&[]
|
|
}
|
|
|
|
fn main_chat_list_position(&self) -> i32 {
|
|
0
|
|
}
|
|
|
|
fn user_cache(&self) -> &UserCache {
|
|
// Not implemented for fake - return empty cache
|
|
use std::sync::OnceLock;
|
|
static EMPTY_CACHE: OnceLock<UserCache> = OnceLock::new();
|
|
EMPTY_CACHE.get_or_init(|| UserCache::new(0))
|
|
}
|
|
|
|
fn network_state(&self) -> tele_tui::tdlib::types::NetworkState {
|
|
FakeTdClient::get_network_state(self)
|
|
}
|
|
|
|
// ============ Setters (mutable) ============
|
|
fn chats_mut(&mut self) -> &mut Vec<ChatInfo> {
|
|
// Can't return mutable reference from Arc<Mutex>
|
|
// This is a design limitation - we need a different approach
|
|
panic!("chats_mut not supported for FakeTdClient - use get_chats() instead")
|
|
}
|
|
|
|
fn folders_mut(&mut self) -> &mut Vec<FolderInfo> {
|
|
panic!("folders_mut not supported for FakeTdClient")
|
|
}
|
|
|
|
fn current_chat_messages_mut(&mut self) -> &mut Vec<MessageInfo> {
|
|
panic!("current_chat_messages_mut not supported for FakeTdClient")
|
|
}
|
|
|
|
fn clear_current_chat_messages(&mut self) {
|
|
if let Some(chat_id) = *self.current_chat_id.lock().unwrap() {
|
|
self.messages.lock().unwrap().remove(&chat_id);
|
|
}
|
|
}
|
|
|
|
fn set_current_chat_messages(&mut self, messages: Vec<MessageInfo>) {
|
|
if let Some(chat_id) = *self.current_chat_id.lock().unwrap() {
|
|
self.messages.lock().unwrap().insert(chat_id, messages);
|
|
}
|
|
}
|
|
|
|
fn set_current_chat_id(&mut self, chat_id: Option<ChatId>) {
|
|
*self.current_chat_id.lock().unwrap() = chat_id.map(|id| id.as_i64());
|
|
}
|
|
|
|
fn set_current_pinned_message(&mut self, msg: Option<MessageInfo>) {
|
|
*self.current_pinned_message.lock().unwrap() = msg;
|
|
}
|
|
|
|
fn set_typing_status(&mut self, _status: Option<(UserId, String, std::time::Instant)>) {
|
|
// Not implemented
|
|
}
|
|
|
|
fn pending_view_messages_mut(&mut self) -> &mut Vec<(ChatId, Vec<MessageId>)> {
|
|
// WORKAROUND: Возвращаем мутабельную ссылку через leak
|
|
// Это безопасно так как мы единственные владельцы &mut self
|
|
let guard = self.pending_view_messages.lock().unwrap();
|
|
unsafe { &mut *(guard.as_ptr() as *mut Vec<(ChatId, Vec<MessageId>)>) }
|
|
}
|
|
|
|
fn pending_user_ids_mut(&mut self) -> &mut Vec<UserId> {
|
|
panic!("pending_user_ids_mut not supported for FakeTdClient")
|
|
}
|
|
|
|
fn set_main_chat_list_position(&mut self, _position: i32) {
|
|
// Not implemented
|
|
}
|
|
|
|
fn user_cache_mut(&mut self) -> &mut UserCache {
|
|
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
|
|
}
|
|
}
|