From 528264283e435f8ecc44eaff4e8f8731f81083b6 Mon Sep 17 00:00:00 2001 From: Cassio Rossi Date: Thu, 30 Apr 2026 17:26:36 -0300 Subject: [PATCH 1/5] feat(#279): restore mark as read/unread for news posts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Brings back the read/unread post tracking UI: - Add `read` and `readAction` to CardContent model - Add "Marcar como lido/não lido" button to card context menu - Apply 0.6 opacity to read posts on news cards and highlights - FeedDB.read is toggled via context menu and auto-set on article open - No widget badge or counter (intentionally excluded) Co-Authored-By: Claude Opus 4.6 --- .../Cards/Model/CardContent.swift | 8 +++++++- .../Cards/Views/Components/Menu/MenuButton.swift | 14 ++++++++++++++ .../Cards/Views/NewsCard.swift | 1 + .../NewsLibrary/Extensions/FeedDBExtensions.swift | 7 +++++++ .../Views/Components/FeedHighlightCardView.swift | 3 ++- 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Cards/Model/CardContent.swift b/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Cards/Model/CardContent.swift index 90c44941..576bc761 100644 --- a/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Cards/Model/CardContent.swift +++ b/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Cards/Model/CardContent.swift @@ -71,7 +71,9 @@ public struct CardContent { public let artworkUrl: String public let urlToShare: String public let favorite: Bool + public let read: Bool public let favoriteAction: () -> Void + public let readAction: (() -> Void)? /// The aspect ratio for the card thumbnail. /// When `nil`, the thumbnail fills its parent frame (useful for externally-sized cards like carousels). @@ -86,8 +88,10 @@ public struct CardContent { artworkUrl: String, urlToShare: String, favorite: Bool, + read: Bool = false, aspectRatio: CGFloat? = 16 / 9, - favoriteAction: @escaping () -> Void + favoriteAction: @escaping () -> Void, + readAction: (() -> Void)? = nil ) { self.type = type self.title = title @@ -97,7 +101,9 @@ public struct CardContent { self.urlToShare = urlToShare self.artworkUrl = artworkUrl self.favorite = favorite + self.read = read self.aspectRatio = aspectRatio self.favoriteAction = favoriteAction + self.readAction = readAction } } diff --git a/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Cards/Views/Components/Menu/MenuButton.swift b/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Cards/Views/Components/Menu/MenuButton.swift index 75d7db89..01b06a57 100644 --- a/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Cards/Views/Components/Menu/MenuButton.swift +++ b/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Cards/Views/Components/Menu/MenuButton.swift @@ -25,6 +25,12 @@ public struct MenuContent: View { } public var body: some View { + if let readAction = data.readAction { + Button(readText, systemImage: readImage) { + readAction() + } + } + Button("Favorito", systemImage: favoriteImage) { data.favoriteAction() } @@ -41,4 +47,12 @@ private extension MenuContent { var favoriteImage: String { data.favorite ? "star.fill" : "star" } + + var readText: String { + data.read ? "Marcar como não lido" : "Marcar como lido" + } + + var readImage: String { + data.read ? "circle" : "circle.fill" + } } diff --git a/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Cards/Views/NewsCard.swift b/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Cards/Views/NewsCard.swift index 29a7a343..6d099a17 100644 --- a/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Cards/Views/NewsCard.swift +++ b/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Cards/Views/NewsCard.swift @@ -18,6 +18,7 @@ public struct NewsCard: View { public var body: some View { Button(action: { onSelect() }, label: { content }) + .opacity(data.read ? 0.6 : 1) } } diff --git a/MacMagazine/Features/NewsLibrary/Sources/NewsLibrary/Extensions/FeedDBExtensions.swift b/MacMagazine/Features/NewsLibrary/Sources/NewsLibrary/Extensions/FeedDBExtensions.swift index 7631b805..f3776edd 100644 --- a/MacMagazine/Features/NewsLibrary/Sources/NewsLibrary/Extensions/FeedDBExtensions.swift +++ b/MacMagazine/Features/NewsLibrary/Sources/NewsLibrary/Extensions/FeedDBExtensions.swift @@ -25,6 +25,7 @@ public extension FeedDB { artworkUrl: self.artworkURL, urlToShare: self.link, favorite: self.favorite, + read: self.read, aspectRatio: aspectRatio, favoriteAction: { [weak self] in guard let self, let context else { return } @@ -35,6 +36,12 @@ public extension FeedDB { buttonId: AnalyticsConstants.ButtonID.newsFavorite.id, screen: screen ?? type.screenName )) + }, + readAction: { [weak self] in + guard let self, let context else { return } + self.read.toggle() + self.modifiedAt = Date() + try? context.save() } ) } diff --git a/MacMagazine/Features/NewsLibrary/Sources/NewsLibrary/Views/Components/FeedHighlightCardView.swift b/MacMagazine/Features/NewsLibrary/Sources/NewsLibrary/Views/Components/FeedHighlightCardView.swift index ed00f6b1..7377db75 100644 --- a/MacMagazine/Features/NewsLibrary/Sources/NewsLibrary/Views/Components/FeedHighlightCardView.swift +++ b/MacMagazine/Features/NewsLibrary/Sources/NewsLibrary/Views/Components/FeedHighlightCardView.swift @@ -29,7 +29,8 @@ public struct FeedHighlightCardView: View { aspectRatio: nil ) GlassCardView(data: data) - .compositingGroup() + .opacity(post.read ? 0.6 : 1) + .compositingGroup() } // MARK: - Init From 452a95f9bc4202fdc0ceb9b7c840f1c9e1c5d6fe Mon Sep 17 00:00:00 2001 From: Cassio Rossi Date: Thu, 30 Apr 2026 17:35:57 -0300 Subject: [PATCH 2/5] feat(settings): add ModelReadable protocol for mark-all-as-read in Settings Restores ModelReadable protocol in MacMagazineLibrary so SettingsLibrary can mark all posts as read without importing FeedLibrary directly. Follows the same pattern as ModelFavoritable. Co-Authored-By: Claude Opus 4.6 --- .../Sources/FeedLibrary/Database/FeedDB.swift | 13 +++++++++++++ .../MacMagazineLibrary/ModelProtocols.swift | 4 ++++ .../ViewModels/PostsVisibilityViewModel.swift | 8 ++++++++ .../Supplementals/PostsVisibilityView.swift | 17 +++++++++++++++++ 4 files changed, 42 insertions(+) diff --git a/MacMagazine/Features/FeedLibrary/Sources/FeedLibrary/Database/FeedDB.swift b/MacMagazine/Features/FeedLibrary/Sources/FeedLibrary/Database/FeedDB.swift index 2790c45b..e68554fd 100644 --- a/MacMagazine/Features/FeedLibrary/Sources/FeedLibrary/Database/FeedDB.swift +++ b/MacMagazine/Features/FeedLibrary/Sources/FeedLibrary/Database/FeedDB.swift @@ -77,6 +77,19 @@ extension FeedDB: ModelFavoritable { } } +extension FeedDB: ModelReadable { + public static func markAllAsRead(using context: ModelContext?) { + let descriptor = FetchDescriptor(predicate: #Predicate { !$0.read }) + guard let context, + let data = try? context.fetch(descriptor) else { return } + for post in data { + post.read = true + post.modifiedAt = Date() + } + try? context.save() + } +} + extension FeedDB: ModelDuplicable { public static func deduplicate(using context: ModelContext?) { let descriptor = FetchDescriptor(sortBy: [SortDescriptor(\FeedDB.pubDate, order: .reverse)]) diff --git a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/ModelProtocols.swift b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/ModelProtocols.swift index 388ce400..22e3ce4a 100644 --- a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/ModelProtocols.swift +++ b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/ModelProtocols.swift @@ -5,6 +5,10 @@ public protocol ModelFavoritable: AnyObject, PersistentModel { static func deleteNonFavorites(using context: ModelContext?) } +public protocol ModelReadable: AnyObject, PersistentModel { + static func markAllAsRead(using context: ModelContext?) +} + public protocol ModelDuplicable: AnyObject, PersistentModel { static func deduplicate(using context: ModelContext?) } diff --git a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/PostsVisibilityViewModel.swift b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/PostsVisibilityViewModel.swift index f84e81c8..1d7c8910 100644 --- a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/PostsVisibilityViewModel.swift +++ b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/PostsVisibilityViewModel.swift @@ -27,6 +27,14 @@ extension PostsVisibilityViewModel { case .keepFavoritesAndStatus: keepFavoritesAndStatus() } } + + @MainActor + func markAllAsRead() { + let context = storage?.sharedModelContainer.mainContext + models.forEach { + ($0 as? any ModelReadable.Type)?.markAllAsRead(using: context) + } + } } private extension PostsVisibilityViewModel { diff --git a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/Supplementals/PostsVisibilityView.swift b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/Supplementals/PostsVisibilityView.swift index 92666793..ecbee4ff 100644 --- a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/Supplementals/PostsVisibilityView.swift +++ b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/Supplementals/PostsVisibilityView.swift @@ -11,6 +11,7 @@ struct PostsVisibilityView: View { @State private var isPresenting = false var body: some View { + markAllAsRead cleanPosts .task { viewModel.set( @@ -22,6 +23,22 @@ struct PostsVisibilityView: View { } private extension PostsVisibilityView { + var markAllAsRead: some View { + Section { + Button(action: { + viewModel.markAllAsRead() + analytics.track(.buttonTap( + buttonId: AnalyticsConstants.ButtonID.cleanPostsOptions.id, + screen: AnalyticsConstants.Screen.settingsPosts.name + )) + }, + label: { + Text("Marcar todos como lidos") + .foregroundStyle(theme.main.tint.color ?? .blue) + }) + } + } + var cleanPosts: some View { Section { Button(action: { From e6023f8e95c4924dc89f53f5148d5d3e72a0d2e9 Mon Sep 17 00:00:00 2001 From: Cassio Rossi Date: Thu, 30 Apr 2026 17:38:42 -0300 Subject: [PATCH 3/5] test(feed): add ModelReadable tests for markAllAsRead Mirrors existing ModelFavoritable test coverage pattern. Co-Authored-By: Claude Opus 4.6 --- .../Tests/FeedLibraryTests/FeedDBTests.swift | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/MacMagazine/Features/FeedLibrary/Tests/FeedLibraryTests/FeedDBTests.swift b/MacMagazine/Features/FeedLibrary/Tests/FeedLibraryTests/FeedDBTests.swift index d7faeb9b..e2c084b3 100644 --- a/MacMagazine/Features/FeedLibrary/Tests/FeedLibraryTests/FeedDBTests.swift +++ b/MacMagazine/Features/FeedLibrary/Tests/FeedLibraryTests/FeedDBTests.swift @@ -321,7 +321,67 @@ struct FeedDBTests { #expect(remaining.isEmpty) } - // MARK: - Integration Tests + // MARK: - ModelReadable Tests + + @Test("markAllAsRead should mark all unread posts as read") + func markAllAsReadMarksUnreadPosts() { + let storage = Database(models: [FeedDB.self], inMemory: true) + storage.context.insert(FeedDB(postId: "1", title: "Unread 1")) + storage.context.insert(FeedDB(postId: "2", title: "Unread 2")) + storage.context.insert(FeedDB(postId: "3", title: "Unread 3")) + try? storage.context.save() + + FeedDB.markAllAsRead(using: storage.context) + + let posts = storage.fetch(FeedDB.self) + #expect(posts.count == 3) + #expect(posts.allSatisfy { $0.read == true }) + } + + @Test("markAllAsRead should not affect already-read posts") + func markAllAsReadPreservesAlreadyRead() { + let storage = Database(models: [FeedDB.self], inMemory: true) + let originalDate = Date(timeIntervalSince1970: 1000) + let alreadyRead = FeedDB(postId: "1", title: "Already Read", modifiedAt: originalDate) + alreadyRead.read = true + storage.context.insert(alreadyRead) + storage.context.insert(FeedDB(postId: "2", title: "Unread")) + try? storage.context.save() + + FeedDB.markAllAsRead(using: storage.context) + + let posts = storage.fetch(FeedDB.self) + #expect(posts.allSatisfy { $0.read == true }) + let readPost = posts.first { $0.postId == "1" } + #expect(readPost?.modifiedAt == originalDate) + } + + @Test("markAllAsRead should handle empty database") + func markAllAsReadHandlesEmptyDatabase() { + let storage = Database(models: [FeedDB.self], inMemory: true) + FeedDB.markAllAsRead(using: storage.context) + #expect(storage.fetch(FeedDB.self).isEmpty) + } + + @Test("markAllAsRead with nil context does not crash") + func markAllAsReadNilContext() { + FeedDB.markAllAsRead(using: nil) + } + + @Test("markAllAsRead should update modifiedAt on newly-read posts") + func markAllAsReadUpdatesModifiedAt() { + let storage = Database(models: [FeedDB.self], inMemory: true) + let oldDate = Date(timeIntervalSince1970: 1000) + let post = FeedDB(postId: "1", title: "Old Post", modifiedAt: oldDate) + storage.context.insert(post) + try? storage.context.save() + + FeedDB.markAllAsRead(using: storage.context) + + let fetched = storage.fetch(FeedDB.self).first + #expect(fetched?.read == true) + #expect(fetched?.modifiedAt != oldDate) + } // MARK: - ModelDuplicable Tests From e0b5dc0b1b6425f40ba800e4f06cba1cd3ab37d3 Mon Sep 17 00:00:00 2001 From: Cassio Rossi Date: Thu, 30 Apr 2026 17:39:59 -0300 Subject: [PATCH 4/5] feat(settings): add confirmation alert after marking all posts as read Shows "Todos os posts marcados como lido" alert with OK button after the mark-all-as-read action completes. Co-Authored-By: Claude Opus 4.6 --- .../Views/Supplementals/PostsVisibilityView.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/Supplementals/PostsVisibilityView.swift b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/Supplementals/PostsVisibilityView.swift index ecbee4ff..4f1468e1 100644 --- a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/Supplementals/PostsVisibilityView.swift +++ b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/Supplementals/PostsVisibilityView.swift @@ -9,6 +9,7 @@ struct PostsVisibilityView: View { @Environment(SettingsViewModel.self) private var settingsViewModel @State private var viewModel = PostsVisibilityViewModel() @State private var isPresenting = false + @State private var showReadConfirmation = false var body: some View { markAllAsRead @@ -27,6 +28,7 @@ private extension PostsVisibilityView { Section { Button(action: { viewModel.markAllAsRead() + showReadConfirmation = true analytics.track(.buttonTap( buttonId: AnalyticsConstants.ButtonID.cleanPostsOptions.id, screen: AnalyticsConstants.Screen.settingsPosts.name @@ -37,6 +39,10 @@ private extension PostsVisibilityView { .foregroundStyle(theme.main.tint.color ?? .blue) }) } + .alert("Todos os posts marcados como lido", + isPresented: $showReadConfirmation) { + Button("OK", role: .cancel) {} + } } var cleanPosts: some View { From 2f241a5765c8b5963c4b5fafbef135e5f79c9811 Mon Sep 17 00:00:00 2001 From: Cassio Rossi Date: Thu, 30 Apr 2026 17:52:42 -0300 Subject: [PATCH 5/5] =?UTF-8?q?feat(settings):=20restore=20highlight-read?= =?UTF-8?q?=20toggle=20in=20Apar=C3=AAncia?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds postRead setting back to SettingsDB - Creates ReadingPreferencesView with toggle + mark-all-as-read button - Moves reading preferences from About into Aparência screen - Adds highlightPostRead environment value for NewsCard/Highlights - Opacity now respects user preference via @Environment(\.highlightPostRead) Co-Authored-By: Claude Opus 4.6 --- .../EnvironmentValuesExtensions.swift | 3 + .../Cards/Views/NewsCard.swift | 3 +- .../Components/FeedHighlightCardView.swift | 3 +- .../Definitions/Databases/SettingsDB.swift | 3 + .../ViewModels/Extensions/Database.swift | 11 +++ .../ViewModels/PostsVisibilityViewModel.swift | 8 --- .../ReadingPreferencesViewModel.swift | 38 ++++++++++ .../ViewModels/SettingsViewModel.swift | 3 + .../SettingsLibrary/Views/SettingsView.swift | 1 + .../Supplementals/PostsVisibilityView.swift | 23 ------ .../ReadingPreferencesView.swift | 70 +++++++++++++++++++ .../MacMagazine/MainApp/MacMagazineApp.swift | 1 + 12 files changed, 134 insertions(+), 33 deletions(-) create mode 100644 MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/ReadingPreferencesViewModel.swift create mode 100644 MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/Supplementals/ReadingPreferencesView.swift diff --git a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/EnvironmentValuesExtensions.swift b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/EnvironmentValuesExtensions.swift index 7b3825c7..eec0b417 100644 --- a/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/EnvironmentValuesExtensions.swift +++ b/MacMagazine/Features/MacMagazineLibrary/Sources/MacMagazineLibrary/EnvironmentValuesExtensions.swift @@ -24,4 +24,7 @@ public extension EnvironmentValues { @MainActor @Entry var removeAds: Bool = false + + @MainActor @Entry + var highlightPostRead: Bool = true } diff --git a/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Cards/Views/NewsCard.swift b/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Cards/Views/NewsCard.swift index 6d099a17..6c71db7f 100644 --- a/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Cards/Views/NewsCard.swift +++ b/MacMagazine/Features/MacMagazineUILibrary/Sources/MacMagazineUILibrary/Cards/Views/NewsCard.swift @@ -3,6 +3,7 @@ import SwiftUI public struct NewsCard: View { @Environment(\.dynamicTypeSize) private var dynamicTypeSize + @Environment(\.highlightPostRead) private var highlightPostRead let data: CardContent let onSelect: () -> Void @@ -18,7 +19,7 @@ public struct NewsCard: View { public var body: some View { Button(action: { onSelect() }, label: { content }) - .opacity(data.read ? 0.6 : 1) + .opacity(data.read && highlightPostRead ? 0.6 : 1) } } diff --git a/MacMagazine/Features/NewsLibrary/Sources/NewsLibrary/Views/Components/FeedHighlightCardView.swift b/MacMagazine/Features/NewsLibrary/Sources/NewsLibrary/Views/Components/FeedHighlightCardView.swift index 7377db75..b807cd2d 100644 --- a/MacMagazine/Features/NewsLibrary/Sources/NewsLibrary/Views/Components/FeedHighlightCardView.swift +++ b/MacMagazine/Features/NewsLibrary/Sources/NewsLibrary/Views/Components/FeedHighlightCardView.swift @@ -16,6 +16,7 @@ public struct FeedHighlightCardView: View { let post: FeedDB @Environment(\.modelContext) private var modelContext + @Environment(\.highlightPostRead) private var highlightPostRead @EnvironmentObject private var analytics: AnalyticsManager // MARK: - Body @@ -29,7 +30,7 @@ public struct FeedHighlightCardView: View { aspectRatio: nil ) GlassCardView(data: data) - .opacity(post.read ? 0.6 : 1) + .opacity(post.read && highlightPostRead ? 0.6 : 1) .compositingGroup() } diff --git a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Definitions/Databases/SettingsDB.swift b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Definitions/Databases/SettingsDB.swift index 5139cd2e..5e54f0fe 100644 --- a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Definitions/Databases/SettingsDB.swift +++ b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Definitions/Databases/SettingsDB.swift @@ -8,6 +8,7 @@ public final class SettingsDB: Equatable { var mode = ColorScheme.system var icon = IconType.normal var notification: String = PushPreferences.all.rawValue + var postRead: Bool = true var subscription: Subscription = Subscription(isPatrao: false, expirationDate: Date()) var modifiedAt: Date = Date() @@ -16,6 +17,7 @@ public final class SettingsDB: Equatable { mode: ColorScheme = .system, icon: IconType = .normal, notification: String = PushPreferences.all.rawValue, + postRead: Bool = true, subscription: Subscription? = nil, modifiedAt: Date = Date() ) { @@ -23,6 +25,7 @@ public final class SettingsDB: Equatable { self.mode = mode self.icon = icon self.notification = notification + self.postRead = postRead self.modifiedAt = modifiedAt let date = Calendar.current.date(byAdding: .day, value: -1, to: Date()) ?? Date() diff --git a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/Extensions/Database.swift b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/Extensions/Database.swift index 65f61655..cb480d21 100644 --- a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/Extensions/Database.swift +++ b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/Extensions/Database.swift @@ -50,6 +50,17 @@ extension Database { try? context.save() } + @MainActor + func update(postRead: Bool) { + if let item = settings { + item.postRead = postRead + item.modifiedAt = Date() + } else { + context.insert(SettingsDB(postRead: postRead)) + } + try? context.save() + } + @MainActor func update(isPatrao: Bool) { let date = Calendar.current.date(byAdding: .day, value: isPatrao ? +30 : -1, to: Date()) ?? Date() diff --git a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/PostsVisibilityViewModel.swift b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/PostsVisibilityViewModel.swift index 1d7c8910..f84e81c8 100644 --- a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/PostsVisibilityViewModel.swift +++ b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/PostsVisibilityViewModel.swift @@ -27,14 +27,6 @@ extension PostsVisibilityViewModel { case .keepFavoritesAndStatus: keepFavoritesAndStatus() } } - - @MainActor - func markAllAsRead() { - let context = storage?.sharedModelContainer.mainContext - models.forEach { - ($0 as? any ModelReadable.Type)?.markAllAsRead(using: context) - } - } } private extension PostsVisibilityViewModel { diff --git a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/ReadingPreferencesViewModel.swift b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/ReadingPreferencesViewModel.swift new file mode 100644 index 00000000..7d6f09a8 --- /dev/null +++ b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/ReadingPreferencesViewModel.swift @@ -0,0 +1,38 @@ +import Foundation +import MacMagazineLibrary +import StorageLibrary +import SwiftData + +@Observable +final class ReadingPreferencesViewModel { + var postRead: Bool = true + var storage: Database? + var models: [any PersistentModel.Type] = [] +} + +extension ReadingPreferencesViewModel { + @MainActor + func set( + storage: Database?, + models: [any PersistentModel.Type] + ) { + self.storage = storage + self.models = models + self.postRead = storage?.settings?.postRead ?? true + } +} + +extension ReadingPreferencesViewModel { + @MainActor + func change(postRead: Bool) async { + storage?.update(postRead: postRead) + } + + @MainActor + func markAllAsRead() { + let context = storage?.sharedModelContainer.mainContext + models.forEach { + ($0 as? any ModelReadable.Type)?.markAllAsRead(using: context) + } + } +} diff --git a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/SettingsViewModel.swift b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/SettingsViewModel.swift index 6781d36b..c70037a2 100644 --- a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/SettingsViewModel.swift +++ b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/ViewModels/SettingsViewModel.swift @@ -12,6 +12,7 @@ final public class SettingsViewModel { public var news: [News] = News.allCases public var isLive = false public var removeAds = false + public var highlightPostRead = false private var storedTabs: [AppTabs] = AppTabs.allCases @@ -39,6 +40,7 @@ final public class SettingsViewModel { self.social = self.storage.customization?.social ?? Social.allCases self.news = self.storage.customization?.news ?? News.allCases self.removeAds = self.storage.settings?.subscription.removeAds ?? false + self.highlightPostRead = self.storage.settings?.postRead ?? true updateSchema() @@ -53,6 +55,7 @@ final public class SettingsViewModel { self?.social = self?.storage.customization?.social ?? Social.allCases self?.news = self?.storage.customization?.news ?? News.allCases self?.removeAds = self?.storage.settings?.subscription.removeAds ?? false + self?.highlightPostRead = self?.storage.settings?.postRead ?? true } } } diff --git a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/SettingsView.swift b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/SettingsView.swift index dce3e3d5..ead56a30 100644 --- a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/SettingsView.swift +++ b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/SettingsView.swift @@ -69,6 +69,7 @@ private extension SettingsView { NavigationLink { List { AppearanceView() + ReadingPreferencesView() IconsView() CustomTabView() CustomSocialView() diff --git a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/Supplementals/PostsVisibilityView.swift b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/Supplementals/PostsVisibilityView.swift index 4f1468e1..92666793 100644 --- a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/Supplementals/PostsVisibilityView.swift +++ b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/Supplementals/PostsVisibilityView.swift @@ -9,10 +9,8 @@ struct PostsVisibilityView: View { @Environment(SettingsViewModel.self) private var settingsViewModel @State private var viewModel = PostsVisibilityViewModel() @State private var isPresenting = false - @State private var showReadConfirmation = false var body: some View { - markAllAsRead cleanPosts .task { viewModel.set( @@ -24,27 +22,6 @@ struct PostsVisibilityView: View { } private extension PostsVisibilityView { - var markAllAsRead: some View { - Section { - Button(action: { - viewModel.markAllAsRead() - showReadConfirmation = true - analytics.track(.buttonTap( - buttonId: AnalyticsConstants.ButtonID.cleanPostsOptions.id, - screen: AnalyticsConstants.Screen.settingsPosts.name - )) - }, - label: { - Text("Marcar todos como lidos") - .foregroundStyle(theme.main.tint.color ?? .blue) - }) - } - .alert("Todos os posts marcados como lido", - isPresented: $showReadConfirmation) { - Button("OK", role: .cancel) {} - } - } - var cleanPosts: some View { Section { Button(action: { diff --git a/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/Supplementals/ReadingPreferencesView.swift b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/Supplementals/ReadingPreferencesView.swift new file mode 100644 index 00000000..ca975981 --- /dev/null +++ b/MacMagazine/Features/SettingsLibrary/Sources/SettingsLibrary/Views/Supplementals/ReadingPreferencesView.swift @@ -0,0 +1,70 @@ +import AnalyticsLibrary +import MacMagazineLibrary +import SwiftUI +import UIComponentsLibrary + +struct ReadingPreferencesView: View { + @EnvironmentObject private var analytics: AnalyticsManager + @Environment(\.theme) private var theme: ThemeColor + @Environment(SettingsViewModel.self) private var settingsViewModel + @State private var viewModel = ReadingPreferencesViewModel() + @State private var showReadConfirmation = false + + var body: some View { + Section { + Toggle("Identificar posts já lidos", isOn: $viewModel.postRead) + .tint(theme.button.primary.color) + + Button(action: { + viewModel.markAllAsRead() + showReadConfirmation = true + analytics.track(.buttonTap( + buttonId: AnalyticsConstants.ButtonID.cleanPostsOptions.id, + screen: AnalyticsConstants.Screen.settingsAppearance.name + )) + }, + label: { + Text("Marcar todos como lidos") + .foregroundStyle(theme.main.tint.color ?? .blue) + }) + } header: { + Text("Leitura") + .font(.headline) + .foregroundColor(theme.text.terciary.color) + } footer: { + Text("Marca visualmente os posts que você já leu") + .accessibilityHidden(true) + } + .alert("Todos os posts marcados como lido", + isPresented: $showReadConfirmation) { + Button("OK", role: .cancel) {} + } + .task { + viewModel.set( + storage: settingsViewModel.storage, + models: settingsViewModel.models + ) + } + .onChange(of: viewModel.postRead) { _, value in + analytics.track(.buttonTap( + buttonId: AnalyticsConstants.ButtonID.theme("\(value)").id, + screen: AnalyticsConstants.Screen.settingsAppearance.name + )) + Task { await viewModel.change(postRead: value) } + } + } +} + +#if DEBUG +import StorageLibrary + +#Preview { + let storage = Database(models: [SettingsDB.self], inMemory: true) + + List { + ReadingPreferencesView() + } + .environment(\.theme, ThemeColor()) + .environment(SettingsViewModel(storage: storage, models: [])) +} +#endif diff --git a/MacMagazine/MacMagazine/MainApp/MacMagazineApp.swift b/MacMagazine/MacMagazine/MainApp/MacMagazineApp.swift index 47903f5d..fac67608 100644 --- a/MacMagazine/MacMagazine/MainApp/MacMagazineApp.swift +++ b/MacMagazine/MacMagazine/MainApp/MacMagazineApp.swift @@ -132,6 +132,7 @@ private extension SceneView { .environment(viewModel.searchViewModel) .environment(podcastPlayerManager) .environment(\.removeAds, viewModel.settingsViewModel.removeAds) + .environment(\.highlightPostRead, viewModel.settingsViewModel.highlightPostRead) .environment(viewModel.sessionState) .environmentObject(viewModel.analytics) .preferredColorScheme(viewModel.settingsViewModel.colorSchema)