Rust · 6080 bytes Raw Blame History
1 //! Notification storage and timeout management
2
3 use std::collections::HashMap;
4 use std::sync::atomic::{AtomicU32, Ordering};
5 use std::sync::Arc;
6 use tokio::runtime::Handle;
7 use tokio::sync::Mutex;
8 use tokio::task::JoinHandle;
9 use tracing::{debug, info};
10
11 use super::types::{CloseReason, Notification, UrgencyTimeouts};
12
13 /// Counter for notification IDs
14 static NEXT_ID: AtomicU32 = AtomicU32::new(1);
15
16 /// Generate the next notification ID
17 pub fn next_notification_id() -> u32 {
18 NEXT_ID.fetch_add(1, Ordering::SeqCst)
19 }
20
21 /// Event sent for notification lifecycle changes
22 #[derive(Debug, Clone)]
23 pub enum NotificationEvent {
24 /// A new notification was created
25 Created(Notification),
26 /// A notification was updated (replaced)
27 Updated(Notification),
28 /// A notification should be closed
29 Closed { id: u32, reason: CloseReason },
30 }
31
32 /// Notification storage with timeout management
33 pub struct NotificationStore {
34 /// Active notifications by ID
35 notifications: HashMap<u32, Notification>,
36 /// Timeout handles by notification ID
37 timeouts: HashMap<u32, JoinHandle<()>>,
38 /// Channel to send expiration events
39 event_tx: tokio::sync::mpsc::Sender<NotificationEvent>,
40 /// Default timeout configuration
41 urgency_timeouts: UrgencyTimeouts,
42 /// Tokio runtime handle for spawning tasks
43 tokio_handle: Handle,
44 }
45
46 impl NotificationStore {
47 /// Create a new notification store
48 pub fn new(
49 event_tx: tokio::sync::mpsc::Sender<NotificationEvent>,
50 urgency_timeouts: UrgencyTimeouts,
51 tokio_handle: Handle,
52 ) -> Self {
53 Self {
54 notifications: HashMap::new(),
55 timeouts: HashMap::new(),
56 event_tx,
57 urgency_timeouts,
58 tokio_handle,
59 }
60 }
61
62 /// Add a notification to the store
63 ///
64 /// If replaces_id > 0 and exists, replaces that notification.
65 /// Returns the notification ID.
66 pub fn add(&mut self, mut notification: Notification) -> u32 {
67 // Handle replacement
68 let is_replacement = notification.replaces_id > 0
69 && self.notifications.contains_key(&notification.replaces_id);
70
71 let id = if is_replacement {
72 // Cancel existing timeout
73 if let Some(handle) = self.timeouts.remove(&notification.replaces_id) {
74 handle.abort();
75 }
76 notification.replaces_id
77 } else {
78 notification.id
79 };
80
81 // Update ID if we're replacing
82 notification.id = id;
83
84 info!(
85 "Storing notification: id={} app=\"{}\" summary=\"{}\" urgency={}",
86 id, notification.app_name, notification.summary, notification.hints.urgency
87 );
88
89 // Schedule timeout if needed
90 self.schedule_timeout(&notification);
91
92 // Send event for UI to show/update popup
93 let event = if is_replacement {
94 NotificationEvent::Updated(notification.clone())
95 } else {
96 NotificationEvent::Created(notification.clone())
97 };
98 let tx = self.event_tx.clone();
99 let _ = self.tokio_handle.spawn(async move {
100 let _ = tx.send(event).await;
101 });
102
103 // Store notification
104 self.notifications.insert(id, notification);
105
106 id
107 }
108
109 /// Schedule a timeout for a notification
110 fn schedule_timeout(&mut self, notification: &Notification) {
111 let timeout_ms = notification.effective_timeout(
112 self.urgency_timeouts.normal,
113 &self.urgency_timeouts,
114 );
115
116 if let Some(ms) = timeout_ms {
117 let id = notification.id;
118 let tx = self.event_tx.clone();
119
120 debug!("Scheduling timeout for notification {}: {}ms", id, ms);
121
122 // Use the stored tokio handle to spawn the timeout task
123 // This allows spawning from non-tokio contexts (like zbus's executor)
124 let handle = self.tokio_handle.spawn(async move {
125 tokio::time::sleep(tokio::time::Duration::from_millis(ms)).await;
126 let _ = tx
127 .send(NotificationEvent::Closed {
128 id,
129 reason: CloseReason::Expired,
130 })
131 .await;
132 });
133
134 self.timeouts.insert(id, handle);
135 } else {
136 debug!(
137 "Notification {} has no timeout (will persist)",
138 notification.id
139 );
140 }
141 }
142
143 /// Get a notification by ID
144 pub fn get(&self, id: u32) -> Option<&Notification> {
145 self.notifications.get(&id)
146 }
147
148 /// Remove a notification from the store
149 ///
150 /// Returns the removed notification if it existed.
151 pub fn remove(&mut self, id: u32) -> Option<Notification> {
152 // Cancel timeout
153 if let Some(handle) = self.timeouts.remove(&id) {
154 handle.abort();
155 }
156
157 self.notifications.remove(&id)
158 }
159
160 /// Get all active notifications
161 pub fn list(&self) -> Vec<&Notification> {
162 self.notifications.values().collect()
163 }
164
165 /// Get count of active notifications
166 pub fn count(&self) -> usize {
167 self.notifications.len()
168 }
169
170 /// Check if a notification exists
171 pub fn contains(&self, id: u32) -> bool {
172 self.notifications.contains_key(&id)
173 }
174
175 /// Clear all notifications
176 pub fn clear(&mut self) -> Vec<Notification> {
177 // Cancel all timeouts
178 for (_, handle) in self.timeouts.drain() {
179 handle.abort();
180 }
181
182 self.notifications.drain().map(|(_, n)| n).collect()
183 }
184 }
185
186 /// Thread-safe wrapper around NotificationStore
187 pub type SharedNotificationStore = Arc<Mutex<NotificationStore>>;
188
189 /// Create a new shared notification store
190 pub fn new_shared_store(
191 event_tx: tokio::sync::mpsc::Sender<NotificationEvent>,
192 urgency_timeouts: UrgencyTimeouts,
193 tokio_handle: Handle,
194 ) -> SharedNotificationStore {
195 Arc::new(Mutex::new(NotificationStore::new(event_tx, urgency_timeouts, tokio_handle)))
196 }
197