Add scoped searching of articles
This commit is contained in:
parent
ba36572497
commit
fe2e0155da
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>? {
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue