1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Notification {
8 pub id: String,
10 pub user_id: String,
12 pub notification_type: NotificationType,
14 pub title: String,
16 pub body: String,
18 pub url: Option<String>,
20 pub read: bool,
22 pub created_at: u64,
24 #[serde(default)]
26 pub metadata: NotificationMetadata,
27}
28
29impl Notification {
30 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 pub fn with_url(mut self, url: String) -> Self {
52 self.url = Some(url);
53 self
54 }
55
56 pub fn with_metadata(mut self, metadata: NotificationMetadata) -> Self {
58 self.metadata = metadata;
59 self
60 }
61
62 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
77#[serde(rename_all = "snake_case")]
78pub enum NotificationType {
79 Mention,
81 Assigned,
83 ReviewRequested,
85 PrMerged,
87 PrClosed,
89 NewComment,
91 ReviewSubmitted,
93 IssueClosed,
95 Push,
97 TeamAdded,
99 CollaboratorAdded,
101 WorkflowCompleted,
103}
104
105impl NotificationType {
106 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 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
145pub struct NotificationMetadata {
146 #[serde(skip_serializing_if = "Option::is_none")]
148 pub repo_key: Option<String>,
149 #[serde(skip_serializing_if = "Option::is_none")]
151 pub pr_number: Option<u64>,
152 #[serde(skip_serializing_if = "Option::is_none")]
154 pub issue_number: Option<u64>,
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub actor: Option<String>,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct NotificationPreferences {
163 pub user_id: String,
165 pub enabled_types: Vec<NotificationType>,
167 pub email_enabled: bool,
169 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 pub fn for_user(user_id: String) -> Self {
194 Self {
195 user_id,
196 ..Default::default()
197 }
198 }
199
200 pub fn is_enabled(&self, notification_type: NotificationType) -> bool {
202 self.enabled_types.contains(¬ification_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}