diff --git a/.DS_Store b/.DS_Store index 135b76d..8810206 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/Resell.xcodeproj/project.pbxproj b/Resell.xcodeproj/project.pbxproj index 9223a7d..32f2796 100644 --- a/Resell.xcodeproj/project.pbxproj +++ b/Resell.xcodeproj/project.pbxproj @@ -147,7 +147,7 @@ 2E8A5AAE2DBCD16500B1F281 /* FirebaseStorageCombine-Community in Frameworks */ = {isa = PBXBuildFile; productRef = 2E8A5AAD2DBCD16500B1F281 /* FirebaseStorageCombine-Community */; }; 2E8A5AB02DBCD16500B1F281 /* FirebaseVertexAI in Frameworks */ = {isa = PBXBuildFile; productRef = 2E8A5AAF2DBCD16500B1F281 /* FirebaseVertexAI */; }; 2E8A5AB12DBCD68200B1F281 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = D051D8162D7E2E0500C089AF /* GoogleService-Info.plist */; }; - 2E8A5AB22DBCD68700B1F281 /* resell-service.json in Resources */ = {isa = PBXBuildFile; fileRef = D0B4A25F2DA7184C00A1722C /* resell-service.json */; }; + 2E8A5AB22DBCD68700B1F281 /* (null) in Resources */ = {isa = PBXBuildFile; }; 2E8A5AB52DBD5B4300B1F281 /* SavedRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E8A5AB42DBD5B3200B1F281 /* SavedRow.swift */; }; 2E8C3D972DBD8A8B0074BFAB /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C1CE75D2CF6D04F00D38C25 /* NotificationsView.swift */; }; 2E8C3D992DBEE07B0074BFAB /* DetailedFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2E8C3D982DBEE06E0074BFAB /* DetailedFilterView.swift */; }; @@ -155,23 +155,9 @@ 2EBB64182D8B783800CCAC48 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2EBB64172D8B783600CCAC48 /* Filter.swift */; }; 2ECB2F652E749ADD00CAACA2 /* ForYouView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ECB2F642E749AD900CAACA2 /* ForYouView.swift */; }; 2ECB2F672E74E03700CAACA2 /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2ECB2F662E74E02F00CAACA2 /* SearchViewModel.swift */; }; - D037B9E62D7E308C00EF3024 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = D037B9E52D7E308C00EF3024 /* FirebaseAnalytics */; }; - D037B9E82D7E308C00EF3024 /* FirebaseAnalyticsOnDeviceConversion in Frameworks */ = {isa = PBXBuildFile; productRef = D037B9E72D7E308C00EF3024 /* FirebaseAnalyticsOnDeviceConversion */; }; - D037B9EA2D7E308C00EF3024 /* FirebaseAnalyticsWithoutAdIdSupport in Frameworks */ = {isa = PBXBuildFile; productRef = D037B9E92D7E308C00EF3024 /* FirebaseAnalyticsWithoutAdIdSupport */; }; - D037B9EC2D7E308C00EF3024 /* FirebaseAppCheck in Frameworks */ = {isa = PBXBuildFile; productRef = D037B9EB2D7E308C00EF3024 /* FirebaseAppCheck */; }; - D037B9EE2D7E308C00EF3024 /* FirebaseAppDistribution-Beta in Frameworks */ = {isa = PBXBuildFile; productRef = D037B9ED2D7E308C00EF3024 /* FirebaseAppDistribution-Beta */; }; - D037B9F02D7E313100EF3024 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = D037B9EF2D7E313100EF3024 /* FirebaseMessaging */; }; - D037B9F22D7E314D00EF3024 /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = D037B9F12D7E314D00EF3024 /* FirebaseFirestore */; }; - D037B9F42D7E317700EF3024 /* FirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = D037B9F32D7E317700EF3024 /* FirebaseAuth */; }; - D043ED6D2D70CBEB00389DC1 /* GoogleSignIn in Frameworks */ = {isa = PBXBuildFile; productRef = D043ED6C2D70CBEB00389DC1 /* GoogleSignIn */; }; - D043ED6F2D70CBEB00389DC1 /* GoogleSignInSwift in Frameworks */ = {isa = PBXBuildFile; productRef = D043ED6E2D70CBEB00389DC1 /* GoogleSignInSwift */; }; + C6B37F592E970D7700A564DB /* FiltersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6B37F582E970D7700A564DB /* FiltersViewModel.swift */; }; D051D8172D7E2E0500C089AF /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = D051D8162D7E2E0500C089AF /* GoogleService-Info.plist */; }; - D0961AF82D6E28D600DCC293 /* MessageDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0961AF72D6E28D300DCC293 /* MessageDocument.swift */; }; - D0961AFC2D6E42D500DCC293 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0961AFB2D6E42D100DCC293 /* Message.swift */; }; - D0961AFE2D6E47F500DCC293 /* Chat.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0961AFD2D6E47F200DCC293 /* Chat.swift */; }; D0A25DEE2E5804A900607E1F /* EmptyStateModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0A25DED2E5804A900607E1F /* EmptyStateModifier.swift */; }; - D0DAEF292D6F607300641151 /* MessageCluster.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DAEF282D6F606C00641151 /* MessageCluster.swift */; }; - D0DAEF2B2D6FF48800641151 /* MessagesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0DAEF2A2D6FF47E00641151 /* MessagesViewModel.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -304,8 +290,8 @@ 2EBB64172D8B783600CCAC48 /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = ""; }; 2ECB2F642E749AD900CAACA2 /* ForYouView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForYouView.swift; sourceTree = ""; }; 2ECB2F662E74E02F00CAACA2 /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; + C6B37F582E970D7700A564DB /* FiltersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersViewModel.swift; sourceTree = ""; }; D051D8162D7E2E0500C089AF /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; - D063A0182DBC268700F17A9C /* Untitled.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Untitled.swift; sourceTree = ""; }; D0961AF72D6E28D300DCC293 /* MessageDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageDocument.swift; sourceTree = ""; }; D0961AFB2D6E42D100DCC293 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; D0961AFD2D6E47F200DCC293 /* Chat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chat.swift; sourceTree = ""; }; @@ -652,6 +638,7 @@ 2C525B802CB1F195007D5B8E /* SendFeedbackViewModel.swift */, 2C18FFE92CA1E4C900564577 /* SettingsViewModel.swift */, 2C4DD97C2C98D45B0055D0AB /* SetupProfileViewModel.swift */, + C6B37F582E970D7700A564DB /* FiltersViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -857,7 +844,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 2E8A5AB22DBCD68700B1F281 /* resell-service.json in Resources */, + 2E8A5AB22DBCD68700B1F281 /* (null) in Resources */, D051D8172D7E2E0500C089AF /* GoogleService-Info.plist in Resources */, 2C9EAF702CF26DA00010A44C /* Rubik-Regular.ttf in Resources */, 2C9B4D0C2C90EF1D0029DF61 /* Launch Screen.storyboard in Resources */, @@ -915,6 +902,7 @@ 2C9337542C935C9500818C8E /* ChatsView.swift in Sources */, 2E8A5AB52DBD5B4300B1F281 /* SavedRow.swift in Sources */, 2CD7CAB92CE937B10056209E /* Listing.swift in Sources */, + C6B37F592E970D7700A564DB /* FiltersViewModel.swift in Sources */, 2CD6CA8C2CB48286005A4F78 /* PopupModal.swift in Sources */, 2CF3561F2CDE93E00045A173 /* EditProfileViewModel.swift in Sources */, 2C02B3992CC040AE0020DF90 /* PriceInputView.swift in Sources */, diff --git a/Resell/.DS_Store b/Resell/.DS_Store index efa7d13..b513c9b 100644 Binary files a/Resell/.DS_Store and b/Resell/.DS_Store differ diff --git a/Resell/API/NetworkManager.swift b/Resell/API/NetworkManager.swift index f4e4cb1..64a5422 100644 --- a/Resell/API/NetworkManager.swift +++ b/Resell/API/NetworkManager.swift @@ -480,13 +480,21 @@ class NetworkManager: APIClient { return try await post(url: url, body: image) } + // MARK: - Notifications Networking Functions + func getNotifications() async throws -> [Notifications] { + let url = try constructURL(endpoint: "/notif/recent/") + + return try await get(url: url) + } + // func createNotif(notifBody: Notification) async throws -> ListingResponse { // let url = try constructURL(endpoint: "/notif/") // // return try await post(url: url, body: notifBody) // } } + diff --git a/Resell/Models/Notification.swift b/Resell/Models/Notification.swift index 8fc4380..de8dc78 100644 --- a/Resell/Models/Notification.swift +++ b/Resell/Models/Notification.swift @@ -6,16 +6,200 @@ // import Foundation -// Original name Notification overrides Foundation definition... + struct Notifications: Codable { let userID: String let title: String let body: String let data: NotificationData var isRead: Bool = false + let createdAt: Date + let updatedAt: Date } struct NotificationData: Codable { let type: String let messageId: String } + +enum NotificationSection: String, CaseIterable, Identifiable { + case new = "New" + case last7 = "Last 7 Days" + case last30 = "Last 30 Days" + case older = "Older" + + var id: String {rawValue} +} + +enum LoadState { + case idle + case loading + case success + case empty + case error +} + +extension Notifications { + static let dummydata: [Notifications] = { + let now = Date() + let cal = Calendar.current + + func hoursAgo(_ h: Int) -> Date { + now.addingTimeInterval(TimeInterval(-h * 3600)) + } + func daysAgo(_ d: Int) -> Date { + cal.date(byAdding: .day, value: -d, to: now)! + } + + return [ + Notifications( + userID: "user-mateo", + title: "New Message", + body: "You have received a new message from Mateo", + data: NotificationData(type: "messages", messageId: "msg-0001"), + createdAt: hoursAgo(1), + updatedAt: hoursAgo(1) + ), + Notifications( + userID: "user-angelina", + title: "Request Received", + body: "You have a new request from Angelina", + data: NotificationData(type: "requests", messageId: "req-0001"), + createdAt: hoursAgo(5), + updatedAt: hoursAgo(5) + ), + Notifications( + userID: "user-lina", + title: "Bookmarked Item", + body: "Your bookmarked item is back in stock", + data: NotificationData(type: "bookmarks", messageId: "bm-0001"), + createdAt: hoursAgo(12), + updatedAt: hoursAgo(12) + ), +// Notifications( +// userID: "user-jay", +// title: "Listing Activity", +// body: "Your listing has been bookmarked", +// data: NotificationData(type: "your listings", messageId: "yl-0001"), +// createdAt: hoursAgo(20), +// updatedAt: hoursAgo(20) +// ), + + Notifications( + userID: "user-sam", + title: "New Message", + body: "Sam: Is this still available?", + data: NotificationData(type: "messages", messageId: "msg-0002"), + createdAt: daysAgo(1), + updatedAt: daysAgo(1) + ), + Notifications( + userID: "user-zoe", + title: "Request Updated", + body: "Zoe updated her request", + data: NotificationData(type: "requests", messageId: "req-0002"), + createdAt: daysAgo(2), + updatedAt: daysAgo(2) + ), + Notifications( + userID: "user-rio", + title: "Discount Alert", + body: "An item you bookmarked was discounted", + data: NotificationData(type: "bookmarks", messageId: "bm-0002"), + createdAt: daysAgo(3), + updatedAt: daysAgo(3) + ), +// Notifications( +// userID: "user-noah", +// title: "Listing Saved", +// body: "Noah bookmarked your listing", +// data: NotificationData(type: "your listings", messageId: "yl-0002"), +// createdAt: daysAgo(4), +// updatedAt: daysAgo(4) +// ), + Notifications( + userID: "user-ivy", + title: "New Message", + body: "Ivy sent you a follow-up", + data: NotificationData(type: "messages", messageId: "msg-0003"), + createdAt: daysAgo(6), + updatedAt: daysAgo(6) + ), + + Notifications( + userID: "user-ken", + title: "Request Accepted", + body: "Ken accepted your offer", + data: NotificationData(type: "requests", messageId: "req-0003"), + createdAt: daysAgo(7), + updatedAt: daysAgo(7) + ), + Notifications( + userID: "user-luca", + title: "Price Drop", + body: "Bookmarked item dropped in price", + data: NotificationData(type: "bookmarks", messageId: "bm-0003"), + createdAt: daysAgo(10), + updatedAt: daysAgo(10) + ), +// Notifications( +// userID: "user-mia", +// title: "Listing Saved", +// body: "Mia bookmarked your listing", +// data: NotificationData(type: "your listings", messageId: "yl-0003"), +// createdAt: daysAgo(15), +// updatedAt: daysAgo(15) +// ), + Notifications( + userID: "user-omar", + title: "New Message", + body: "Omar sent a question about size", + data: NotificationData(type: "messages", messageId: "msg-0004"), + createdAt: daysAgo(20), + updatedAt: daysAgo(20) + ), + Notifications( + userID: "user-pia", + title: "Request Withdrawn", + body: "Pia withdrew a request", + data: NotificationData(type: "requests", messageId: "req-0004"), + createdAt: daysAgo(28), + updatedAt: daysAgo(28) + ), + + Notifications( + userID: "user-quinn", + title: "Old Message", + body: "Quinn asked about shipping", + data: NotificationData(type: "messages", messageId: "msg-0005"), + createdAt: daysAgo(31), + updatedAt: daysAgo(31) + ), + Notifications( + userID: "user-ryan", + title: "Past Request", + body: "Ryan's request expired", + data: NotificationData(type: "requests", messageId: "req-0005"), + createdAt: daysAgo(45), + updatedAt: daysAgo(45) + ), + Notifications( + userID: "user-sara", + title: "Old Bookmark", + body: "Sara bookmarked a while ago", + data: NotificationData(type: "bookmarks", messageId: "bm-0004"), + createdAt: daysAgo(60), + updatedAt: daysAgo(60) + ), +// Notifications( +// userID: "user-tim", +// title: "Older Listing Activity", +// body: "Tim bookmarked your listing previously", +// data: NotificationData(type: "your listings", messageId: "yl-0004"), +// createdAt: daysAgo(120), +// updatedAt: daysAgo(120) +// ) + ] + }() +} + diff --git a/Resell/Resources/Assets.xcassets/Message Round Icon from Figma.imageset/Contents.json b/Resell/Resources/Assets.xcassets/Message Round Icon from Figma.imageset/Contents.json new file mode 100644 index 0000000..abed00c --- /dev/null +++ b/Resell/Resources/Assets.xcassets/Message Round Icon from Figma.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Message Round Icon from Figma.svg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resell/Resources/Assets.xcassets/Message Round Icon from Figma.imageset/Message Round Icon from Figma.svg b/Resell/Resources/Assets.xcassets/Message Round Icon from Figma.imageset/Message Round Icon from Figma.svg new file mode 100644 index 0000000..aa6c56d --- /dev/null +++ b/Resell/Resources/Assets.xcassets/Message Round Icon from Figma.imageset/Message Round Icon from Figma.svg @@ -0,0 +1,3 @@ + + + diff --git a/Resell/Resources/Assets.xcassets/bell.imageset/Bell Icon from Figma.svg b/Resell/Resources/Assets.xcassets/bell.imageset/Bell Icon from Figma.svg new file mode 100644 index 0000000..73ea424 --- /dev/null +++ b/Resell/Resources/Assets.xcassets/bell.imageset/Bell Icon from Figma.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Resell/Resources/Assets.xcassets/bell.imageset/Contents.json b/Resell/Resources/Assets.xcassets/bell.imageset/Contents.json index e7387d0..f1e3ce4 100644 --- a/Resell/Resources/Assets.xcassets/bell.imageset/Contents.json +++ b/Resell/Resources/Assets.xcassets/bell.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "bell.png", + "filename" : "Bell Icon from Figma.svg", "idiom" : "universal", "scale" : "1x" }, diff --git a/Resell/Resources/Assets.xcassets/bell.imageset/bell.png b/Resell/Resources/Assets.xcassets/bell.imageset/bell.png deleted file mode 100644 index 6e2b3ac..0000000 Binary files a/Resell/Resources/Assets.xcassets/bell.imageset/bell.png and /dev/null differ diff --git a/Resell/Resources/Assets.xcassets/ellipse-notification.imageset/Contents.json b/Resell/Resources/Assets.xcassets/ellipse-notification.imageset/Contents.json new file mode 100644 index 0000000..5e28e0a --- /dev/null +++ b/Resell/Resources/Assets.xcassets/ellipse-notification.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "Ellipse 28 Resell FA25.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resell/Resources/Assets.xcassets/ellipse-notification.imageset/Ellipse 28 Resell FA25.png b/Resell/Resources/Assets.xcassets/ellipse-notification.imageset/Ellipse 28 Resell FA25.png new file mode 100644 index 0000000..64e4225 Binary files /dev/null and b/Resell/Resources/Assets.xcassets/ellipse-notification.imageset/Ellipse 28 Resell FA25.png differ diff --git a/Resell/Resources/Assets.xcassets/read-notification.imageset/Contents.json b/Resell/Resources/Assets.xcassets/read-notification.imageset/Contents.json index 6d3f655..62cdc74 100644 --- a/Resell/Resources/Assets.xcassets/read-notification.imageset/Contents.json +++ b/Resell/Resources/Assets.xcassets/read-notification.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "mage_message-round.png", + "filename" : "read-notification-dot.svg", "idiom" : "universal", "scale" : "1x" }, diff --git a/Resell/Resources/Assets.xcassets/read-notification.imageset/mage_message-round.png b/Resell/Resources/Assets.xcassets/read-notification.imageset/mage_message-round.png deleted file mode 100644 index 5d8153f..0000000 Binary files a/Resell/Resources/Assets.xcassets/read-notification.imageset/mage_message-round.png and /dev/null differ diff --git a/Resell/Resources/Assets.xcassets/read-notification.imageset/read-notification-dot.svg b/Resell/Resources/Assets.xcassets/read-notification.imageset/read-notification-dot.svg new file mode 100644 index 0000000..aa6c56d --- /dev/null +++ b/Resell/Resources/Assets.xcassets/read-notification.imageset/read-notification-dot.svg @@ -0,0 +1,3 @@ + + + diff --git a/Resell/ViewModels/NotificationsViewModel.swift b/Resell/ViewModels/NotificationsViewModel.swift index bfa29a4..1bc2e65 100644 --- a/Resell/ViewModels/NotificationsViewModel.swift +++ b/Resell/ViewModels/NotificationsViewModel.swift @@ -14,7 +14,11 @@ class NotificationsViewModel: ObservableObject { // MARK: - Properties - @Published var selectedTab: String = "All" + @Published var selectedTab: String = "All" { + didSet { recalcLoadState() } + } + + // MARK: - What is this for @Published var unreadNotifs: [String: Int] = [ "All": 10, "Messages": 2, @@ -23,39 +27,51 @@ class NotificationsViewModel: ObservableObject { "Your Listings": 5 ] - @Published var notifications: [Notifications] = [ - Notifications( - userID: "381527oef-42b4-4fdd-b074-dfwbejko229", - title: "New Message", - body: "You have received a new message from Mateo", - data: NotificationData(type: "messages", messageId: "134841-42b4-4fdd-b074-jkfale") - ), - Notifications( - userID: "381527oef-42b4-4fdd-b074-dfwbejko229", - title: "Request Received", - body: "You have a new request from Angelina", - data: NotificationData(type: "requests", messageId: "1") - ), - Notifications( - userID: "381527oef-42b4-4fdd-b074-dfwbejko229", - title: "Bookmarked Item", - body: "Your bookmarked item is back in stock", - data: NotificationData(type: "bookmarks", messageId: "2") - ), - Notifications( - userID: "381527oef-42b4-4fdd-b074-dfwbejko229", - title: "Order Update", - body: "Your listing has been bookmarked", - data: NotificationData(type: "your listings", messageId: "3") - ) - ] - + @Published var notifications: [Notifications] = Notifications.dummydata { + didSet { recalcLoadState() } + } + // MARK: - turn back to .idle when we use actual backend networking + @Published var loadState: LoadState = .success + var filteredNotifications: [Notifications] { if selectedTab == "All" { return notifications } else { - return notifications.filter { $0.data.type.lowercased() == selectedTab.lowercased() } + return notifications.filter { $0.data.type.lowercased() == selectedTab.lowercased() } } + } + + private func recalcLoadState() { + switch loadState { + case .loading, .error: + return + default: break } + loadState = filteredNotifications.isEmpty ? .empty : .success + } + + var groupedFilteredNotifications: [NotificationSection: [Notifications]] { + let source = filteredNotifications + let now = Date() + let cal = Calendar.current + + var dict: [NotificationSection: [Notifications]] = [:] + + for noti in source { + let days = cal.dateComponents([.day], from: noti.createdAt, to: now).day ?? 0 + let section: NotificationSection + switch days { + case 0: section = .new + case 1...6: section = .last7 + case 7...29: section = .last30 + default: section = .older + } + dict[section, default: []].append(noti) + } + + for section in dict.keys { + dict[section]?.sort { $0.createdAt > $1.createdAt } + } + return dict } // MARK: - Functions @@ -66,21 +82,38 @@ class NotificationsViewModel: ObservableObject { notifications[index].isRead = true } } + + func fetchNotifications() { + Task { + loadState = .loading + do { + // MARK: - Check with backend to see if there are actually any notis + self.notifications = try await NetworkManager.shared.getNotifications() + } catch { + NetworkManager.shared.logger.error("Error in NotificationsViewModel.fetchNotifications: \(error.localizedDescription)") + loadState = .error + } + } + } /// Simulate fetching data - func fetchNotifications() { + func dummyFetchNotifications() { notifications = [ Notifications( userID: "381527oef-42b4-4fdd-b074-dfwbejko229", title: "New Message", body: "You have received a new message from Mateo", - data: NotificationData(type: "messages", messageId: "12345") + data: NotificationData(type: "messages", messageId: "12345"), + createdAt: Date(), + updatedAt: Date() ), Notifications( userID: "381527oef-42b4-4fdd-b074-dfwbejko229", title: "New Request", body: "You have a new request from Angelina", - data: NotificationData(type: "requests", messageId: "23456") + data: NotificationData(type: "requests", messageId: "23456"), + createdAt: Date(), + updatedAt: Date() ) ] } diff --git a/Resell/Views/Home/HomeView.swift b/Resell/Views/Home/HomeView.swift index af34868..d6e1747 100644 --- a/Resell/Views/Home/HomeView.swift +++ b/Resell/Views/Home/HomeView.swift @@ -147,7 +147,13 @@ struct HomeView: View { Button(action: { router.push(.notifications) }, label: { - Icon(image: "bell") + ZStack (alignment: .topTrailing){ + Icon(image: "bell") + Circle() + .fill(Color.red) + .frame(width: 8, height: 8) + .offset(x: -1, y: 4) + } }) } .padding(.horizontal, Constants.Spacing.horizontalPadding) diff --git a/Resell/Views/Home/NotificationsView.swift b/Resell/Views/Home/NotificationsView.swift index 0bc35ea..751a0fe 100644 --- a/Resell/Views/Home/NotificationsView.swift +++ b/Resell/Views/Home/NotificationsView.swift @@ -13,27 +13,84 @@ struct NotificationsView: View { @EnvironmentObject var router: Router @StateObject private var viewModel = NotificationsViewModel() + + private let relativeFormatter: RelativeDateTimeFormatter = { + let f = RelativeDateTimeFormatter() + f.unitsStyle = .full + return f + }() - + private func timeAgo(_ date: Date) -> String { + relativeFormatter.localizedString(for: date, relativeTo: Date()) + } + var body: some View { VStack { filtersView .padding(.leading, 15) - Text("New") - .font(.headline) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.leading, 30) - .padding(.vertical, 10) - List(viewModel.filteredNotifications, id: \.data.messageId) { notification in - notificationView(for: notification) - .listRowInsets(EdgeInsets()) - .listRowSeparator(.hidden) + .zIndex(1) + + switch viewModel.loadState { + case .idle: + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + .offset(y: -60) + case .loading: + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + .offset(y: -60) + case .success: + List { + ForEach(NotificationSection.allCases) { section in + if let items = viewModel.groupedFilteredNotifications[section], !items.isEmpty { + Text(section.rawValue) + .font(.custom("Rubik-Medium", size: 18)) + .foregroundColor(.primary) + .textCase(nil) + .padding(.leading, 8) + .padding(.top, 5) + .listRowSeparator(.hidden) + ForEach(items, id: \.data.messageId) { notification in + notificationView(for: notification) + .listRowInsets(EdgeInsets()) + .listRowSeparator(.hidden) + } + } + } + } + .listStyle(.plain) + .listRowSeparator(.hidden) + case .empty: + VStack (alignment: .center, spacing: 16) { + Text("You're all caught up!") + .font(.custom("Rubik-Medium", size: 22)) + .foregroundStyle(.black) + Text("No new notifications right now") + .font(.custom("Rubik", size: 18)) + .foregroundStyle(.gray) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + .offset(y: -60) + case .error: + VStack (alignment: .center, spacing: 16) { + Text("Something went wrong!") + .font(.custom("Rubik-Medium", size: 22)) + .foregroundStyle(.black) + Text("Please try again. If this problem persists, feel free to let us know") + .font(.custom("Rubik", size: 18)) + .foregroundStyle(.gray) + } + .frame(maxWidth: 312, maxHeight: .infinity, alignment: .center) + .offset(y: -60) } - .listStyle(PlainListStyle()) } .padding(.top, 5) .padding(.vertical, 1) .navigationTitle("Notifications") + // MARK: - Uncomment when confirm notification data in backend +// .onAppear { +// viewModel.fetchNotifications() +// } } // Creates the filter for notifications sorting @@ -48,6 +105,7 @@ struct NotificationsView: View { } } .padding(.top, 20) + .padding(.bottom, 1) } .padding(.leading, 15) } @@ -65,30 +123,31 @@ struct NotificationsView: View { Spacer() notifText(for: notification) .font(.system(size: 14)) - Text("5 days ago") + Text(timeAgo(notification.createdAt)) .font(.footnote) .foregroundColor(.gray) Spacer() } - .padding(.leading, 20) + .padding(.leading, 10) Spacer() } - .padding(15) - .padding(.horizontal, 15) - .background(notification.isRead ? Color.white : Color.purple.opacity(0.1)) - .swipeActions(edge: .leading) { + .padding(12) + .padding(.horizontal, 12) + .contentShape(Rectangle()) + .background(notification.isRead ? Color.white : Constants.Colors.resellPurple.opacity(0.1)) + .swipeActions(edge: .leading, allowsFullSwipe: true) { Button(action: { viewModel.markAsRead(notification: notification) }) { Image("read-notification") } - .tint(Color.purple.opacity(0.7)) + .tint(Constants.Colors.resellPurple.opacity(0.7)) } } private func notifText(for notification: Notifications) -> some View { switch notification.data.type { - case "message": + case "messages": return Text(notification.userID).bold() + Text(" sent you a message") case "requests": return Text("Your request for ") @@ -104,6 +163,5 @@ struct NotificationsView: View { return Text(notification.title) } } - }