Add scoped searching of articles

This commit is contained in:
Maurice Parker 2019-08-31 15:53:47 -05:00
parent ba36572497
commit fe2e0155da
7 changed files with 130 additions and 24 deletions

View File

@ -48,6 +48,7 @@ public enum FetchType {
case feed(Feed)
case articleIDs(Set<String>)
case search(String)
case searchWithArticleIDs(String, Set<String>)
}
public final class Account: DisplayNameProvider, UnreadCountProvider, Container, Hashable {
@ -534,6 +535,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
return fetchArticles(articleIDs: articleIDs)
case .search(let searchString):
return fetchArticlesMatching(searchString)
case .searchWithArticleIDs(let searchString, let articleIDs):
return fetchArticlesMatchingWithArticleIDs(searchString, articleIDs)
}
}
@ -553,6 +556,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
fetchArticlesAsync(articleIDs: articleIDs, callback)
case .search(let searchString):
fetchArticlesMatchingAsync(searchString, callback)
case .searchWithArticleIDs(let searchString, let articleIDs):
return fetchArticlesMatchingWithArticleIDsAsync(searchString, articleIDs, callback)
}
}
@ -862,10 +867,18 @@ private extension Account {
return database.fetchArticlesMatching(searchString, flattenedFeeds().feedIDs())
}
func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set<String>) -> Set<Article> {
return database.fetchArticlesMatchingWithArticleIDs(searchString, articleIDs)
}
func fetchArticlesMatchingAsync(_ searchString: String, _ callback: @escaping ArticleSetBlock) {
database.fetchArticlesMatchingAsync(searchString, flattenedFeeds().feedIDs(), callback)
}
func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
database.fetchArticlesMatchingWithArticleIDsAsync(searchString, articleIDs, callback)
}
func fetchArticles(articleIDs: Set<String>) -> Set<Article> {
return database.fetchArticles(articleIDs: articleIDs)
}

View File

