Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ extension FeedDB: ModelFavoritable {
}
}

extension FeedDB: ModelReadable {
public static func markAllAsRead(using context: ModelContext?) {
let descriptor = FetchDescriptor<FeedDB>(predicate: #Predicate<FeedDB> { !$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<FeedDB>(sortBy: [SortDescriptor(\FeedDB.pubDate, order: .reverse)])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,7 @@ public extension EnvironmentValues {

@MainActor @Entry
var removeAds: Bool = false

@MainActor @Entry
var highlightPostRead: Bool = true
}
Original file line number Diff line number Diff line change
Expand Up @@ -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?)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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
Expand All @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand All @@ -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"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,6 +19,7 @@ public struct NewsCard: View {
public var body: some View {
Button(action: { onSelect() },
label: { content })
.opacity(data.read && highlightPostRead ? 0.6 : 1)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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()
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,7 +30,8 @@ public struct FeedHighlightCardView: View {
aspectRatio: nil
)
GlassCardView(data: data)
.compositingGroup()
.opacity(post.read && highlightPostRead ? 0.6 : 1)
.compositingGroup()
}

// MARK: - Init
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -16,13 +17,15 @@ 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()
) {
self.id = id
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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()

Expand All @@ -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
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ private extension SettingsView {
NavigationLink {
List {
AppearanceView()
ReadingPreferencesView()
IconsView()
CustomTabView()
CustomSocialView()
Expand Down
Loading
Loading