Skip to main content

this/events/sinks/
preferences.rs

1//! Notification preferences store
2//!
3//! Per-user notification preferences that control which notification
4//! types are enabled/disabled. The `InAppNotificationSink` consults
5//! this store before storing a notification.
6//!
7//! # Default behavior
8//!
9//! All notification types are enabled by default. Users can disable
10//! specific types (e.g., "new_follower", "new_like"). Unknown types
11//! are treated as enabled.
12
13use serde::{Deserialize, Serialize};
14use std::collections::{HashMap, HashSet};
15use tokio::sync::RwLock;
16
17/// Per-user notification preferences
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct UserPreferences {
20    /// Notification types that are explicitly disabled
21    ///
22    /// Any type not in this set is considered enabled (opt-out model).
23    pub disabled_types: HashSet<String>,
24
25    /// Global mute — when true, ALL notifications are suppressed
26    #[serde(default)]
27    pub muted: bool,
28}
29
30impl UserPreferences {
31    /// Create default preferences (everything enabled)
32    pub fn new() -> Self {
33        Self {
34            disabled_types: HashSet::new(),
35            muted: false,
36        }
37    }
38
39    /// Check if a notification type is enabled
40    pub fn is_type_enabled(&self, notification_type: &str) -> bool {
41        if self.muted {
42            return false;
43        }
44        !self.disabled_types.contains(notification_type)
45    }
46
47    /// Disable a notification type
48    pub fn disable_type(&mut self, notification_type: impl Into<String>) {
49        self.disabled_types.insert(notification_type.into());
50    }
51
52    /// Enable a notification type (remove from disabled set)
53    pub fn enable_type(&mut self, notification_type: &str) {
54        self.disabled_types.remove(notification_type);
55    }
56}
57
58impl Default for UserPreferences {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64/// In-memory notification preferences store
65///
66/// Thread-safe store mapping user IDs to their notification preferences.
67#[derive(Debug)]
68pub struct NotificationPreferencesStore {
69    preferences: RwLock<HashMap<String, UserPreferences>>,
70}
71
72impl NotificationPreferencesStore {
73    /// Create an empty preferences store
74    pub fn new() -> Self {
75        Self {
76            preferences: RwLock::new(HashMap::new()),
77        }
78    }
79
80    /// Get preferences for a user (returns defaults if not set)
81    pub async fn get(&self, user_id: &str) -> UserPreferences {
82        let store = self.preferences.read().await;
83        store.get(user_id).cloned().unwrap_or_default()
84    }
85
86    /// Update preferences for a user
87    pub async fn update(&self, user_id: impl Into<String>, prefs: UserPreferences) {
88        let mut store = self.preferences.write().await;
89        store.insert(user_id.into(), prefs);
90    }
91
92    /// Check if a notification type is enabled for a user
93    ///
94    /// Convenience method that combines get + is_type_enabled.
95    /// Returns true if the user has no preferences set (default = all enabled).
96    pub async fn is_enabled(&self, user_id: &str, notification_type: &str) -> bool {
97        let store = self.preferences.read().await;
98        match store.get(user_id) {
99            Some(prefs) => prefs.is_type_enabled(notification_type),
100            None => true, // Default: all enabled
101        }
102    }
103
104    /// Disable a specific notification type for a user
105    pub async fn disable_type(&self, user_id: &str, notification_type: &str) {
106        let mut store = self.preferences.write().await;
107        let prefs = store
108            .entry(user_id.to_string())
109            .or_insert_with(UserPreferences::new);
110        prefs.disable_type(notification_type);
111    }
112
113    /// Enable a specific notification type for a user
114    pub async fn enable_type(&self, user_id: &str, notification_type: &str) {
115        let mut store = self.preferences.write().await;
116        if let Some(prefs) = store.get_mut(user_id) {
117            prefs.enable_type(notification_type);
118        }
119    }
120
121    /// Mute all notifications for a user
122    pub async fn mute(&self, user_id: &str) {
123        let mut store = self.preferences.write().await;
124        let prefs = store
125            .entry(user_id.to_string())
126            .or_insert_with(UserPreferences::new);
127        prefs.muted = true;
128    }
129
130    /// Unmute all notifications for a user
131    pub async fn unmute(&self, user_id: &str) {
132        let mut store = self.preferences.write().await;
133        if let Some(prefs) = store.get_mut(user_id) {
134            prefs.muted = false;
135        }
136    }
137}
138
139impl Default for NotificationPreferencesStore {
140    fn default() -> Self {
141        Self::new()
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn test_user_prefs_default_all_enabled() {
151        let prefs = UserPreferences::new();
152        assert!(prefs.is_type_enabled("new_follower"));
153        assert!(prefs.is_type_enabled("new_like"));
154        assert!(prefs.is_type_enabled("anything"));
155        assert!(!prefs.muted);
156    }
157
158    #[test]
159    fn test_user_prefs_disable_type() {
160        let mut prefs = UserPreferences::new();
161        prefs.disable_type("new_like");
162
163        assert!(!prefs.is_type_enabled("new_like"));
164        assert!(prefs.is_type_enabled("new_follower"));
165    }
166
167    #[test]
168    fn test_user_prefs_enable_type() {
169        let mut prefs = UserPreferences::new();
170        prefs.disable_type("new_like");
171        assert!(!prefs.is_type_enabled("new_like"));
172
173        prefs.enable_type("new_like");
174        assert!(prefs.is_type_enabled("new_like"));
175    }
176
177    #[test]
178    fn test_user_prefs_muted() {
179        let mut prefs = UserPreferences::new();
180        prefs.muted = true;
181
182        assert!(!prefs.is_type_enabled("new_follower"));
183        assert!(!prefs.is_type_enabled("new_like"));
184        assert!(!prefs.is_type_enabled("anything"));
185    }
186
187    #[tokio::test]
188    async fn test_store_default_all_enabled() {
189        let store = NotificationPreferencesStore::new();
190        assert!(store.is_enabled("user-A", "new_follower").await);
191        assert!(store.is_enabled("user-A", "new_like").await);
192    }
193
194    #[tokio::test]
195    async fn test_store_disable_type() {
196        let store = NotificationPreferencesStore::new();
197        store.disable_type("user-A", "new_like").await;
198
199        assert!(!store.is_enabled("user-A", "new_like").await);
200        assert!(store.is_enabled("user-A", "new_follower").await);
201        // Other users not affected
202        assert!(store.is_enabled("user-B", "new_like").await);
203    }
204
205    #[tokio::test]
206    async fn test_store_enable_type() {
207        let store = NotificationPreferencesStore::new();
208        store.disable_type("user-A", "new_like").await;
209        store.enable_type("user-A", "new_like").await;
210
211        assert!(store.is_enabled("user-A", "new_like").await);
212    }
213
214    #[tokio::test]
215    async fn test_store_mute_unmute() {
216        let store = NotificationPreferencesStore::new();
217        store.mute("user-A").await;
218
219        assert!(!store.is_enabled("user-A", "new_follower").await);
220        assert!(!store.is_enabled("user-A", "new_like").await);
221
222        store.unmute("user-A").await;
223        assert!(store.is_enabled("user-A", "new_follower").await);
224    }
225
226    #[tokio::test]
227    async fn test_store_update_full_preferences() {
228        let store = NotificationPreferencesStore::new();
229
230        let mut prefs = UserPreferences::new();
231        prefs.disable_type("new_follower");
232        prefs.disable_type("new_comment");
233        store.update("user-A", prefs).await;
234
235        assert!(!store.is_enabled("user-A", "new_follower").await);
236        assert!(!store.is_enabled("user-A", "new_comment").await);
237        assert!(store.is_enabled("user-A", "new_like").await);
238    }
239
240    #[tokio::test]
241    async fn test_store_get_returns_defaults() {
242        let store = NotificationPreferencesStore::new();
243        let prefs = store.get("nonexistent").await;
244        assert!(prefs.disabled_types.is_empty());
245        assert!(!prefs.muted);
246    }
247
248    #[tokio::test]
249    async fn test_store_get_returns_updated() {
250        let store = NotificationPreferencesStore::new();
251        store.disable_type("user-A", "new_like").await;
252
253        let prefs = store.get("user-A").await;
254        assert!(prefs.disabled_types.contains("new_like"));
255    }
256}