@ -68,6 +68,10 @@ public final class ArticlesDatabase {
return articlesTable.fetchArticlesMatching(searchString, feedIDs)
}
public func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set<String>) -> Set<Article> {
return articlesTable.fetchArticlesMatchingWithArticleIDs(searchString, articleIDs)
}
// MARK: - Fetching Articles Async
public func fetchArticlesAsync(_ feedID: String, _ callback: @escaping ArticleSetBlock) {
@ -94,6 +98,10 @@ public final class ArticlesDatabase {
articlesTable.fetchArticlesMatchingAsync(searchString, feedIDs, callback)
}
public func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
articlesTable.fetchArticlesMatchingWithArticleIDsAsync(searchString, articleIDs, callback)
}
// MARK: - Unread Counts
public func fetchUnreadCounts(for feedIDs: Set<String>, _ callback: @escaping UnreadCountCompletionBlock) {

View File

@ -146,39 +146,46 @@ final class ArticlesTable: DatabaseTable {
// MARK: - Fetching Search Articles
func fetchArticlesMatching(_ searchString: String, _ feedIDs: Set<String>) -> Set<Article> {
func fetchArticlesMatching(_ searchString: String) -> Set<Article> {
var articles: Set<Article> = Set<Article>()
queue.fetchSync { (database) in
articles = self.fetchArticlesMatching(searchString, database)
}
return articles
}
func fetchArticlesMatching(_ searchString: String, _ feedIDs: Set<String>) -> Set<Article> {
var articles = fetchArticlesMatching(searchString)
articles = articles.filter{ feedIDs.contains($0.feedID) }
return articles
}
func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set<String>) -> Set<Article> {
var articles = fetchArticlesMatching(searchString)
articles = articles.filter{ articleIDs.contains($0.articleID) }
return articles
}
func fetchArticlesMatchingAsync(_ searchString: String, _ feedIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
fetchArticlesAsync({ self.fetchArticlesMatching(searchString, feedIDs, $0) }, callback)
}
private func fetchArticlesMatching(_ searchString: String, _ feedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
let sql = "select rowid from search where search match ?;"
let sqlSearchString = sqliteSearchString(with: searchString)
let searchStringParameters = [sqlSearchString]
guard let resultSet = database.executeQuery(sql, withArgumentsIn: searchStringParameters) else {
return Set<Article>()
}
let searchRowIDs = resultSet.mapToSet { $0.longLongInt(forColumnIndex: 0) }
if searchRowIDs.isEmpty {
return Set<Article>()
}
func fetchArticlesMatchingWithArticleIDsAsync(_ searchString: String, _ articleIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
fetchArticlesAsync({ self.fetchArticlesMatchingWithArticleIDs(searchString, articleIDs, $0) }, callback)
}
let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(searchRowIDs.count))!
let whereClause = "searchRowID in \(placeholders)"
let parameters: [AnyObject] = Array(searchRowIDs) as [AnyObject]
let articles = fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: true)
private func fetchArticlesMatching(_ searchString: String, _ feedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
let articles = fetchArticlesMatching(searchString, database)
// TODO: include the feedIDs in the SQL rather than filtering here.
return articles.filter{ feedIDs.contains($0.feedID) }
}
private func fetchArticlesMatchingWithArticleIDs(_ searchString: String, _ articleIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
let articles = fetchArticlesMatching(searchString, database)
// TODO: include the articleIDs in the SQL rather than filtering here.
return articles.filter{ articleIDs.contains($0.articleID) }
}
// MARK: - Fetching Articles for Indexer
func fetchArticleSearchInfos(_ articleIDs: Set<String>, in database: FMDatabase) -> Set<ArticleSearchInfo>? {

View File

@ -49,6 +49,8 @@
51934CCB230F599B006127BE /* ThemedNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CC1230F5963006127BE /* ThemedNavigationController.swift */; };
51934CCE2310792F006127BE /* ActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CCD2310792F006127BE /* ActivityManager.swift */; };
51934CD023108953006127BE /* ActivityID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CCF23108953006127BE /* ActivityID.swift */; };
51938DF2231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */; };
51938DF3231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */; };
519B8D332143397200FA689C /* SharingServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519B8D322143397200FA689C /* SharingServiceDelegate.swift */; };
519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E743422C663F900A78E47 /* SceneDelegate.swift */; };
51C451A9226377C200C03939 /* ArticlesDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8407167F2262A61100344432 /* ArticlesDatabase.framework */; };
@ -710,6 +712,7 @@
51934CC1230F5963006127BE /* ThemedNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemedNavigationController.swift; sourceTree = "<group>"; };
51934CCD2310792F006127BE /* ActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityManager.swift; sourceTree = "<group>"; };
51934CCF23108953006127BE /* ActivityID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityID.swift; sourceTree = "<group>"; };
51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTimelineFeedDelegate.swift; sourceTree = "<group>"; };
5194B5ED22B6965300144881 /* SettingsSubscriptionsImportDocumentPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSubscriptionsImportDocumentPickerView.swift; sourceTree = "<group>"; };
5194B5F122B69FCC00144881 /* SettingsSubscriptionsExportDocumentPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSubscriptionsExportDocumentPickerView.swift; sourceTree = "<group>"; };
519B8D322143397200FA689C /* SharingServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingServiceDelegate.swift; sourceTree = "<group>"; };
@ -1813,6 +1816,7 @@
84F2D5361FC22FCB00998D64 /* TodayFeedDelegate.swift */,
845EE7B01FC2366500854A1F /* StarredFeedDelegate.swift */,
8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */,
51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */,
84AD1EB92031649C00BC20B7 /* SmartFeedPasteboardWriter.swift */,
);
path = SmartFeeds;
@ -2419,6 +2423,7 @@
5126EE97226CB48A00C22AFC /* AppCoordinator.swift in Sources */,
84CAFCB022BC8C35007694F0 /* FetchRequestOperation.swift in Sources */,
51EF0F77227716200050506E /* FaviconGenerator.swift in Sources */,
51938DF3231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */,
51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */,
5183CCEF227125970010922C /* SettingsViewController.swift in Sources */,
51F85BE5227217D000C787DC /* RefreshIntervalViewController.swift in Sources */,
@ -2565,6 +2570,7 @@
849A97531ED9EAC0007D329B /* AddFeedController.swift in Sources */,
5183CCE8226F68D90010922C /* AccountRefreshTimer.swift in Sources */,
849A97831ED9EC63007D329B /* SidebarStatusBarView.swift in Sources */,
51938DF2231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */,
84F2D5381FC22FCC00998D64 /* TodayFeedDelegate.swift in Sources */,
841ABA5E20145E9200980E11 /* FolderInspectorViewController.swift in Sources */,
84DEE56522C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */,

