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
3 changes: 3 additions & 0 deletions IceCubesApp/App/Tabs/Settings/ContentSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
18 changes: 15 additions & 3 deletions Packages/Timeline/Sources/Timeline/TimelineContentFilter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}

Expand All @@ -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()
Expand Down Expand Up @@ -55,20 +59,28 @@ 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 {
Snapshot(
showBoosts: showBoosts,
showReplies: showReplies,
showThreads: showThreads,
showQuotePosts: showQuotePosts
showQuotePosts: showQuotePosts,
hidePostsWithMedia: hidePostsWithMedia
)
}
}
3 changes: 3 additions & 0 deletions Packages/Timeline/Sources/Timeline/View/TimelineView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,5 +186,6 @@ actor TimelineDatasource {
&& (showBoosts || status.reblog == nil)
&& (showThreads || status.inReplyToAccountId != status.account.id)
&& (showQuotePosts || (!hasQuote && !hasLegacyQuoteLink))
&& (!filter.hidePostsWithMedia || status.mediaAttachments.isEmpty)
}
}
Original file line number Diff line number Diff line change
@@ -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..<mediaCount).map { idx in
let url = URL(string: "https://example.com/media/\(idx).jpg")
return MediaAttachment.imageWith(url: url!)
}
}

@Test("Hides posts that contain media when the setting is ON")
func hidePostsWithMediaWhenEnabled() {
let a = makeStatus(id: "a", mediaCount: 0)
let b = makeStatus(id: "b", mediaCount: 2)
let c = makeStatus(id: "c", mediaCount: 1)
let d = makeStatus(id: "d", mediaCount: 0)
let input = [a, b, c, d]

//When
let hidePostsWithMedia = true
let output = input.filter { status in
hidePostsWithMedia ? status.mediaAttachments.isEmpty : true
}

//Then
#expect(output.map(\.id) == ["a", "d"])
}

@Test("Show all posts when the setting is OFF")
func showAllPostsWhenDisabled() {
let a = makeStatus(id: "a", mediaCount: 0)
let b = makeStatus(id: "b", mediaCount: 2)
let c = makeStatus(id: "c", mediaCount: 0)
let input = [a, b, c]

//When
let hidePostsWithMedia = false
let output = input.filter { status in
hidePostsWithMedia ? status.mediaAttachments.isEmpty : true
}

//Then
#expect(output.map(\.id) == ["a", "b", "c"])
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,8 @@ struct Tests {
showBoosts: true,
showReplies: true,
showThreads: true,
showQuotePosts: false
showQuotePosts: false,
hidePostsWithMedia: false
)
let filtered = await datasource.getFiltered(using: snapshot)
#expect(filtered.map(\.id) == ["normal"])
Expand Down