feat: implement desktop notifications with comprehensive filtering
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>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user