View File

@ -0,0 +1,31 @@
//
// SearchTimelineFeedDelegate.swift
// NetNewsWire
//
// Created by Maurice Parker on 8/31/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import Foundation
import Account
import Articles
struct SearchTimelineFeedDelegate: SmartFeedDelegate {
var nameForDisplay: String {
return nameForDisplayPrefix + searchString
}
let nameForDisplayPrefix = NSLocalizedString("Search: ", comment: "Search smart feed title prefix")
let searchString: String
let fetchType: FetchType
init(searchString: String, articleIDs: Set<String>) {
self.searchString = searchString
self.fetchType = .searchWithArticleIDs(searchString, articleIDs)
}
func fetchUnreadCount(for: Account, callback: @escaping (Int) -> Void) {
// TODO: after 5.0
}
}

View File

@ -12,6 +12,11 @@ import Articles
import RSCore
import RSTree
enum SearchScope: Int {
case timeline = 0
case global = 1
}
class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
var undoableCommands = [UndoableCommand]()
@ -49,7 +54,9 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
private var expandedNodes = [Node]()
private var shadowTable = [[Node]]()
private var lastSearchString = ""
private var lastSearchScope: SearchScope? = nil
private var isSearching: Bool = false
private var searchArticleIds: Set<String>? = nil
private(set) var sortDirection = AppDefaults.timelineSortDirection {
didSet {
@ -517,25 +524,41 @@ class AppCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
}
func searchArticles(_ searchString: String) {
func searchArticles(_ searchString: String, _ searchScope: SearchScope) {
guard !searchString.isEmpty else {
isSearching = false
lastSearchString = ""
lastSearchScope = nil
searchArticleIds = nil
if let ip = currentMasterIndexPath, let node = nodeFor(ip), let fetcher = node.representedObject as? ArticleFetcher {
timelineFetcher = fetcher
}
return
}
isSearching = true
if !isSearching {
isSearching = true
searchArticleIds = Set(articles.map { $0.articleID })
}
if searchString.count < 3 {
timelineFetcher = nil
return
}
if searchString != lastSearchString {
timelineFetcher = SmartFeed(delegate: SearchFeedDelegate(searchString: searchString))
if searchString != lastSearchString || searchScope != lastSearchScope {
switch searchScope {
case .global:
timelineFetcher = SmartFeed(delegate: SearchFeedDelegate(searchString: searchString))
case .timeline:
timelineFetcher = SmartFeed(delegate: SearchTimelineFeedDelegate(searchString: searchString, articleIDs: searchArticleIds!))
}
lastSearchString = searchString
lastSearchScope = searchScope
}
}

View File

@ -44,10 +44,16 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
// Setup the Search Controller
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
searchController.searchBar.delegate = self
searchController.searchBar.placeholder = NSLocalizedString("Search Articles", comment: "Search Articles")
searchController.searchBar.showsScopeBar = true
searchController.searchBar.scopeButtonTitles = [
NSLocalizedString("Here", comment: "Here"),
NSLocalizedString("All Articles", comment: "All Articles")
]
navigationItem.searchController = searchController
definesPresentationContext = true
// Setup the Refresh Control
refreshControl = UIRefreshControl()
refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged)
@ -403,12 +409,24 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
}
// MARK: UISearchResultsUpdating
// MARK: Searching
extension MasterTimelineViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
coordinator.searchArticles(searchController.searchBar.text!)
let searchScope = SearchScope(rawValue: searchController.searchBar.selectedScopeButtonIndex)!
coordinator.searchArticles(searchController.searchBar.text!, searchScope)
}
}
extension MasterTimelineViewController: UISearchBarDelegate {
func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
let searchScope = SearchScope(rawValue: selectedScope)!
coordinator.searchArticles(searchBar.text!, searchScope)
}
}
// MARK: Private