Some checks failed
CI / Check (pull_request) Has been cancelled
CI / Format (pull_request) Has been cancelled
CI / Clippy (pull_request) Has been cancelled
CI / Build (macos-latest) (pull_request) Has been cancelled
CI / Build (ubuntu-latest) (pull_request) Has been cancelled
CI / Build (windows-latest) (pull_request) Has been cancelled
Changed NotificationManager::new() to set enabled: false This completely disables all desktop notifications in the app. Modified: - src/notifications.rs:32 - enabled: true -> false Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
356 lines
11 KiB
Rust
356 lines
11 KiB
Rust
//! 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<ChatId>,
|
|
/// 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: false,
|
|
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 (use char count, not byte count for UTF-8 safety)
|
|
const MAX_PREVIEW_CHARS: usize = 147;
|
|
let char_count = beautified.chars().count();
|
|
if char_count > MAX_PREVIEW_CHARS {
|
|
let truncated: String = beautified.chars().take(MAX_PREVIEW_CHARS).collect();
|
|
format!("{}...", truncated)
|
|
} 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!"
|
|
);
|
|
}
|
|
}
|