diff --git a/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift b/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift index b368e8cd5..3d7708d5b 100644 --- a/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift +++ b/IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift @@ -147,6 +147,9 @@ struct ContentSettingsView: View { Toggle(isOn: $contentFilter.showQuotePosts) { Label("timeline.filter.show-quote", systemImage: "quote.bubble") } + Toggle(isOn: $contentFilter.hidePostsWithMedia) { + Label("timeline.filter.hide-posts-with-media", systemImage: "photo.on.rectangle.angled") + } } #if !os(visionOS) .listRowBackground(theme.primaryBackgroundColor) diff --git a/Packages/Timeline/Sources/Timeline/TimelineContentFilter.swift b/Packages/Timeline/Sources/Timeline/TimelineContentFilter.swift index e1f2d1556..0f0f33a46 100644 --- a/Packages/Timeline/Sources/Timeline/TimelineContentFilter.swift +++ b/Packages/Timeline/Sources/Timeline/TimelineContentFilter.swift @@ -8,17 +8,20 @@ import SwiftUI public let showReplies: Bool public let showThreads: Bool public let showQuotePosts: Bool + public let hidePostsWithMedia: Bool public init( showBoosts: Bool, showReplies: Bool, showThreads: Bool, - showQuotePosts: Bool + showQuotePosts: Bool, + hidePostsWithMedia: Bool ) { self.showBoosts = showBoosts self.showReplies = showReplies self.showThreads = showThreads self.showQuotePosts = showQuotePosts + self.hidePostsWithMedia = hidePostsWithMedia } } @@ -27,6 +30,7 @@ import SwiftUI @AppStorage("timeline_show_replies") var showReplies: Bool = true @AppStorage("timeline_show_threads") var showThreads: Bool = true @AppStorage("timeline_quote_posts") var showQuotePosts: Bool = true + @AppStorage("timeline_hide_posts_with_media") var hidePostsWithMedia: Bool = false } public static let shared = TimelineContentFilter() @@ -55,12 +59,19 @@ import SwiftUI storage.showQuotePosts = showQuotePosts } } - + + public var hidePostsWithMedia: Bool { + didSet { + storage.hidePostsWithMedia = hidePostsWithMedia + } + } + private init() { showBoosts = storage.showBoosts showReplies = storage.showReplies showThreads = storage.showThreads showQuotePosts = storage.showQuotePosts + hidePostsWithMedia = storage.hidePostsWithMedia } public func snapshot() -> Snapshot { @@ -68,7 +79,8 @@ import SwiftUI showBoosts: showBoosts, showReplies: showReplies, showThreads: showThreads, - showQuotePosts: showQuotePosts + showQuotePosts: showQuotePosts, + hidePostsWithMedia: hidePostsWithMedia ) } } diff --git a/Packages/Timeline/Sources/Timeline/View/TimelineView.swift b/Packages/Timeline/Sources/Timeline/View/TimelineView.swift index 92e71d751..96ef5147f 100644 --- a/Packages/Timeline/Sources/Timeline/View/TimelineView.swift +++ b/Packages/Timeline/Sources/Timeline/View/TimelineView.swift @@ -190,6 +190,9 @@ public struct TimelineView: View { .onChange(of: contentFilter.showQuotePosts) { _, _ in refreshContentFilter() } + .onChange(of: contentFilter.hidePostsWithMedia) { _, _ in + refreshContentFilter() + } .onChange(of: scenePhase) { _, newValue in switch newValue { case .active: diff --git a/Packages/Timeline/Sources/Timeline/actors/TimelineDatasource.swift b/Packages/Timeline/Sources/Timeline/actors/TimelineDatasource.swift index 8fcdf2873..a2c03bfd2 100644 --- a/Packages/Timeline/Sources/Timeline/actors/TimelineDatasource.swift +++ b/Packages/Timeline/Sources/Timeline/actors/TimelineDatasource.swift @@ -186,5 +186,6 @@ actor TimelineDatasource { && (showBoosts || status.reblog == nil) && (showThreads || status.inReplyToAccountId != status.account.id) && (showQuotePosts || (!hasQuote && !hasLegacyQuoteLink)) + && (!filter.hidePostsWithMedia || status.mediaAttachments.isEmpty) } } diff --git a/Packages/Timeline/Tests/TimelineTests/TimelineDatasourceMediaFilterTests.swift b/Packages/Timeline/Tests/TimelineTests/TimelineDatasourceMediaFilterTests.swift new file mode 100644 index 000000000..eac72e071 --- /dev/null +++ b/Packages/Timeline/Tests/TimelineTests/TimelineDatasourceMediaFilterTests.swift @@ -0,0 +1,96 @@ +// +// TimelineDataSourceFilterTests.swift +// Timeline +// +// Created by Dshynt Pwr on 28/12/25. +// +import Testing +import Foundation +@testable import Timeline +@testable import Models +@testable import Env + +@Suite("TimelineDatasource media filter") +struct TimelineDatasourceMediaFilterTests { + + //Helper to build a Status with a given number of media attachments + private func makeStatus(id: String, mediaCount: Int) -> Status { + return Status( + id: id, + content: .init(stringValue: "", parseMarkdown: false), + account: .placeholder(), + createdAt: ServerDate(), + editedAt: nil, + reblog: nil, + mediaAttachments: makeAttachments(mediaCount: mediaCount), + mentions: [], + repliesCount: 0, + reblogsCount: 0, + favouritesCount: 0, + card: nil, + favourited: nil, + reblogged: nil, + pinned: nil, + bookmarked: nil, + emojis: [], + url: nil, + application: nil, + inReplyToId: nil, + inReplyToAccountId: nil, + visibility: .pub, + poll: nil, + spoilerText: .init(stringValue: ""), + filtered: [], + sensitive: false, + language: nil, + tags: [], + quote: nil, + quotesCount: nil, + quoteApproval: nil + ) + } + + private func makeAttachments(mediaCount: Int) -> [MediaAttachment] { + guard mediaCount > 0 else { return [] } + return (0..