mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-12-23 23:50:36 +00:00
Add timeline content filter
This commit is contained in:
parent
801b6c5682
commit
2f5307bfc7
14 changed files with 757 additions and 164 deletions
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -106,7 +106,6 @@ public struct StatusRowView: View {
|
|||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.markSeen()
|
||||
if !reasons.contains(.placeholder) {
|
||||
if !isCompact, viewModel.embeddedStatus == nil {
|
||||
Task {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue