Add timeline content filter

This commit is contained in:
Thomas Ricouard 2024-01-11 18:55:35 +01:00
parent 801b6c5682
commit 2f5307bfc7
14 changed files with 757 additions and 164 deletions

View file

@ -114,6 +114,8 @@ extension View {
ActivityView(image: image, status: status)
case let .editTagGroup(tagGroup, onSaved):
EditTagGroupView(tagGroup: tagGroup, onSaved: onSaved)
case .timelineContentFilter:
NavigationSheet { TimelineContentFilterView() }
}
}
.withEnvironments()

View file

@ -6,24 +6,18 @@ import Network
import NukeUI
import SwiftUI
import UserNotifications
import Timeline
@MainActor
struct ContentSettingsView: View {
@Environment(UserPreferences.self) private var userPreferences
@Environment(Theme.self) private var theme
@State private var contentFilter = TimelineContentFilter.shared
var body: some View {
@Bindable var userPreferences = userPreferences
Form {
Section("settings.content.boosts") {
Toggle(isOn: $userPreferences.suppressDupeReblogs) {
Text("settings.content.hide-repeated-boosts")
}
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
Section("settings.content.media") {
Toggle(isOn: $userPreferences.autoPlayVideo) {
Text("settings.other.autoplay-video")
@ -89,7 +83,7 @@ struct ContentSettingsView: View {
} footer: {
Text("settings.content.collapse-long-posts-hint")
}
#if !os(visionOS)
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
@ -122,6 +116,24 @@ struct ContentSettingsView: View {
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
Section("timeline.content-filter.title") {
Toggle(isOn: $contentFilter.showBoosts) {
Label("timeline.filter.show-boosts", image: "Rocket")
}
Toggle(isOn: $contentFilter.showReplies) {
Label("timeline.filter.show-replies", systemImage: "bubble.left.and.bubble.right")
}
Toggle(isOn: $contentFilter.showThreads) {
Label("timeline.filter.show-threads", systemImage: "bubble.left.and.text.bubble.right")
}
Toggle(isOn: $contentFilter.showQuotePosts) {
Label("timeline.filter.show-quote", systemImage: "quote.bubble")
}
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
.navigationTitle("settings.content.navigation-title")
#if !os(visionOS)

View file

@ -124,7 +124,10 @@ struct TimelineTab: View {
@ViewBuilder
private var timelineFilterButton: some View {
latestOrResumeButtons
contentFilterButton
Divider()
pinMenuButton
Divider()
timelineFiltersButtons
listsFiltersButons
tagsFiltersButtons
@ -203,7 +206,6 @@ struct TimelineTab: View {
}
}
}
Divider()
}
}
@ -225,8 +227,6 @@ struct TimelineTab: View {
Label("status.action.pin", systemImage: "pin")
}
}
Divider()
}
private var timelineFiltersButtons: some View {
@ -311,6 +311,14 @@ struct TimelineTab: View {
}
}
}
private var contentFilterButton: some View {
Button(action: {
routerPath.presentedSheet = .timelineContentFilter
}, label: {
Label("timeline.content-filter.title", systemSymbol: .line3HorizontalDecrease)
})
}
private func resetTimelineFilter() {
if client.isAuth, canFilterTimeline {

View file

@ -40122,6 +40122,7 @@
}
},
"settings.content.boosts" : {
"extractionState" : "stale",
"localizations" : {
"be" : {
"stringUnit" : {
@ -41066,6 +41067,7 @@
}
},
"settings.content.hide-repeated-boosts" : {
"extractionState" : "stale",
"localizations" : {
"be" : {
"stringUnit" : {
@ -73453,6 +73455,124 @@
}
}
},
"timeline.content-filter.title" : {
"localizations" : {
"be" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Content Filter"
}
},
"ca" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Content Filter"
}
},
"de" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Content Filter"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Content Filter"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Content Filter"
}
},
"es" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Content Filter"
}
},
"eu" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Content Filter"
}
},
"fr" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Content Filter"
}
},
"it" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Content Filter"
}
},
"ja" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Content Filter"
}
},
"ko" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Content Filter"
}
},
"nb" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Content Filter"
}
},
"nl" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Content Filter"
}
},
"pl" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Content Filter"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Content Filter"
}
},
"tr" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Content Filter"
}
},
"uk" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Content Filter"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Content Filter"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Content Filter"
}
}
}
},
"timeline.federated" : {
"comment" : "MARK: Package: Timeline",
"extractionState" : "manual",
@ -74163,6 +74283,482 @@
}
}
},
"timeline.filter.show-boosts" : {
"extractionState" : "manual",
"localizations" : {
"be" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Boosts"
}
},
"ca" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Boosts"
}
},
"de" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Boosts"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Show Boosts"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Show Boosts"
}
},
"es" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Boosts"
}
},
"eu" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Boosts"
}
},
"fr" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Boosts"
}
},
"it" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Boosts"
}
},
"ja" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Boosts"
}
},
"ko" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Boosts"
}
},
"nb" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Boosts"
}
},
"nl" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Boosts"
}
},
"pl" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Boosts"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Boosts"
}
},
"tr" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Boosts"
}
},
"uk" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Boosts"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Boosts"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Boosts"
}
}
}
},
"timeline.filter.show-quote" : {
"extractionState" : "manual",
"localizations" : {
"be" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Quotes"
}
},
"ca" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Quotes"
}
},
"de" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Quotes"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Show Quotes"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Show Quotes"
}
},
"es" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Quotes"
}
},
"eu" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Quotes"
}
},
"fr" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Quotes"
}
},
"it" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Quotes"
}
},
"ja" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Quotes"
}
},
"ko" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Quotes"
}
},
"nb" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Quotes"
}
},
"nl" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Quotes"
}
},
"pl" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Quotes"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Quotes"
}
},
"tr" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Quotes"
}
},
"uk" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Quotes"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Quotes"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Quotes"
}
}
}
},
"timeline.filter.show-replies" : {
"extractionState" : "manual",
"localizations" : {
"be" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Replies"
}
},
"ca" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Replies"
}
},
"de" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Replies"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Show Replies"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Show Replies"
}
},
"es" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Replies"
}
},
"eu" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Replies"
}
},
"fr" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Replies"
}
},
"it" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Replies"
}
},
"ja" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Replies"
}
},
"ko" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Replies"
}
},
"nb" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Replies"
}
},
"nl" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Replies"
}
},
"pl" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Replies"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Replies"
}
},
"tr" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Replies"
}
},
"uk" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Replies"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Replies"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Replies"
}
}
}
},
"timeline.filter.show-threads" : {
"extractionState" : "manual",
"localizations" : {
"be" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Threads"
}
},
"ca" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Threads"
}
},
"de" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Threads"
}
},
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Show Threads"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Show Threads"
}
},
"es" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Threads"
}
},
"eu" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Threads"
}
},
"fr" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Threads"
}
},
"it" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Threads"
}
},
"ja" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Threads"
}
},
"ko" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Threads"
}
},
"nb" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Threads"
}
},
"nl" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Threads"
}
},
"pl" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Threads"
}
},
"pt-BR" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Threads"
}
},
"tr" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Threads"
}
},
"uk" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Threads"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Threads"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "Show Threads"
}
}
}
},
"timeline.filter.tag-groups" : {
"localizations" : {
"be" : {
@ -75243,4 +75839,4 @@
}
},
"version" : "1.0"
}
}

