Updated global search to observe contacts database changes

pull/1061/head
Morgan Pretty 2 months ago
parent e5333c671e
commit 48e23f5bf7

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

@ -50,8 +50,7 @@ public extension UIContextualAction {
threadViewModel: SessionThreadViewModel, threadViewModel: SessionThreadViewModel,
viewController: UIViewController?, viewController: UIViewController?,
navigatableStateHolder: NavigatableStateHolder?, navigatableStateHolder: NavigatableStateHolder?,
using dependencies: Dependencies, using dependencies: Dependencies
onActionSuccess: (() -> ())? = nil
) -> [UIContextualAction]? { ) -> [UIContextualAction]? {
guard !actions.isEmpty else { return nil } guard !actions.isEmpty else { return nil }
@ -114,8 +113,6 @@ public extension UIContextualAction {
case false: threadViewModel.markAsUnread(using: dependencies) case false: threadViewModel.markAsUnread(using: dependencies)
} }
onActionSuccess?()
} }
completionHandler(true) completionHandler(true)
} }
@ -142,9 +139,7 @@ public extension UIContextualAction {
cancelStyle: .alert_text, cancelStyle: .alert_text,
dismissOnConfirm: true, dismissOnConfirm: true,
onConfirm: { _ in onConfirm: { _ in
dependencies[singleton: .storage] dependencies[singleton: .storage].writeAsync { db in
.writeAsync(
updates: { db in
try SessionThread.deleteOrLeave( try SessionThread.deleteOrLeave(
db, db,
type: .deleteContactConversationAndMarkHidden, type: .deleteContactConversationAndMarkHidden,
@ -152,14 +147,7 @@ public extension UIContextualAction {
threadVariant: threadViewModel.threadVariant, threadVariant: threadViewModel.threadVariant,
using: dependencies using: dependencies
) )
},
completion: { result in
switch result {
case .failure: break
case .success: onActionSuccess?()
}
} }
)
completionHandler(true) completionHandler(true)
}, },
@ -201,9 +189,7 @@ public extension UIContextualAction {
cancelStyle: .alert_text, cancelStyle: .alert_text,
dismissOnConfirm: true, dismissOnConfirm: true,
onConfirm: { _ in onConfirm: { _ in
dependencies[singleton: .storage] dependencies[singleton: .storage].writeAsync { db in
.writeAsync(
updates: { db in
try SessionThread.deleteOrLeave( try SessionThread.deleteOrLeave(
db, db,
type: .hideContactConversation, type: .hideContactConversation,
@ -211,14 +197,7 @@ public extension UIContextualAction {
threadVariant: threadViewModel.threadVariant, threadVariant: threadViewModel.threadVariant,
using: dependencies using: dependencies
) )
},
completion: { result in
switch result {
case .failure: break
case .success: onActionSuccess?()
}
} }
)
completionHandler(true) completionHandler(true)
}, },
@ -260,9 +239,7 @@ public extension UIContextualAction {
// Delay the change to give the cell "unswipe" animation some time to complete // Delay the change to give the cell "unswipe" animation some time to complete
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) { DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) {
dependencies[singleton: .storage] dependencies[singleton: .storage].writeAsync { db in
.writeAsync(
updates: { db in
try SessionThread try SessionThread
.filter(id: threadViewModel.threadId) .filter(id: threadViewModel.threadId)
.updateAllAndConfig( .updateAllAndConfig(
@ -271,14 +248,7 @@ public extension UIContextualAction {
.set(to: (threadViewModel.threadPinnedPriority == 0 ? 1 : 0)), .set(to: (threadViewModel.threadPinnedPriority == 0 ? 1 : 0)),
using: dependencies using: dependencies
) )
},
completion: { result in
switch result {
case .failure: break
case .success: onActionSuccess?()
}
} }
)
} }
} }
@ -313,9 +283,7 @@ public extension UIContextualAction {
// Delay the change to give the cell "unswipe" animation some time to complete // Delay the change to give the cell "unswipe" animation some time to complete
DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) { DispatchQueue.global(qos: .default).asyncAfter(deadline: .now() + unswipeAnimationDelay) {
dependencies[singleton: .storage] dependencies[singleton: .storage].writeAsync { db in
.writeAsync(
updates: { db in
let currentValue: TimeInterval? = try SessionThread let currentValue: TimeInterval? = try SessionThread
.filter(id: threadViewModel.threadId) .filter(id: threadViewModel.threadId)
.select(.mutedUntilTimestamp) .select(.mutedUntilTimestamp)
@ -333,14 +301,7 @@ public extension UIContextualAction {
) )
) )
) )
},
completion: { result in
switch result {
case .failure: break
case .success: onActionSuccess?()
}
} }
)
} }
} }
@ -455,14 +416,7 @@ public extension UIContextualAction {
} }
} }
.subscribe(on: DispatchQueue.global(qos: .userInitiated)) .subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete( .sinkUntilComplete()
receiveCompletion: { result in
switch result {
case .failure: break
case .finished: onActionSuccess?()
}
}
)
} }
} }
@ -566,9 +520,7 @@ public extension UIContextualAction {
} }
}() }()
dependencies[singleton: .storage] dependencies[singleton: .storage].writeAsync { db in
.writeAsync(
updates: { db in
do { do {
try SessionThread.deleteOrLeave( try SessionThread.deleteOrLeave(
db, db,
@ -599,14 +551,7 @@ public extension UIContextualAction {
} }
throw error throw error
} }
},
completion: { result in
switch result {
case .failure: break
case .success: onActionSuccess?()
}
} }
)
completionHandler(true) completionHandler(true)
}, },
@ -704,9 +649,7 @@ public extension UIContextualAction {
} }
}() }()
dependencies[singleton: .storage] dependencies[singleton: .storage].writeAsync { db in
.writeAsync(
updates: { db in
try SessionThread.deleteOrLeave( try SessionThread.deleteOrLeave(
db, db,
type: deletionType, type: deletionType,
@ -714,14 +657,7 @@ public extension UIContextualAction {
threadVariant: threadViewModel.threadVariant, threadVariant: threadViewModel.threadVariant,
using: dependencies using: dependencies
) )
},
completion: { result in
switch result {
case .failure: break
case .success: onActionSuccess?()
} }
}
)
completionHandler(true) completionHandler(true)
}, },
@ -759,9 +695,7 @@ public extension UIContextualAction {
cancelStyle: .alert_text, cancelStyle: .alert_text,
dismissOnConfirm: true, dismissOnConfirm: true,
onConfirm: { _ in onConfirm: { _ in
dependencies[singleton: .storage] dependencies[singleton: .storage].writeAsync { db in
.writeAsync(
updates: { db in
try SessionThread.deleteOrLeave( try SessionThread.deleteOrLeave(
db, db,
type: .deleteContactConversationAndContact, type: .deleteContactConversationAndContact,
@ -769,14 +703,7 @@ public extension UIContextualAction {
threadVariant: threadViewModel.threadVariant, threadVariant: threadViewModel.threadVariant,
using: dependencies using: dependencies
) )
},
completion: { result in
switch result {
case .failure: break
case .success: onActionSuccess?()
} }
}
)
completionHandler(true) completionHandler(true)
}, },

Loading…
Cancel
Save