From 394e9a92c301842f2538c689e18e8b6df41e10b6 Mon Sep 17 00:00:00 2001
From: Wilhelm Oks <oksw@MBP-von-Wilhelm.fritz.box>
Date: Thu, 12 Dec 2024 16:33:46 +0100
Subject: [PATCH] Models WIP: Notification, NotificationFeed

---
 .../SwiftDevRant/Models/Notification.swift    | 86 +++++++++++++++++++
 .../NotificationFeed.UnreadNumbers.swift      | 50 +++++++++++
 .../Models/NotificationFeed.UserInfo.swift    | 49 +++++++++++
 .../Models/NotificationFeed.swift             | 51 +++++++++++
 4 files changed, 236 insertions(+)
 create mode 100644 Sources/SwiftDevRant/Models/Notification.swift
 create mode 100644 Sources/SwiftDevRant/Models/NotificationFeed.UnreadNumbers.swift
 create mode 100644 Sources/SwiftDevRant/Models/NotificationFeed.UserInfo.swift
 create mode 100644 Sources/SwiftDevRant/Models/NotificationFeed.swift

diff --git a/Sources/SwiftDevRant/Models/Notification.swift b/Sources/SwiftDevRant/Models/Notification.swift
new file mode 100644
index 0000000..e5334aa
--- /dev/null
+++ b/Sources/SwiftDevRant/Models/Notification.swift
@@ -0,0 +1,86 @@
+import Foundation
+
+/// A notification about activities in a rant or a comment.
+public struct Notification: Hashable, Identifiable {
+    public enum Kind: String {
+        /// An upvote for a rant.
+        case rantUpvote = "content_vote"
+        
+        /// An upvote for a comment.
+        case commentUpvote = "comment_vote"
+        
+        /// A new comment in one of the logged in user's rants.
+        case newCommentInOwnRant = "comment_content"
+        
+        /// A new comment in a rant that the logged in user has commented in.
+        case newComment = "comment_discuss"
+        
+        /// A mention of the logged in user in a comment.
+        case mentionInComment = "comment_mention"
+        
+        /// A new rant posted by someone that the logged in user is subscribed to.
+        case newRantOfSubscribedUser = "rant_sub"
+    }
+    
+    /// The id of the rant associated with this notification.
+    public let rantId: Int
+    
+    /// The id of the comment associated with this notification, if this notification is for a comment.
+    public let commentId: Int?
+    
+    /// The time when this notification was created.
+    public let created: Date
+    
+    /// True if the user has already read this notification.
+    public let read: Bool
+    
+    /// The type of this notification.
+    public let kind: Kind
+    
+    /// The id of the user who triggered the notification.
+    public let userId: Int
+    
+    public var id: String {
+        [
+            String(rantId),
+            commentId.flatMap{ String($0) } ?? "-",
+            String(Int(created.timeIntervalSince1970)),
+            String(read),
+            kind.rawValue,
+            String(userId)
+        ].joined(separator: "|")
+    }
+    
+    public init(rantId: Int, commentId: Int?, created: Date, read: Bool, kind: Notification.Kind, userId: Int) {
+        self.rantId = rantId
+        self.commentId = commentId
+        self.created = created
+        self.read = read
+        self.kind = kind
+        self.userId = userId
+    }
+}
+
+extension Notification {
+    struct CodingData: Codable {
+        let rant_id: Int
+        let comment_id: Int?
+        let created_time: Int
+        let read: Int
+        let type: String
+        let uid: Int
+    }
+}
+
+extension Notification.CodingData {
+    var decoded: Notification {
+        .init(
+            rantId: rant_id,
+            commentId: comment_id,
+            created: Date(timeIntervalSince1970: TimeInterval(created_time)),
+            read: read != 0,
+            kind: .init(rawValue: type) ?? .newComment,
+            userId: uid
+        )
+    }
+}
diff --git a/Sources/SwiftDevRant/Models/NotificationFeed.UnreadNumbers.swift b/Sources/SwiftDevRant/Models/NotificationFeed.UnreadNumbers.swift
new file mode 100644
index 0000000..bd4ac99
--- /dev/null
+++ b/Sources/SwiftDevRant/Models/NotificationFeed.UnreadNumbers.swift
@@ -0,0 +1,50 @@
+public extension NotificationFeed {
+    /// Holds numbers of unread notifications for each type of notification.
+    struct UnreadNumbers: Decodable, Hashable {
+        /// The total number of unread notifications
+        public let all: Int
+        
+        /// The number of unread commets.
+        public let comments: Int
+        
+        /// The number of unread mentions.
+        public let mentions: Int
+        
+        /// The number of unread rants from users which the logged in user is subscribed to.
+        public let subscriptions: Int
+        
+        /// The number of unread upvotes.
+        public let upvotes: Int
+        
+        public init(all: Int, comments: Int, mentions: Int, subscriptions: Int, upvotes: Int) {
+            self.all = all
+            self.comments = comments
+            self.mentions = mentions
+            self.subscriptions = subscriptions
+            self.upvotes = upvotes
+        }
+    }
+}
+
+extension NotificationFeed.UnreadNumbers {
+    struct CodingData: Codable {
+        let all: Int
+        let comments: Int
+        let mentions: Int
+        let subs: Int
+        let upvotes: Int
+        //let total: Int //Not needed because it's the same as `all`.
+    }
+}
+
+extension NotificationFeed.UnreadNumbers.CodingData {
+    var decoded: NotificationFeed.UnreadNumbers {
+        .init(
+            all: all,
+            comments: comments,
+            mentions: mentions,
+            subscriptions: subs,
+            upvotes: upvotes
+        )
+    }
+}
diff --git a/Sources/SwiftDevRant/Models/NotificationFeed.UserInfo.swift b/Sources/SwiftDevRant/Models/NotificationFeed.UserInfo.swift
new file mode 100644
index 0000000..d4eaf27
--- /dev/null
+++ b/Sources/SwiftDevRant/Models/NotificationFeed.UserInfo.swift
@@ -0,0 +1,49 @@
+public extension NotificationFeed {
+    struct UserInfo: Hashable {
+        public let avatar: User.Avatar
+        public let username: String
+        public let userId: String //TODO: why is this String? The other user ids are Int.
+        
+        public init(avatar: User.Avatar, username: String, userId: String) {
+            self.avatar = avatar
+            self.username = username
+            self.userId = userId
+        }
+    }
+}
+
+extension NotificationFeed.UserInfo {
+    struct CodingData: Decodable {
+        struct Container: Decodable {
+            let array: [CodingData]
+        }
+        
+        let avatar: User.Avatar.CodingData
+        let name: String
+        let uidForUsername: String //TODO: why is this String? The other user ids are Int.
+        
+        private enum CodingKeys: CodingKey {
+            case avatar
+            case name
+        }
+        
+        init(from decoder: Decoder) throws {
+            let values = try decoder.container(keyedBy: CodingKeys.self)
+            
+            avatar = try values.decode(User.Avatar.CodingData.self, forKey: .avatar)
+            name = try values.decode(String.self, forKey: .name)
+            
+            uidForUsername = values.codingPath[values.codingPath.endIndex - 1].stringValue //TODO: wtf is this? Check if it can be made simpler and easier to understand.
+        }
+    }
+}
+
+extension NotificationFeed.UserInfo.CodingData {
+    var decoded: NotificationFeed.UserInfo {
+        .init(
+            avatar: avatar.decoded,
+            username: name,
+            userId: uidForUsername
+        )
+    }
+}
diff --git a/Sources/SwiftDevRant/Models/NotificationFeed.swift b/Sources/SwiftDevRant/Models/NotificationFeed.swift
new file mode 100644
index 0000000..2f681a4
--- /dev/null
+++ b/Sources/SwiftDevRant/Models/NotificationFeed.swift
@@ -0,0 +1,51 @@
+import Foundation
+
+/// Contains a list of all notifications for the logged in user and the numbers of unread notifications.
+public struct NotificationFeed: Hashable {
+    public enum Categories: String, CaseIterable {
+        case all = ""
+        case upvotes = "upvotes"
+        case mentions = "mentions"
+        case comments = "comments"
+        case subscriptions = "subs"
+    }
+    
+    /// The time when the notifications were last checked.
+    public let lastChecked: Date
+    
+    /// The list of all notifications for the logged in user.
+    public let notifications: [Notification]
+    
+    /// The numbers of unread notifications.
+    public let unreadNumbers: UnreadNumbers
+    
+    /// Infos about the user name and avatar for each user id.
+    public let userInfos: [UserInfo]
+    
+    public init(lastChecked: Date, notifications: [Notification], unreadNumbers: NotificationFeed.UnreadNumbers, userInfos: [UserInfo]) {
+        self.lastChecked = lastChecked
+        self.notifications = notifications
+        self.unreadNumbers = unreadNumbers
+        self.userInfos = userInfos
+    }
+}
+
+extension NotificationFeed {
+    struct CodingData: Decodable {
+        let check_time: Int
+        let items: [Notification.CodingData]
+        let unread: UnreadNumbers.CodingData
+        let username_map: UserInfo.CodingData.Container
+    }
+}
+
+extension NotificationFeed.CodingData {
+    var decoded: NotificationFeed {
+        .init(
+            lastChecked: Date(timeIntervalSince1970: TimeInterval(check_time)),
+            notifications: items.map(\.decoded),
+            unreadNumbers: unread.decoded,
+            userInfos: username_map.array.map(\.decoded)
+        )
+    }
+}