mirror of
https://github.com/metabolist/metatext.git
synced 2025-01-03 18:48:40 +00:00
Filtering
This commit is contained in:
parent
cd59aeab0e
commit
725438cc9e
7 changed files with 75 additions and 1 deletions
|
@ -143,10 +143,15 @@ extension ContentDatabase {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func activeFiltersObservation(date: Date) -> AnyPublisher<[Filter], Error> {
|
||||
func activeFiltersObservation(date: Date, context: Filter.Context? = nil) -> AnyPublisher<[Filter], Error> {
|
||||
ValueObservation.tracking(Filter.filter(Column("expiresAt") == nil || Column("expiresAt") > date).fetchAll)
|
||||
.removeDuplicates()
|
||||
.publisher(in: databaseQueue)
|
||||
.map {
|
||||
guard let context = context else { return $0 }
|
||||
|
||||
return $0.filter { $0.context.contains(context) }
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,30 @@ extension Filter {
|
|||
wholeWord: true)
|
||||
}
|
||||
|
||||
extension Array where Element == Filter {
|
||||
// Adapted from https://github.com/tootsuite/mastodon/blob/bf477cee9f31036ebf3d164ddec1cebef5375513/app/javascript/mastodon/selectors/index.js#L43
|
||||
func regularExpression() -> String? {
|
||||
guard !isEmpty else { return nil }
|
||||
|
||||
return map {
|
||||
var expression = NSRegularExpression.escapedPattern(for: $0.phrase)
|
||||
|
||||
if $0.wholeWord {
|
||||
if expression.range(of: #"^[\w]"#, options: .regularExpression) != nil {
|
||||
expression = #"\b"# + expression
|
||||
}
|
||||
|
||||
if expression.range(of: #"[\w]$"#, options: .regularExpression) != nil {
|
||||
expression += #"\b"#
|
||||
}
|
||||
}
|
||||
|
||||
return expression
|
||||
}
|
||||
.joined(separator: "|")
|
||||
}
|
||||
}
|
||||
|
||||
extension Filter.Context: Identifiable {
|
||||
var id: Self { self }
|
||||
}
|
||||
|
|
|
@ -110,6 +110,14 @@ extension Status {
|
|||
var displayStatus: Status {
|
||||
reblog ?? self
|
||||
}
|
||||
|
||||
var filterableContent: String {
|
||||
[content.attributed.string,
|
||||
spoilerText,
|
||||
(poll?.options.map(\.title) ?? []).joined(separator: " "),
|
||||
reblog?.filterableContent ?? ""]
|
||||
.joined(separator: " ")
|
||||
}
|
||||
}
|
||||
|
||||
extension Status: Hashable {
|
||||
|
|
|
@ -33,6 +33,10 @@ struct ContextService {
|
|||
}
|
||||
|
||||
extension ContextService: StatusListService {
|
||||
var filters: AnyPublisher<[Filter], Error> {
|
||||
contentDatabase.activeFiltersObservation(date: Date(), context: .thread)
|
||||
}
|
||||
|
||||
var contextParentID: String? { status.id }
|
||||
|
||||
func isReplyInContext(status: Status) -> Bool {
|
||||
|
|
|
@ -5,6 +5,7 @@ import Combine
|
|||
|
||||
protocol StatusListService {
|
||||
var statusSections: AnyPublisher<[[Status]], Error> { get }
|
||||
var filters: AnyPublisher<[Filter], Error> { get }
|
||||
var paginates: Bool { get }
|
||||
var contextParentID: String? { get }
|
||||
func isPinned(status: Status) -> Bool
|
||||
|
|
|
@ -21,6 +21,10 @@ struct TimelineService {
|
|||
}
|
||||
|
||||
extension TimelineService: StatusListService {
|
||||
var filters: AnyPublisher<[Filter], Error> {
|
||||
contentDatabase.activeFiltersObservation(date: Date(), context: filterContext)
|
||||
}
|
||||
|
||||
func request(maxID: String?, minID: String?) -> AnyPublisher<Never, Error> {
|
||||
networkClient.request(Paged(timeline.endpoint, maxID: maxID, minID: minID))
|
||||
.map { ($0, timeline) }
|
||||
|
@ -36,3 +40,14 @@ extension TimelineService: StatusListService {
|
|||
ContextService(status: status.displayStatus, networkClient: networkClient, contentDatabase: contentDatabase)
|
||||
}
|
||||
}
|
||||
|
||||
private extension TimelineService {
|
||||
var filterContext: Filter.Context {
|
||||
switch timeline {
|
||||
case .home, .list:
|
||||
return .home
|
||||
case .local, .federated:
|
||||
return .public
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ class StatusListViewModel: ObservableObject {
|
|||
@Published var alertItem: AlertItem?
|
||||
@Published private(set) var loading = false
|
||||
private(set) var maintainScrollPositionOfStatusID: String?
|
||||
|
||||
@Published private var filterRegularExpression: String?
|
||||
private var statuses = [String: Status]()
|
||||
private let statusListService: StatusListService
|
||||
private var statusViewModelCache = [Status: (StatusViewModel, AnyCancellable)]()
|
||||
|
@ -16,7 +18,22 @@ class StatusListViewModel: ObservableObject {
|
|||
init(statusListService: StatusListService) {
|
||||
self.statusListService = statusListService
|
||||
|
||||
statusListService.filters
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.map { $0.regularExpression() }
|
||||
.assign(to: &$filterRegularExpression)
|
||||
|
||||
statusListService.statusSections
|
||||
.map {
|
||||
$0.map {
|
||||
$0.filter { [weak self] in
|
||||
guard let filterRegularExpression = self?.filterRegularExpression else { return true }
|
||||
|
||||
return $0.filterableContent.range(of: filterRegularExpression,
|
||||
options: [.regularExpression, .caseInsensitive]) == nil
|
||||
}
|
||||
}
|
||||
}
|
||||
.handleEvents(receiveOutput: { [weak self] in
|
||||
self?.determineIfScrollPositionShouldBeMaintained(newStatusSections: $0)
|
||||
self?.cleanViewModelCache(newStatusSections: $0)
|
||||
|
|
Loading…
Reference in a new issue