Content Filter and improvements
This commit is contained in:
parent
d8096f5986
commit
4608d0b1a5
@ -30,6 +30,7 @@ Join the Matrix Space to get support, with full security: [Join #threadedapp:mat
|
||||
- [EmojiText](https://github.com/divadretlaw/EmojiText)
|
||||
- [KeychainSwift](https://github.com/evgenyneu/keychain-swift)
|
||||
- [RevenueCat](https://www.revenuecat.com/)
|
||||
- A derivative of [ProboscisKit](https://github.com/lumaa-dev/ProboscisKit)
|
||||
|
||||
## To-do list
|
||||
|
||||
|
@ -11,6 +11,7 @@
|
||||
B9029FC42B8125CE00AA9B68 /* HuggingFace.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9029FC32B8125CE00AA9B68 /* HuggingFace.swift */; };
|
||||
B915C4422B6F908C00042DDB /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B915C4412B6F908C00042DDB /* ProfileView.swift */; };
|
||||
B93126EA2C29C63100BF16E9 /* ContentFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93126E92C29C63000BF16E9 /* ContentFilter.swift */; };
|
||||
B93126F02C2AEB8300BF16E9 /* FilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93126EF2C2AEB8300BF16E9 /* FilterView.swift */; };
|
||||
B934EA242BAB5E7F001F4345 /* RestrictedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B934EA232BAB5E7F001F4345 /* RestrictedView.swift */; };
|
||||
B93757112B7FB8D400652F91 /* AltClients.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93757102B7FB8D400652F91 /* AltClients.swift */; };
|
||||
B93ADFCB2B7625CD00FF9172 /* DiscoveryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B93ADFCA2B7625CD00FF9172 /* DiscoveryView.swift */; };
|
||||
@ -191,6 +192,7 @@
|
||||
B9029FC32B8125CE00AA9B68 /* HuggingFace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HuggingFace.swift; sourceTree = "<group>"; };
|
||||
B915C4412B6F908C00042DDB /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
|
||||
B93126E92C29C63000BF16E9 /* ContentFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentFilter.swift; sourceTree = "<group>"; };
|
||||
B93126EF2C2AEB8300BF16E9 /* FilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterView.swift; sourceTree = "<group>"; };
|
||||
B934EA232BAB5E7F001F4345 /* RestrictedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestrictedView.swift; sourceTree = "<group>"; };
|
||||
B93757102B7FB8D400652F91 /* AltClients.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AltClients.swift; sourceTree = "<group>"; };
|
||||
B93ADFCA2B7625CD00FF9172 /* DiscoveryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryView.swift; sourceTree = "<group>"; };
|
||||
@ -569,6 +571,7 @@
|
||||
B97798882B853E6600DC869F /* UpdateView.swift */,
|
||||
B9B469B12B9A6E8300AD5585 /* PrivacyView.swift */,
|
||||
B934EA232BAB5E7F001F4345 /* RestrictedView.swift */,
|
||||
B93126EF2C2AEB8300BF16E9 /* FilterView.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
@ -834,6 +837,7 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B93126F02C2AEB8300BF16E9 /* FilterView.swift in Sources */,
|
||||
B9FB94922B2E35D000D81C07 /* SettingsView.swift in Sources */,
|
||||
B98BC7472B46CE6300595441 /* PostDetailsView.swift in Sources */,
|
||||
B9CC45B82B40A2D6001E4FA5 /* AboutView.swift in Sources */,
|
||||
|
@ -6,6 +6,7 @@ import SwiftData
|
||||
|
||||
/// A content filter designed for posts and its author
|
||||
protocol PostFilter {
|
||||
var type: ContentFilter.FilterContentType { get }
|
||||
var categoryName: String { get }
|
||||
var content: [String] { get set }
|
||||
var post: Status? { get set }
|
||||
@ -62,8 +63,8 @@ extension PostFilter {
|
||||
class ContentFilter {
|
||||
static let defaultFilter: ContentFilter.WordFilter = .init(categoryName: "Default", words: [])
|
||||
|
||||
@Model
|
||||
class WordFilter: PostFilter {
|
||||
let type: ContentFilter.FilterContentType = .words
|
||||
let categoryName: String
|
||||
var content: [String]
|
||||
var post: Status?
|
||||
@ -74,6 +75,11 @@ class ContentFilter {
|
||||
self.content = rearranged
|
||||
}
|
||||
|
||||
init(model: ModelFilter) {
|
||||
self.categoryName = model.name
|
||||
self.content = model.sensitive
|
||||
}
|
||||
|
||||
func setContent(_ content: [String]) {
|
||||
let rearranged: [String] = content.compactMap({ $0.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) }).uniqued()
|
||||
self.content = rearranged
|
||||
@ -118,6 +124,7 @@ class ContentFilter {
|
||||
}
|
||||
|
||||
class URLFilter: PostFilter {
|
||||
var type: ContentFilter.FilterContentType = .url
|
||||
let categoryName: String
|
||||
var content: [String]
|
||||
var post: Status?
|
||||
@ -128,6 +135,11 @@ class ContentFilter {
|
||||
self.content = rearranged
|
||||
}
|
||||
|
||||
init(model: ModelFilter) {
|
||||
self.categoryName = model.name
|
||||
self.content = model.sensitive
|
||||
}
|
||||
|
||||
func setContent(_ content: [String]) {
|
||||
let rearranged: [String] = content.compactMap({ $0.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) }).uniqued()
|
||||
self.content = rearranged
|
||||
@ -176,63 +188,9 @@ class ContentFilter {
|
||||
}
|
||||
}
|
||||
|
||||
class InstanceFilter: PostFilter {
|
||||
let categoryName: String
|
||||
var content: [String]
|
||||
var post: Status?
|
||||
|
||||
init(categoryName: String, urls: [URL]) {
|
||||
self.categoryName = categoryName
|
||||
let rearranged: [String] = urls.compactMap({ $0.host() ?? "https://example.com" }).uniqued()
|
||||
self.content = rearranged
|
||||
}
|
||||
|
||||
func setContent(_ content: [String]) {
|
||||
let rearranged: [String] = content.compactMap({ $0.lowercased().trimmingCharacters(in: .whitespacesAndNewlines) }).uniqued()
|
||||
self.content = rearranged
|
||||
}
|
||||
|
||||
func setContent(_ content: [URL]) {
|
||||
let rearranged: [String] = content.compactMap({ $0.host() ?? "https://example.com" }).uniqued()
|
||||
self.content = rearranged
|
||||
}
|
||||
|
||||
func filter(_ post: Status, type: ContentFilter.FilterType = .remove) -> Bool {
|
||||
self.setPost(post)
|
||||
if type == .censor {
|
||||
let sensitives: [String] = self.willFilter(post.content)
|
||||
for word in sensitives {
|
||||
post.content.asMarkdown = self.post!.content.asMarkdown.replacingOccurrences(of: word, with: "***")
|
||||
post.content.asSafeMarkdownAttributedString = self.post!.content.asSafeMarkdownAttributedString.replacing(word, with: "")
|
||||
}
|
||||
|
||||
return !sensitives.isEmpty
|
||||
} else {
|
||||
let includesSensitive: Bool = self.willFilter(post.content)
|
||||
return includesSensitive
|
||||
}
|
||||
}
|
||||
|
||||
func filter(_ post: Status, type: ContentFilter.FilterType = .remove, manualEdit: @escaping (String) -> Void = {_ in}) -> Bool {
|
||||
self.setPost(post)
|
||||
if type == .censor {
|
||||
let sensitives: [String] = self.willFilter(post.content)
|
||||
for word in sensitives {
|
||||
manualEdit(word)
|
||||
}
|
||||
|
||||
return !sensitives.isEmpty
|
||||
} else {
|
||||
let includesSensitive: Bool = self.willFilter(post.content)
|
||||
return includesSensitive
|
||||
}
|
||||
}
|
||||
|
||||
private func setPost(_ post: Status?) {
|
||||
if let p = post {
|
||||
self.post = p
|
||||
}
|
||||
}
|
||||
enum FilterContentType: String, Codable {
|
||||
case words = "words"
|
||||
case url = "url"
|
||||
}
|
||||
|
||||
enum FilterType: String, CaseIterable {
|
||||
@ -260,6 +218,25 @@ class ContentFilter {
|
||||
}
|
||||
}
|
||||
|
||||
@Model
|
||||
class ModelFilter {
|
||||
let name: String
|
||||
let sensitive: [String]
|
||||
let type: ContentFilter.FilterContentType
|
||||
|
||||
init(name: String, sensitive: [String], type: ContentFilter.FilterContentType = .words) {
|
||||
self.name = name
|
||||
self.sensitive = sensitive
|
||||
self.type = type
|
||||
}
|
||||
|
||||
init(postFilter: PostFilter) {
|
||||
self.name = postFilter.categoryName
|
||||
self.sensitive = postFilter.content
|
||||
self.type = postFilter.type
|
||||
}
|
||||
}
|
||||
|
||||
extension NSAttributedString {
|
||||
func replacing(_ placeholder: String, with valueString: String) -> NSAttributedString {
|
||||
if let range = self.string.range(of: placeholder) {
|
||||
|
@ -8,6 +8,8 @@ class FetchTimeline {
|
||||
public var statusesState: LoadingState = .loading
|
||||
|
||||
private var timeline: TimelineFilter = .home
|
||||
public private(set) var filtering: Bool = false
|
||||
public private(set) var lastFilter: PostFilter? = nil
|
||||
|
||||
init(client: Client) {
|
||||
self.client = client
|
||||
@ -31,7 +33,7 @@ class FetchTimeline {
|
||||
|
||||
self.statusesState = .loading
|
||||
let lastStatus = self.datasource.last!
|
||||
let newStatuses: [Status] = await fetchNewestStatuses(lastStatusId: lastStatus.id)
|
||||
let newStatuses: [Status] = await fetchNewestStatuses(lastStatusId: lastStatus.id, filter: lastFilter)
|
||||
self.datasource.append(contentsOf: newStatuses)
|
||||
self.statusesState = .loaded
|
||||
|
||||
@ -44,6 +46,7 @@ class FetchTimeline {
|
||||
do {
|
||||
var statuses: [Status] = try await client!.get(endpoint: timeline.endpoint(sinceId: nil, maxId: lastStatusId, minId: nil, offset: 0))
|
||||
statuses = applyContentFilter(statuses, filter: filter)
|
||||
self.lastFilter = filter
|
||||
if lastStatusId == nil {
|
||||
self.datasource = statuses
|
||||
}
|
||||
@ -66,28 +69,52 @@ class FetchTimeline {
|
||||
return await self.fetchNewestStatuses(lastStatusId: nil, filter: filter)
|
||||
}
|
||||
|
||||
func applyContentFilter(_ statuses: [Status], filter: PostFilter? = nil) -> [Status] {
|
||||
private func applyContentFilter(_ statuses: [Status], filter: PostFilter? = nil) -> [Status] {
|
||||
guard let postFilter = filter else { return statuses }
|
||||
var filteredStatuses: [Status] = statuses
|
||||
let contentFilter: any PostFilter = postFilter
|
||||
|
||||
let filterType: ContentFilter.FilterType = UserDefaults.standard.bool(forKey: "censorsFilter") ? .censor : .remove
|
||||
|
||||
for post in statuses {
|
||||
let i = statuses.firstIndex(of: post) ?? -1
|
||||
|
||||
_ = contentFilter.filter(post, type: .censor) { sensitive in
|
||||
let isFiltered = contentFilter.filter(post, type: filterType) { sensitive in
|
||||
post.content.asRawText = post.content.asRawText.replacingOccurrences(of: sensitive, with: "***")
|
||||
post.content.asMarkdown = post.content.asMarkdown.replacingOccurrences(of: sensitive, with: "***")
|
||||
post.content.asSafeMarkdownAttributedString = post.content.asSafeMarkdownAttributedString.replacing(sensitive, with: "***")
|
||||
|
||||
filteredStatuses[i] = post
|
||||
|
||||
print("Edited \(post.account.acct)'s post")
|
||||
print("Censored \(post.account.acct)'s post")
|
||||
}
|
||||
|
||||
if isFiltered && filterType == .remove {
|
||||
filteredStatuses.remove(at: i)
|
||||
|
||||
print("Removed \(post.account.acct)'s post")
|
||||
}
|
||||
}
|
||||
|
||||
return filteredStatuses
|
||||
}
|
||||
|
||||
func toggleContentFilter(filter: PostFilter) async -> [Status] {
|
||||
if self.filtering {
|
||||
self.lastFilter = nil
|
||||
self.filtering = false
|
||||
|
||||
self.datasource = []
|
||||
self.statusesState = .loading
|
||||
return await self.fetchNewestStatuses(lastStatusId: nil, filter: nil)
|
||||
} else {
|
||||
self.lastFilter = filter
|
||||
self.filtering = true
|
||||
|
||||
return await self.useContentFilter(filter)
|
||||
}
|
||||
}
|
||||
|
||||
func getStatuses() -> [Status] {
|
||||
return datasource
|
||||
}
|
||||
|
@ -105,6 +105,7 @@ public enum SheetDestination: Identifiable {
|
||||
case safari(url: URL)
|
||||
case shareImage(image: UIImage, status: Status)
|
||||
case update
|
||||
case filter
|
||||
|
||||
public var id: String {
|
||||
switch self {
|
||||
@ -125,6 +126,8 @@ public enum SheetDestination: Identifiable {
|
||||
return "shareImage"
|
||||
case .update:
|
||||
return "update"
|
||||
case .filter:
|
||||
return "contentfilter"
|
||||
}
|
||||
}
|
||||
|
||||
@ -147,6 +150,8 @@ public enum SheetDestination: Identifiable {
|
||||
return false
|
||||
case .update:
|
||||
return false
|
||||
case .filter:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -158,6 +163,7 @@ public enum RouterDestination: Hashable {
|
||||
case about
|
||||
case privacy
|
||||
case restricted
|
||||
case filter
|
||||
|
||||
case account(acc: Account)
|
||||
case post(status: Status)
|
||||
@ -166,7 +172,7 @@ public enum RouterDestination: Hashable {
|
||||
}
|
||||
|
||||
extension RouterDestination {
|
||||
static let allSettings: [RouterDestination] = [.settings, .support, .about, .appearence, .privacy, .restricted]
|
||||
static let allSettings: [RouterDestination] = [.settings, .support, .about, .appearence, .privacy, .restricted, .filter]
|
||||
}
|
||||
|
||||
extension View {
|
||||
@ -193,6 +199,8 @@ extension View {
|
||||
RestrictedView()
|
||||
case .timeline(let timeline):
|
||||
PostsView(filter: timeline)
|
||||
case .filter:
|
||||
FilterView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1984,6 +1984,102 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.content-filter.allow" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Enable Content Filter"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Activer le Filtre de Contenu"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.content-filter.auto" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Auto start Content Filter"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Démarrage auto du Filtre de Contenu"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.content-filter.new-word" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sensitive Word"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Mot sensible"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.content-filter.type" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Filter type"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Type de filtre"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.content-filter.words" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sensitive Words"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Mots sensibles"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.content-filter.words.footer" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Sensitive Words are the words that will be filtered by the Content Filter, the Content Filter will detect your words even if they’re in the middle of a word like “annoy” will filter “annoying”."
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Les mots sensibles sont des mots qui sont filtrés par le Filtre de Contenu, le Filtre de Contenu détectera tous les mots même si ils sont au milieu d’un mot comme “sacre” filtrera “sacre-bleu”."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.done" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -2048,6 +2144,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.privacy.filter" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Content Filter"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Filtre de Contenu"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.privacy.restricted" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -2544,6 +2656,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"status.content-filter.censor" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Censor"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Censurer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status.content-filter.remove" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Remove"
|
||||
}
|
||||
},
|
||||
"fr" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Supprimer"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status.cross-post.alts" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -3698,4 +3842,4 @@
|
||||
}
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ public extension View {
|
||||
@ViewBuilder
|
||||
func modelData() -> some View {
|
||||
self
|
||||
.modelContainer(for: [LoggedAccount.self, ContentFilter.WordFilter.self])
|
||||
.modelContainer(for: [LoggedAccount.self, ModelFilter.self])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,106 @@
|
||||
//Made by Lumaa
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct FilterView: View {
|
||||
@Environment(\.modelContext) private var modelContext: ModelContext
|
||||
|
||||
@AppStorage("allowFilter") private var allowFilter: Bool = false
|
||||
@AppStorage("autoOnFilter") private var enableAutoFilter: Bool = false
|
||||
@AppStorage("censorsFilter") private var censorsFilter: Bool = false
|
||||
|
||||
@State private var censorType: ContentFilter.FilterType = .censor
|
||||
|
||||
@Query private var filters: [ModelFilter]
|
||||
@State private var wordsFilter: ContentFilter.WordFilter = ContentFilter.defaultFilter
|
||||
|
||||
@State private var newWord: String = ""
|
||||
@FocusState private var filterState: Bool
|
||||
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
List {
|
||||
Toggle(isOn: $allowFilter.animation(.spring)) {
|
||||
Text("settings.content-filter.allow")
|
||||
}
|
||||
.tint(Color.green)
|
||||
.listRowThreaded()
|
||||
|
||||
if allowFilter {
|
||||
Toggle(isOn: $enableAutoFilter.animation(.spring)) {
|
||||
Text("settings.content-filter.auto")
|
||||
}
|
||||
.tint(Color.green)
|
||||
.listRowThreaded()
|
||||
|
||||
Picker(selection: $censorType) {
|
||||
ForEach(ContentFilter.FilterType.allCases, id: \.self) { type in
|
||||
type.label
|
||||
.id(type)
|
||||
}
|
||||
} label: {
|
||||
Text("settings.content-filter.type")
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.listRowThreaded()
|
||||
.onAppear {
|
||||
censorType = censorsFilter ? .censor : .remove
|
||||
}
|
||||
.onChange(of: censorType) { _, newValue in
|
||||
censorsFilter = newValue == .censor
|
||||
}
|
||||
|
||||
Section(header: Text("settings.content-filter.words"), footer: Text("settings.content-filter.words.footer")) {
|
||||
TextField("settings.content-filter.new-word", text: $newWord)
|
||||
.keyboardType(.asciiCapable)
|
||||
.submitLabel(.done)
|
||||
.focused($filterState)
|
||||
.onSubmit {
|
||||
filterState.toggle()
|
||||
|
||||
let sensitive = newWord.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !sensitive.isEmpty {
|
||||
wordsFilter.content.append(sensitive)
|
||||
}
|
||||
|
||||
newWord = ""
|
||||
saveData()
|
||||
}
|
||||
|
||||
ForEach(wordsFilter.content, id: \.self) { word in
|
||||
Text(word)
|
||||
.lineLimit(1)
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
.onDelete { i in
|
||||
wordsFilter.content.remove(atOffsets: i)
|
||||
saveData()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
self.wordsFilter = filters.compactMap({ ContentFilter.WordFilter(model: $0) }).first ?? ContentFilter.defaultFilter
|
||||
filterState = true
|
||||
}
|
||||
.listRowThreaded()
|
||||
}
|
||||
}
|
||||
.navigationTitle(Text("settings.privacy.filter"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.listThreaded()
|
||||
}
|
||||
|
||||
private func saveData() {
|
||||
let pack: ModelFilter = ModelFilter(postFilter: wordsFilter)
|
||||
|
||||
do {
|
||||
if let firstFilter = filters.first {
|
||||
modelContext.delete(firstFilter)
|
||||
}
|
||||
modelContext.insert(pack)
|
||||
try modelContext.save()
|
||||
} catch {
|
||||
print("Couldn't save properly: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,13 @@ struct PrivacyView: View {
|
||||
}
|
||||
.listRowThreaded()
|
||||
|
||||
Button {
|
||||
navigator.navigate(to: .filter)
|
||||
} label: {
|
||||
Label("settings.privacy.filter", systemImage: "line.3.horizontal.decrease.circle")
|
||||
}
|
||||
.listRowThreaded()
|
||||
|
||||
Spacer()
|
||||
.frame(height: 30)
|
||||
.listRowThreaded()
|
||||
|
@ -1,6 +1,7 @@
|
||||
//Made by Lumaa
|
||||
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
struct TimelineView: View {
|
||||
@Environment(AccountManager.self) private var accountManager: AccountManager
|
||||
@ -13,6 +14,9 @@ struct TimelineView: View {
|
||||
@State private var statuses: [Status]?
|
||||
@State private var lastSeen: Int?
|
||||
|
||||
@Query private var filters: [ModelFilter]
|
||||
@State private var wordsFilter: ContentFilter.WordFilter = ContentFilter.defaultFilter
|
||||
|
||||
@State var filter: TimelineFilter = .home
|
||||
@State var showHero: Bool = true
|
||||
@State var timelineModel: FetchTimeline // home timeline by default
|
||||
@ -64,6 +68,7 @@ struct TimelineView: View {
|
||||
.disabled(t == filter)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 7.5)
|
||||
|
||||
ScrollView(.horizontal) {
|
||||
HStack {
|
||||
@ -80,6 +85,7 @@ struct TimelineView: View {
|
||||
.disabled(t == filter)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 7.5)
|
||||
}
|
||||
.padding(.vertical)
|
||||
.scrollIndicators(.hidden)
|
||||
@ -98,7 +104,7 @@ struct TimelineView: View {
|
||||
}
|
||||
.refreshable {
|
||||
if let client = accountManager.getClient() {
|
||||
statuses = []
|
||||
statuses = nil
|
||||
|
||||
Task {
|
||||
loadingStatuses = true
|
||||
@ -116,17 +122,21 @@ struct TimelineView: View {
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button {
|
||||
statuses = []
|
||||
|
||||
Task {
|
||||
loadingStatuses = true
|
||||
statuses = await self.timelineModel.useContentFilter(ContentFilter.WordFilter(categoryName: "Test", words: ["is"]))
|
||||
loadingStatuses = false
|
||||
if UserDefaults.standard.bool(forKey: "allowFilter") {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button {
|
||||
statuses = nil
|
||||
|
||||
Task {
|
||||
loadingStatuses = true
|
||||
statuses = await self.timelineModel.toggleContentFilter(filter: wordsFilter)
|
||||
loadingStatuses = false
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: self.timelineModel.filtering ? "line.3.horizontal.decrease.circle.fill" :"line.3.horizontal.decrease.circle")
|
||||
.symbolEffect(.pulse.wholeSymbol, isActive: self.timelineModel.filtering)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "line.3.horizontal.decrease.circle")
|
||||
.tint(Color(uiColor: UIColor.label))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -163,12 +173,14 @@ struct TimelineView: View {
|
||||
}
|
||||
} label: {
|
||||
Text(t.localizedTitle())
|
||||
.padding(.horizontal)
|
||||
.frame(width: 20)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.buttonStyle(LargeButton(filled: t == filter, height: 7.5))
|
||||
.disabled(t == filter)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 7.5)
|
||||
|
||||
ScrollView(.horizontal) {
|
||||
HStack {
|
||||
@ -185,6 +197,7 @@ struct TimelineView: View {
|
||||
.disabled(t == filter)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 7.5)
|
||||
}
|
||||
.padding(.vertical)
|
||||
.scrollIndicators(.hidden)
|
||||
@ -209,9 +222,17 @@ struct TimelineView: View {
|
||||
Color.appBackground
|
||||
.ignoresSafeArea()
|
||||
.onAppear {
|
||||
if UserDefaults.standard.bool(forKey: "allowFilter") {
|
||||
self.wordsFilter = filters.compactMap({ ContentFilter.WordFilter(model: $0) }).first ?? ContentFilter.defaultFilter
|
||||
}
|
||||
|
||||
if let client = accountManager.getClient() {
|
||||
Task {
|
||||
statuses = await timelineModel.fetch(client: client)
|
||||
|
||||
if UserDefaults.standard.bool(forKey: "autoOnFilter") {
|
||||
statuses = await self.timelineModel.useContentFilter(wordsFilter)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -244,6 +265,10 @@ struct TimelineView: View {
|
||||
loadingStatuses = true
|
||||
statuses = await timelineModel.fetch(client: client)
|
||||
lastSeen = nil
|
||||
|
||||
if timelineModel.filtering {
|
||||
statuses = await self.timelineModel.useContentFilter(wordsFilter)
|
||||
}
|
||||
loadingStatuses = false
|
||||
}
|
||||
}
|
||||
|
@ -11,3 +11,10 @@ struct ThreadedWatch_Watch_AppApp: App {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func modelData() -> some View {
|
||||
self
|
||||
.modelContainer(for: LoggedAccount.self)
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user