Filtering

This commit is contained in:
Justin Mazzocchi 2020-08-29 22:31:30 -07:00
parent cd59aeab0e
commit 725438cc9e
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
7 changed files with 75 additions and 1 deletions

View file

@ -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()
}

View file

@ -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 }
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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

View file

@ -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
}
}
}

View file

@ -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)