mirror of
https://github.com/metabolist/metatext.git
synced 2025-01-05 11:38: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()
|
.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)
|
ValueObservation.tracking(Filter.filter(Column("expiresAt") == nil || Column("expiresAt") > date).fetchAll)
|
||||||
.removeDuplicates()
|
.removeDuplicates()
|
||||||
.publisher(in: databaseQueue)
|
.publisher(in: databaseQueue)
|
||||||
|
.map {
|
||||||
|
guard let context = context else { return $0 }
|
||||||
|
|
||||||
|
return $0.filter { $0.context.contains(context) }
|
||||||
|
}
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,6 +32,30 @@ extension Filter {
|
||||||
wholeWord: true)
|
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 {
|
extension Filter.Context: Identifiable {
|
||||||
var id: Self { self }
|
var id: Self { self }
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,6 +110,14 @@ extension Status {
|
||||||
var displayStatus: Status {
|
var displayStatus: Status {
|
||||||
reblog ?? self
|
reblog ?? self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var filterableContent: String {
|
||||||
|
[content.attributed.string,
|
||||||
|
spoilerText,
|
||||||
|
(poll?.options.map(\.title) ?? []).joined(separator: " "),
|
||||||
|
reblog?.filterableContent ?? ""]
|
||||||
|
.joined(separator: " ")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Status: Hashable {
|
extension Status: Hashable {
|
||||||
|
|
|
@ -33,6 +33,10 @@ struct ContextService {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ContextService: StatusListService {
|
extension ContextService: StatusListService {
|
||||||
|
var filters: AnyPublisher<[Filter], Error> {
|
||||||
|
contentDatabase.activeFiltersObservation(date: Date(), context: .thread)
|
||||||
|
}
|
||||||
|
|
||||||
var contextParentID: String? { status.id }
|
var contextParentID: String? { status.id }
|
||||||
|
|
||||||
func isReplyInContext(status: Status) -> Bool {
|
func isReplyInContext(status: Status) -> Bool {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Combine
|
||||||
|
|
||||||
protocol StatusListService {
|
protocol StatusListService {
|
||||||
var statusSections: AnyPublisher<[[Status]], Error> { get }
|
var statusSections: AnyPublisher<[[Status]], Error> { get }
|
||||||
|
var filters: AnyPublisher<[Filter], Error> { get }
|
||||||
var paginates: Bool { get }
|
var paginates: Bool { get }
|
||||||
var contextParentID: String? { get }
|
var contextParentID: String? { get }
|
||||||
func isPinned(status: Status) -> Bool
|
func isPinned(status: Status) -> Bool
|
||||||
|
|
|
@ -21,6 +21,10 @@ struct TimelineService {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelineService: StatusListService {
|
extension TimelineService: StatusListService {
|
||||||
|
var filters: AnyPublisher<[Filter], Error> {
|
||||||
|
contentDatabase.activeFiltersObservation(date: Date(), context: filterContext)
|
||||||
|
}
|
||||||
|
|
||||||
func request(maxID: String?, minID: String?) -> AnyPublisher<Never, Error> {
|
func request(maxID: String?, minID: String?) -> AnyPublisher<Never, Error> {
|
||||||
networkClient.request(Paged(timeline.endpoint, maxID: maxID, minID: minID))
|
networkClient.request(Paged(timeline.endpoint, maxID: maxID, minID: minID))
|
||||||
.map { ($0, timeline) }
|
.map { ($0, timeline) }
|
||||||
|
@ -36,3 +40,14 @@ extension TimelineService: StatusListService {
|
||||||
ContextService(status: status.displayStatus, networkClient: networkClient, contentDatabase: contentDatabase)
|
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 var alertItem: AlertItem?
|
||||||
@Published private(set) var loading = false
|
@Published private(set) var loading = false
|
||||||
private(set) var maintainScrollPositionOfStatusID: String?
|
private(set) var maintainScrollPositionOfStatusID: String?
|
||||||
|
|
||||||
|
@Published private var filterRegularExpression: String?
|
||||||
private var statuses = [String: Status]()
|
private var statuses = [String: Status]()
|
||||||
private let statusListService: StatusListService
|
private let statusListService: StatusListService
|
||||||
private var statusViewModelCache = [Status: (StatusViewModel, AnyCancellable)]()
|
private var statusViewModelCache = [Status: (StatusViewModel, AnyCancellable)]()
|
||||||
|
@ -16,7 +18,22 @@ class StatusListViewModel: ObservableObject {
|
||||||
init(statusListService: StatusListService) {
|
init(statusListService: StatusListService) {
|
||||||
self.statusListService = statusListService
|
self.statusListService = statusListService
|
||||||
|
|
||||||
|
statusListService.filters
|
||||||
|
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||||
|
.map { $0.regularExpression() }
|
||||||
|
.assign(to: &$filterRegularExpression)
|
||||||
|
|
||||||
statusListService.statusSections
|
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
|
.handleEvents(receiveOutput: { [weak self] in
|
||||||
self?.determineIfScrollPositionShouldBeMaintained(newStatusSections: $0)
|
self?.determineIfScrollPositionShouldBeMaintained(newStatusSections: $0)
|
||||||
self?.cleanViewModelCache(newStatusSections: $0)
|
self?.cleanViewModelCache(newStatusSections: $0)
|
||||||
|
|
Loading…
Reference in a new issue