View file

@ -57,6 +57,7 @@ public enum SheetDestination: Identifiable {
case report(status: Status)
case shareImage(image: UIImage, status: Status)
case editTagGroup(tagGroup: TagGroup, onSaved: ((TagGroup) -> Void)?)
case timelineContentFilter
public var id: String {
switch self {
@ -85,6 +86,8 @@ public enum SheetDestination: Identifiable {
"editTagGroup"
case .settings, .support, .about, .accountPushNotficationsSettings:
"settings"
case .timelineContentFilter:
"timelineContentFilter"
}
}
}

View file

@ -28,8 +28,6 @@ import SwiftUI
@AppStorage("user_deepl_api_free") public var userDeeplAPIFree = true
@AppStorage("auto_detect_post_language") public var autoDetectPostLanguage = true
@AppStorage("suppress_dupe_reblogs") public var suppressDupeReblogs: Bool = false
@AppStorage("inAppBrowserReaderView") public var inAppBrowserReaderView = false
@AppStorage("haptic_tab") public var hapticTabSelectionEnabled = true
@ -197,12 +195,6 @@ import SwiftUI
}
}
public var suppressDupeReblogs: Bool {
didSet {
storage.suppressDupeReblogs = suppressDupeReblogs
}
}
public var inAppBrowserReaderView: Bool {
didSet {
storage.inAppBrowserReaderView = inAppBrowserReaderView
@ -478,7 +470,6 @@ import SwiftUI
alwaysUseDeepl = storage.alwaysUseDeepl
userDeeplAPIFree = storage.userDeeplAPIFree
autoDetectPostLanguage = storage.autoDetectPostLanguage
suppressDupeReblogs = storage.suppressDupeReblogs
inAppBrowserReaderView = storage.inAppBrowserReaderView
hapticTabSelectionEnabled = storage.hapticTabSelectionEnabled
hapticTimelineEnabled = storage.hapticTimelineEnabled

View file

@ -1,120 +0,0 @@
import Env
import Foundation
import LRUCache
import Models
import SwiftUI
public class ReblogCache: @unchecked Sendable {
struct CacheEntry: Codable {
var reblogId: String
var postId: String
var seen: Bool
}
public static let shared = ReblogCache()
var statusCache = LRUCache<String, CacheEntry>()
private var needsWrite = false
init() {
statusCache.countLimit = 300 // can tune the cache here, 100 is super conservative
// read any existing cache from disk
if FileManager.default.fileExists(atPath: cacheFile.path()) {
do {
let data = try Data(contentsOf: cacheFile)
let cacheData = try JSONDecoder().decode([CacheEntry].self, from: data)
for entry in cacheData {
statusCache.setValue(entry, forKey: entry.reblogId)
}
} catch {
print("Error reading cache from disc")
}
print("Starting cache has \(statusCache.count) items")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 30.0) { [weak self] in
self?.saveCache()
}
}
private func saveCache() {
if needsWrite {
do {
let data = try JSONEncoder().encode(statusCache.allValues)
try data.write(to: cacheFile)
} catch {
print("Error writing cache to disc")
}
needsWrite = false
}
DispatchQueue.main.asyncAfter(deadline: .now() + 30.0) { [weak self] in
self?.saveCache()
}
}
private var cacheFile: URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
let documentsDirectory = paths[0]
return URL(fileURLWithPath: documentsDirectory.path()).appendingPathComponent("reblog.json")
}
@MainActor public func removeDuplicateReblogs(_ statuses: inout [Status]) {
if !UserPreferences.shared.suppressDupeReblogs {
return
}
var i = statuses.count
for status in statuses.reversed() {
// go backwards through the status list
// so that we can remove items without
// borking the array
i -= 1
if let reblog = status.reblog {
if let cached = statusCache.value(forKey: reblog.id) {
// this is already cached
if cached.postId != status.id, cached.seen {
// This was posted by someone other than the person we have in the cache
// and we have seen the items at some point, so we might want to suppress it
if status.account.id != CurrentAccount.shared.account?.id {
// just a quick check to makes sure that this wasn't boosted by the current
// user. Hiding that would be confusing
// But assuming it isn't then we can suppress this boost
print("suppressing: \(reblog.id)/ \(String(describing: reblog.account.displayName)) by \(String(describing: status.account.displayName))")
statuses.remove(at: i)
// assert(statuses.count == (ct-1))
}
}
}
cache(status, seen: false)
}
}
}
public func cache(_ status: Status, seen: Bool) {
var wasSeen = false
var postToCache = status.id
if let reblog = status.reblog {
// only caching boosts at the moment.
if let cached = statusCache.value(forKey: reblog.id) {
// every time we see it, we refresh it in the list
// so poplular things are kept in the cache
wasSeen = cached.seen
if wasSeen {
postToCache = cached.postId
// if we have seen a particular version of the post
// that's the one we keep
}
}
statusCache.setValue(CacheEntry(reblogId: reblog.id, postId: postToCache, seen: seen || wasSeen), forKey: reblog.id)
needsWrite = true
}
}
}

View file

@ -106,7 +106,6 @@ public struct StatusRowView: View {
}
}
.onAppear {
viewModel.markSeen()
if !reasons.contains(.placeholder) {
if !isCompact, viewModel.embeddedStatus == nil {
Task {

View file

@ -151,19 +151,6 @@ import SwiftUI
recalcCollapse()
}
func markSeen() {
// called in on appear so we can cache that the status has been seen.
if UserPreferences.shared.suppressDupeReblogs, !seen {
DispatchQueue.global().async { [weak self] in
guard let self else { return }
ReblogCache.shared.cache(status, seen: true)
Task { @MainActor in
self.seen = true
}
}
}
}
func navigateToDetail() {
if isRemote, let url = URL(string: finalStatus.url ?? "") {
routerPath.navigate(to: .remoteStatusDetail(url: url))

View file

@ -0,0 +1,46 @@
import Foundation
import SwiftUI
@MainActor
@Observable public class TimelineContentFilter {
class Storage {
@AppStorage("timeline_show_boosts") var showBoosts: Bool = true
@AppStorage("timeline_show_replies") var showReplies: Bool = true
@AppStorage("timeline_show_threads") var showThreads: Bool = true
@AppStorage("timeline_quote_posts") var showQuotePosts: Bool = true
}
public static let shared = TimelineContentFilter()
private let storage = Storage()
public var showBoosts: Bool {
didSet {
storage.showBoosts = showBoosts
}
}
public var showReplies: Bool {
didSet {
storage.showReplies = showReplies
}
}
public var showThreads: Bool {
didSet {
storage.showThreads = showThreads
}
}
public var showQuotePosts: Bool {
didSet {
storage.showQuotePosts = showQuotePosts
}
}
private init() {
showBoosts = storage.showBoosts
showReplies = storage.showReplies
showThreads = storage.showThreads
showQuotePosts = storage.showQuotePosts
}
}

View file

@ -0,0 +1,38 @@
import SwiftUI
import DesignSystem
public struct TimelineContentFilterView: View {
@Environment(Theme.self) private var theme
@State private var contentFilter = TimelineContentFilter.shared
public init() {}
public var body: some View {
Form {
Section {
Toggle(isOn: $contentFilter.showBoosts) {
Label("timeline.filter.show-boosts", image: "Rocket")
}
Toggle(isOn: $contentFilter.showReplies) {
Label("timeline.filter.show-replies", systemImage: "bubble.left.and.bubble.right")
}
Toggle(isOn: $contentFilter.showThreads) {
Label("timeline.filter.show-threads", systemImage: "bubble.left.and.text.bubble.right")
}
Toggle(isOn: $contentFilter.showQuotePosts) {
Label("timeline.filter.show-quote", systemImage: "quote.bubble")
}
}
#if !os(visionOS)
.listRowBackground(theme.primaryBackgroundColor)
#endif
}
.navigationTitle("timeline.content-filter.title")
.navigationBarTitleDisplayMode(.inline)
#if !os(visionOS)
.scrollContentBackground(.hidden)
.background(theme.secondaryBackgroundColor)
#endif
}
}

View file

@ -20,6 +20,7 @@ public struct TimelineView: View {
@State private var viewModel = TimelineViewModel()
@State private var prefetcher = TimelineMediaPrefetcher()
@State private var contentFilter = TimelineContentFilter.shared
@State private var wasBackgrounded: Bool = false
@State private var collectionView: UICollectionView?
@ -166,6 +167,18 @@ public struct TimelineView: View {
.onChange(of: viewModel.timeline) { _, newValue in
timeline = newValue
}
.onChange(of: contentFilter.showReplies) { _, _ in
Task { await viewModel.refreshTimelineContentFilter() }
}
.onChange(of: contentFilter.showBoosts) { _, _ in
Task { await viewModel.refreshTimelineContentFilter() }
}
.onChange(of: contentFilter.showThreads) { _, _ in
Task { await viewModel.refreshTimelineContentFilter() }
}
.onChange(of: contentFilter.showQuotePosts) { _, _ in
Task { await viewModel.refreshTimelineContentFilter() }
}
.onChange(of: scenePhase) { _, newValue in
switch newValue {
case .active:

View file

@ -205,6 +205,14 @@ extension TimelineViewModel: StatusesFetcher {
}
}
func refreshTimelineContentFilter() async {
timelineTask?.cancel()
let statuses = await datasource.getFiltered()
withAnimation {
statusesState = .display(statuses: statuses, nextPageState: .hasNextPage)
}
}
func fetchStatuses(from: Marker.Content) async throws {
guard let client else { return }
statusesState = .loading
@ -213,7 +221,6 @@ extension TimelineViewModel: StatusesFetcher {
minId: nil,
offset: 0))
ReblogCache.shared.removeDuplicateReblogs(&statuses)
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
await datasource.set(statuses)
@ -284,7 +291,6 @@ extension TimelineViewModel: StatusesFetcher {
minId: nil,
offset: 0))
ReblogCache.shared.removeDuplicateReblogs(&statuses)
StatusDataControllerProvider.shared.updateDataControllers(for: statuses, client: client)
await datasource.set(statuses)
@ -309,7 +315,6 @@ extension TimelineViewModel: StatusesFetcher {
!ids.contains(where: { $0 == status.id })
}
ReblogCache.shared.removeDuplicateReblogs(&newStatuses)
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
// If no new statuses, resume streaming and exit.
@ -401,7 +406,6 @@ extension TimelineViewModel: StatusesFetcher {
{
pagesLoaded += 1
ReblogCache.shared.removeDuplicateReblogs(&newStatuses)
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)
allStatuses.insert(contentsOf: newStatuses, at: 0)
@ -424,7 +428,6 @@ extension TimelineViewModel: StatusesFetcher {
minId: nil,
offset: statuses.count))
ReblogCache.shared.removeDuplicateReblogs(&newStatuses)
await datasource.append(contentOf: newStatuses)
StatusDataControllerProvider.shared.updateDataControllers(for: newStatuses, client: client)

View file

@ -1,5 +1,6 @@
import Foundation
import Models
import Env
actor TimelineDatasource {
private var statuses: [Status] = []
@ -12,8 +13,22 @@ actor TimelineDatasource {
statuses
}
func getFiltered() -> [Status] {
statuses.filter{ !$0.isHidden }
func getFiltered() async -> [Status] {
let contentFilter = TimelineContentFilter.shared
let showReplies = await contentFilter.showReplies
let showBoosts = await contentFilter.showBoosts
let showThreads = await contentFilter.showThreads
let showQuotePosts = await contentFilter.showQuotePosts
return statuses.filter { status in
if status.isHidden ||
!showReplies && status.inReplyToId != nil && status.inReplyToAccountId != status.account.id ||
!showBoosts && status.reblog != nil ||
!showThreads && status.inReplyToAccountId == status.account.id ||
!showQuotePosts && !status.content.statusesURLs.isEmpty {
return false
}
return true
}
}
func count() -> Int {