guts_realtime/
notification.rs

1//! Notification types for user alerts.
2
3use serde::{Deserialize, Serialize};
4
5/// A notification for a user.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Notification {
8    /// Unique notification ID.
9    pub id: String,
10    /// User who receives this notification.
11    pub user_id: String,
12    /// Notification type.
13    pub notification_type: NotificationType,
14    /// Brief title.
15    pub title: String,
16    /// Notification body/description.
17    pub body: String,
18    /// URL to navigate to when clicked.
19    pub url: Option<String>,
20    /// Whether the notification has been read.
21    pub read: bool,
22    /// When the notification was created (Unix timestamp).
23    pub created_at: u64,
24    /// Additional metadata.
25    #[serde(default)]
26    pub metadata: NotificationMetadata,
27}
28
29impl Notification {
30    /// Create a new notification.
31    pub fn new(
32        user_id: String,
33        notification_type: NotificationType,
34        title: String,
35        body: String,
36    ) -> Self {
37        Self {
38            id: uuid::Uuid::new_v4().to_string(),
39            user_id,
40            notification_type,
41            title,
42            body,
43            url: None,
44            read: false,
45            created_at: Self::now(),
46            metadata: NotificationMetadata::default(),
47        }
48    }
49
50    /// Set the URL.
51    pub fn with_url(mut self, url: String) -> Self {
52        self.url = Some(url);
53        self
54    }
55
56    /// Set metadata.
57    pub fn with_metadata(mut self, metadata: NotificationMetadata) -> Self {
58        self.metadata = metadata;
59        self
60    }
61
62    /// Mark as read.
63    pub fn mark_read(&mut self) {
64        self.read = true;
65    }
66
67    fn now() -> u64 {
68        std::time::SystemTime::now()
69            .duration_since(std::time::UNIX_EPOCH)
70            .unwrap_or_default()
71            .as_secs()
72    }
73}
74
75/// Types of notifications.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
77#[serde(rename_all = "snake_case")]
78pub enum NotificationType {
79    /// Mentioned in a comment.
80    Mention,
81    /// Assigned to an issue or PR.
82    Assigned,
83    /// Review requested on a PR.
84    ReviewRequested,
85    /// PR you authored was merged.
86    PrMerged,
87    /// PR you authored was closed.
88    PrClosed,
89    /// New comment on your issue/PR.
90    NewComment,
91    /// Review submitted on your PR.
92    ReviewSubmitted,
93    /// Issue you authored was closed.
94    IssueClosed,
95    /// Push to a watched repository.
96    Push,
97    /// Team added to repository.
98    TeamAdded,
99    /// Collaborator added.
100    CollaboratorAdded,
101    /// Workflow completed (CI/CD).
102    WorkflowCompleted,
103}
104
105impl NotificationType {
106    /// Get a human-readable label.
107    pub fn label(&self) -> &'static str {
108        match self {
109            NotificationType::Mention => "Mentioned",
110            NotificationType::Assigned => "Assigned",
111            NotificationType::ReviewRequested => "Review Requested",
112            NotificationType::PrMerged => "PR Merged",
113            NotificationType::PrClosed => "PR Closed",
114            NotificationType::NewComment => "New Comment",
115            NotificationType::ReviewSubmitted => "Review Submitted",
116            NotificationType::IssueClosed => "Issue Closed",
117            NotificationType::Push => "New Push",
118            NotificationType::TeamAdded => "Team Added",
119            NotificationType::CollaboratorAdded => "Collaborator Added",
120            NotificationType::WorkflowCompleted => "Workflow Completed",
121        }
122    }
123
124    /// Get icon hint for the notification type.
125    pub fn icon(&self) -> &'static str {
126        match self {
127            NotificationType::Mention => "at",
128            NotificationType::Assigned => "user",
129            NotificationType::ReviewRequested => "eye",
130            NotificationType::PrMerged => "git-merge",
131            NotificationType::PrClosed => "git-pull-request",
132            NotificationType::NewComment => "message",
133            NotificationType::ReviewSubmitted => "check-circle",
134            NotificationType::IssueClosed => "issue-closed",
135            NotificationType::Push => "upload",
136            NotificationType::TeamAdded => "users",
137            NotificationType::CollaboratorAdded => "user-plus",
138            NotificationType::WorkflowCompleted => "activity",
139        }
140    }
141}
142
143/// Additional metadata for notifications.
144#[derive(Debug, Clone, Default, Serialize, Deserialize)]
145pub struct NotificationMetadata {
146    /// Repository key if applicable.
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub repo_key: Option<String>,
149    /// PR number if applicable.
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub pr_number: Option<u64>,
152    /// Issue number if applicable.
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub issue_number: Option<u64>,
155    /// Username of the actor who triggered this.
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub actor: Option<String>,
158}
159
160/// User notification preferences.
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct NotificationPreferences {
163    /// User ID.
164    pub user_id: String,
165    /// Enabled notification types.
166    pub enabled_types: Vec<NotificationType>,
167    /// Whether to receive email notifications.
168    pub email_enabled: bool,
169    /// Whether to receive web push notifications.
170    pub push_enabled: bool,
171}
172
173impl Default for NotificationPreferences {
174    fn default() -> Self {
175        Self {
176            user_id: String::new(),
177            enabled_types: vec![
178                NotificationType::Mention,
179                NotificationType::Assigned,
180                NotificationType::ReviewRequested,
181                NotificationType::PrMerged,
182                NotificationType::NewComment,
183                NotificationType::ReviewSubmitted,
184            ],
185            email_enabled: false,
186            push_enabled: false,
187        }
188    }
189}
190
191impl NotificationPreferences {
192    /// Create preferences for a user with defaults.
193    pub fn for_user(user_id: String) -> Self {
194        Self {
195            user_id,
196            ..Default::default()
197        }
198    }
199
200    /// Check if a notification type is enabled.
201    pub fn is_enabled(&self, notification_type: NotificationType) -> bool {
202        self.enabled_types.contains(&notification_type)
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_notification_creation() {
212        let notification = Notification::new(
213            "alice".to_string(),
214            NotificationType::Mention,
215            "You were mentioned".to_string(),
216            "Bob mentioned you in a comment".to_string(),
217        );
218
219        assert!(!notification.id.is_empty());
220        assert_eq!(notification.user_id, "alice");
221        assert_eq!(notification.notification_type, NotificationType::Mention);
222        assert!(!notification.read);
223    }
224
225    #[test]
226    fn test_notification_with_url() {
227        let notification = Notification::new(
228            "alice".to_string(),
229            NotificationType::NewComment,
230            "New comment".to_string(),
231            "Bob commented on your PR".to_string(),
232        )
233        .with_url("/alice/myrepo/pull/1#comment-5".to_string());
234
235        assert_eq!(
236            notification.url,
237            Some("/alice/myrepo/pull/1#comment-5".to_string())
238        );
239    }
240
241    #[test]
242    fn test_notification_mark_read() {
243        let mut notification = Notification::new(
244            "alice".to_string(),
245            NotificationType::Mention,
246            "Test".to_string(),
247            "Test body".to_string(),
248        );
249
250        assert!(!notification.read);
251        notification.mark_read();
252        assert!(notification.read);
253    }
254
255    #[test]
256    fn test_notification_preferences() {
257        let prefs = NotificationPreferences::for_user("alice".to_string());
258
259        assert!(prefs.is_enabled(NotificationType::Mention));
260        assert!(prefs.is_enabled(NotificationType::ReviewRequested));
261        assert!(!prefs.is_enabled(NotificationType::Push));
262    }
263
264    #[test]
265    fn test_notification_type_label() {
266        assert_eq!(NotificationType::Mention.label(), "Mentioned");
267        assert_eq!(NotificationType::PrMerged.label(), "PR Merged");
268    }
269}