|
|
@ -11,7 +11,7 @@ import SignalUtilitiesKit
|
|
|
|
class GlobalSearchViewController: BaseVC, LibSessionRespondingViewController, UITableViewDelegate, UITableViewDataSource {
|
|
|
|
class GlobalSearchViewController: BaseVC, LibSessionRespondingViewController, UITableViewDelegate, UITableViewDataSource {
|
|
|
|
fileprivate typealias SectionModel = ArraySection<SearchSection, SessionThreadViewModel>
|
|
|
|
fileprivate typealias SectionModel = ArraySection<SearchSection, SessionThreadViewModel>
|
|
|
|
|
|
|
|
|
|
|
|
fileprivate struct SearchResultData {
|
|
|
|
fileprivate struct SearchResultData: Equatable {
|
|
|
|
var state: SearchResultsState
|
|
|
|
var state: SearchResultsState
|
|
|
|
var data: [SectionModel]
|
|
|
|
var data: [SectionModel]
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -41,7 +41,31 @@ class GlobalSearchViewController: BaseVC, LibSessionRespondingViewController, UI
|
|
|
|
// MARK: - Variables
|
|
|
|
// MARK: - Variables
|
|
|
|
|
|
|
|
|
|
|
|
private let dependencies: Dependencies
|
|
|
|
private let dependencies: Dependencies
|
|
|
|
private lazy var defaultSearchResults: SearchResultData = GlobalSearchViewController.getDefaultSearchResults(using: dependencies)
|
|
|
|
private var defaultSearchResults: SearchResultData = SearchResultData(state: .none, data: []) {
|
|
|
|
|
|
|
|
didSet {
|
|
|
|
|
|
|
|
guard searchText.isEmpty else { return }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// If we have no search term then the contact list should be showing, so update the results and reload the table
|
|
|
|
|
|
|
|
self.searchResultSet = defaultSearchResults
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
switch Thread.isMainThread {
|
|
|
|
|
|
|
|
case true: self.tableView.reloadData()
|
|
|
|
|
|
|
|
case false: DispatchQueue.main.async { self.tableView.reloadData() }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
private lazy var defaultSearchResultsObservation = ValueObservation
|
|
|
|
|
|
|
|
.trackingConstantRegion { [dependencies] db -> [SessionThreadViewModel] in
|
|
|
|
|
|
|
|
try SessionThreadViewModel
|
|
|
|
|
|
|
|
.defaultContactsQuery(using: dependencies)
|
|
|
|
|
|
|
|
.fetchAll(db)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.map { GlobalSearchViewController.processDefaultSearchResults($0) }
|
|
|
|
|
|
|
|
.removeDuplicates()
|
|
|
|
|
|
|
|
.handleEvents(didFail: { SNLog("[GlobalSearch] Observation failed with error: \($0)") })
|
|
|
|
|
|
|
|
private var defaultDataChangeObservable: DatabaseCancellable? {
|
|
|
|
|
|
|
|
didSet { oldValue?.cancel() } // Cancel the old observable if there was one
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@ThreadSafeObject private var readConnection: Database? = nil
|
|
|
|
@ThreadSafeObject private var readConnection: Database? = nil
|
|
|
|
private lazy var searchResultSet: SearchResultData = defaultSearchResults
|
|
|
|
private lazy var searchResultSet: SearchResultData = defaultSearchResults
|
|
|
@ -115,6 +139,18 @@ class GlobalSearchViewController: BaseVC, LibSessionRespondingViewController, UI
|
|
|
|
setupNavigationBar()
|
|
|
|
setupNavigationBar()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
override func viewWillAppear(_ animated: Bool) {
|
|
|
|
|
|
|
|
super.viewWillAppear(animated)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
defaultDataChangeObservable = dependencies[singleton: .storage].start(
|
|
|
|
|
|
|
|
defaultSearchResultsObservation,
|
|
|
|
|
|
|
|
onError: { _ in },
|
|
|
|
|
|
|
|
onChange: { [weak self] updatedDefaultResults in
|
|
|
|
|
|
|
|
self?.defaultSearchResults = updatedDefaultResults
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public override func viewDidAppear(_ animated: Bool) {
|
|
|
|
public override func viewDidAppear(_ animated: Bool) {
|
|
|
|
super.viewDidAppear(animated)
|
|
|
|
super.viewDidAppear(animated)
|
|
|
|
searchBar.becomeFirstResponder()
|
|
|
|
searchBar.becomeFirstResponder()
|
|
|
@ -123,6 +159,8 @@ class GlobalSearchViewController: BaseVC, LibSessionRespondingViewController, UI
|
|
|
|
public override func viewWillDisappear(_ animated: Bool) {
|
|
|
|
public override func viewWillDisappear(_ animated: Bool) {
|
|
|
|
super.viewWillDisappear(animated)
|
|
|
|
super.viewWillDisappear(animated)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
self.defaultDataChangeObservable = nil
|
|
|
|
|
|
|
|
|
|
|
|
UIView.performWithoutAnimation {
|
|
|
|
UIView.performWithoutAnimation {
|
|
|
|
searchBar.resignFirstResponder()
|
|
|
|
searchBar.resignFirstResponder()
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -169,49 +207,42 @@ class GlobalSearchViewController: BaseVC, LibSessionRespondingViewController, UI
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - Update Search Results
|
|
|
|
// MARK: - Update Search Results
|
|
|
|
|
|
|
|
|
|
|
|
private static func getDefaultSearchResults(using dependencies: Dependencies) -> SearchResultData {
|
|
|
|
private static func processDefaultSearchResults(_ contacts: [SessionThreadViewModel]) -> SearchResultData {
|
|
|
|
let nonalphabeticNameTitle: String = "#" // stringlint:ignore
|
|
|
|
let nonalphabeticNameTitle: String = "#" // stringlint:ignore
|
|
|
|
let contacts: [SessionThreadViewModel] = dependencies[singleton: .storage].read { [dependencies] db -> [SessionThreadViewModel]? in
|
|
|
|
|
|
|
|
try SessionThreadViewModel
|
|
|
|
|
|
|
|
.defaultContactsQuery(using: dependencies)
|
|
|
|
|
|
|
|
.fetchAll(db)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.defaulting(to: [])
|
|
|
|
|
|
|
|
.sorted {
|
|
|
|
|
|
|
|
$0.displayName.lowercased() < $1.displayName.lowercased()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var groupedContacts: [String: SectionModel] = [:]
|
|
|
|
return SearchResultData(
|
|
|
|
contacts.forEach { contactViewModel in
|
|
|
|
state: .defaultContacts,
|
|
|
|
guard !contactViewModel.threadIsNoteToSelf else {
|
|
|
|
data: contacts
|
|
|
|
groupedContacts[""] = SectionModel(
|
|
|
|
.sorted { lhs, rhs in lhs.displayName.lowercased() < rhs.displayName.lowercased() }
|
|
|
|
|
|
|
|
.reduce(into: [String: SectionModel]()) { result, next in
|
|
|
|
|
|
|
|
guard !next.threadIsNoteToSelf else {
|
|
|
|
|
|
|
|
result[""] = SectionModel(
|
|
|
|
model: .groupedContacts(title: ""),
|
|
|
|
model: .groupedContacts(title: ""),
|
|
|
|
elements: [contactViewModel]
|
|
|
|
elements: [next]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let displayName = NSMutableString(string: contactViewModel.displayName)
|
|
|
|
let displayName = NSMutableString(string: next.displayName)
|
|
|
|
CFStringTransform(displayName, nil, kCFStringTransformToLatin, false)
|
|
|
|
CFStringTransform(displayName, nil, kCFStringTransformToLatin, false)
|
|
|
|
CFStringTransform(displayName, nil, kCFStringTransformStripDiacritics, false)
|
|
|
|
CFStringTransform(displayName, nil, kCFStringTransformStripDiacritics, false)
|
|
|
|
|
|
|
|
|
|
|
|
let initialCharacter: String = (displayName.length > 0 ? displayName.substring(to: 1) : "")
|
|
|
|
let initialCharacter: String = (displayName.length > 0 ? displayName.substring(to: 1) : "")
|
|
|
|
let section: String = initialCharacter.capitalized.isSingleAlphabet ?
|
|
|
|
let section: String = (initialCharacter.capitalized.isSingleAlphabet ?
|
|
|
|
initialCharacter.capitalized :
|
|
|
|
initialCharacter.capitalized :
|
|
|
|
nonalphabeticNameTitle
|
|
|
|
nonalphabeticNameTitle
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if groupedContacts[section] == nil {
|
|
|
|
if result[section] == nil {
|
|
|
|
groupedContacts[section] = SectionModel(
|
|
|
|
result[section] = SectionModel(
|
|
|
|
model: .groupedContacts(title: section),
|
|
|
|
model: .groupedContacts(title: section),
|
|
|
|
elements: []
|
|
|
|
elements: []
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
groupedContacts[section]?.elements.append(contactViewModel)
|
|
|
|
result[section]?.elements.append(next)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
.values
|
|
|
|
return SearchResultData(
|
|
|
|
.sorted { sectionModel0, sectionModel1 in
|
|
|
|
state: .defaultContacts,
|
|
|
|
|
|
|
|
data: groupedContacts.values.sorted { sectionModel0, sectionModel1 in
|
|
|
|
|
|
|
|
let title0: String = {
|
|
|
|
let title0: String = {
|
|
|
|
switch sectionModel0.model {
|
|
|
|
switch sectionModel0.model {
|
|
|
|
case .groupedContacts(let title): return title
|
|
|
|
case .groupedContacts(let title): return title
|
|
|
@ -404,16 +435,7 @@ extension GlobalSearchViewController {
|
|
|
|
viewController: self,
|
|
|
|
viewController: self,
|
|
|
|
navigatableStateHolder: nil,
|
|
|
|
navigatableStateHolder: nil,
|
|
|
|
using: dependencies
|
|
|
|
using: dependencies
|
|
|
|
) { [weak self, dependencies] in
|
|
|
|
)
|
|
|
|
let updatedDefaultResults: SearchResultData = GlobalSearchViewController
|
|
|
|
|
|
|
|
.getDefaultSearchResults(using: dependencies)
|
|
|
|
|
|
|
|
self?.defaultSearchResults = updatedDefaultResults
|
|
|
|
|
|
|
|
self?.searchResultSet = updatedDefaultResults
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
|
|
|
self?.tableView.reloadData()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|