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
@ -114,6 +138,18 @@ class GlobalSearchViewController: BaseVC, LibSessionRespondingViewController, UI
navigationItem.hidesBackButton = true navigationItem.hidesBackButton = true
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)
@ -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,68 +207,61 @@ 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] = [:]
contacts.forEach { contactViewModel in
guard !contactViewModel.threadIsNoteToSelf else {
groupedContacts[""] = SectionModel(
model: .groupedContacts(title: ""),
elements: [contactViewModel]
)
return
}
let displayName = NSMutableString(string: contactViewModel.displayName)
CFStringTransform(displayName, nil, kCFStringTransformToLatin, false)
CFStringTransform(displayName, nil, kCFStringTransformStripDiacritics, false)
let initialCharacter: String = (displayName.length > 0 ? displayName.substring(to: 1) : "")
let section: String = initialCharacter.capitalized.isSingleAlphabet ?
initialCharacter.capitalized :
nonalphabeticNameTitle
if groupedContacts[section] == nil {
groupedContacts[section] = SectionModel(
model: .groupedContacts(title: section),
elements: []
)
}
groupedContacts[section]?.elements.append(contactViewModel)
}
return SearchResultData( return SearchResultData(
state: .defaultContacts, state: .defaultContacts,
data: groupedContacts.values.sorted { sectionModel0, sectionModel1 in data: contacts
let title0: String = { .sorted { lhs, rhs in lhs.displayName.lowercased() < rhs.displayName.lowercased() }
switch sectionModel0.model { .reduce(into: [String: SectionModel]()) { result, next in
case .groupedContacts(let title): return title guard !next.threadIsNoteToSelf else {
default: return "" result[""] = SectionModel(
model: .groupedContacts(title: ""),
elements: [next]
)
return
} }
}()
let title1: String = { let displayName = NSMutableString(string: next.displayName)
switch sectionModel1.model { CFStringTransform(displayName, nil, kCFStringTransformToLatin, false)
case .groupedContacts(let title): return title CFStringTransform(displayName, nil, kCFStringTransformStripDiacritics, false)
default: return ""
let initialCharacter: String = (displayName.length > 0 ? displayName.substring(to: 1) : "")
let section: String = (initialCharacter.capitalized.isSingleAlphabet ?
initialCharacter.capitalized :
nonalphabeticNameTitle
)
if result[section] == nil {
result[section] = SectionModel(
model: .groupedContacts(title: section),
elements: []
)
} }
}() result[section]?.elements.append(next)
}
if ![title0, title1].contains(nonalphabeticNameTitle) { .values
return title0 < title1 .sorted { sectionModel0, sectionModel1 in
let title0: String = {
switch sectionModel0.model {
case .groupedContacts(let title): return title
default: return ""
}
}()
let title1: String = {
switch sectionModel1.model {
case .groupedContacts(let title): return title
default: return ""
}
}()
if ![title0, title1].contains(nonalphabeticNameTitle) {
return title0 < title1
}
return title1 == nonalphabeticNameTitle
} }
return title1 == nonalphabeticNameTitle
}
) )
} }
@ -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,24 +139,15 @@ 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( try SessionThread.deleteOrLeave(
updates: { db in db,
try SessionThread.deleteOrLeave( type: .deleteContactConversationAndMarkHidden,
db, threadId: threadViewModel.threadId,
type: .deleteContactConversationAndMarkHidden, threadVariant: threadViewModel.threadVariant,
threadId: threadViewModel.threadId, using: dependencies
threadVariant: threadViewModel.threadVariant,
using: dependencies
)
},
completion: { result in
switch result {
case .failure: break
case .success: onActionSuccess?()
}
}
) )
}
completionHandler(true) completionHandler(true)
}, },
@ -201,24 +189,15 @@ 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( try SessionThread.deleteOrLeave(
updates: { db in db,
try SessionThread.deleteOrLeave( type: .hideContactConversation,
db, threadId: threadViewModel.threadId,
type: .hideContactConversation, threadVariant: threadViewModel.threadVariant,
threadId: threadViewModel.threadId, using: dependencies
threadVariant: threadViewModel.threadVariant,
using: dependencies
)
},
completion: { result in
switch result {
case .failure: break
case .success: onActionSuccess?()
}
}
) )
}
completionHandler(true) completionHandler(true)
}, },
@ -260,25 +239,16 @@ 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( try SessionThread
updates: { db in .filter(id: threadViewModel.threadId)
try SessionThread .updateAllAndConfig(
.filter(id: threadViewModel.threadId) db,
.updateAllAndConfig( SessionThread.Columns.pinnedPriority
db, .set(to: (threadViewModel.threadPinnedPriority == 0 ? 1 : 0)),
SessionThread.Columns.pinnedPriority using: dependencies
.set(to: (threadViewModel.threadPinnedPriority == 0 ? 1 : 0)), )
using: dependencies }
)
},
completion: { result in
switch result {
case .failure: break
case .success: onActionSuccess?()
}
}
)
} }
} }
@ -313,34 +283,25 @@ 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( let currentValue: TimeInterval? = try SessionThread
updates: { db in .filter(id: threadViewModel.threadId)
let currentValue: TimeInterval? = try SessionThread .select(.mutedUntilTimestamp)
.filter(id: threadViewModel.threadId) .asRequest(of: TimeInterval.self)
.select(.mutedUntilTimestamp) .fetchOne(db)
.asRequest(of: TimeInterval.self)
.fetchOne(db) try SessionThread
.filter(id: threadViewModel.threadId)
try SessionThread .updateAll(
.filter(id: threadViewModel.threadId) db,
.updateAll( SessionThread.Columns.mutedUntilTimestamp.set(
db, to: (currentValue == nil ?
SessionThread.Columns.mutedUntilTimestamp.set( Date.distantFuture.timeIntervalSince1970 :
to: (currentValue == nil ? nil
Date.distantFuture.timeIntervalSince1970 :
nil
)
)
) )
}, )
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,47 +520,38 @@ public extension UIContextualAction {
} }
}() }()
dependencies[singleton: .storage] dependencies[singleton: .storage].writeAsync { db in
.writeAsync( do {
updates: { db in try SessionThread.deleteOrLeave(
do { db,
try SessionThread.deleteOrLeave( type: deletionType,
db, threadId: threadViewModel.threadId,
type: deletionType, threadVariant: threadViewModel.threadVariant,
threadId: threadViewModel.threadId, using: dependencies
threadVariant: threadViewModel.threadVariant, )
using: dependencies } catch {
) DispatchQueue.main.async {
} catch { let toastBody: String = {
DispatchQueue.main.async { switch threadViewModel.threadVariant {
let toastBody: String = { case .legacyGroup, .group:
switch threadViewModel.threadVariant { return "groupLeaveErrorFailed"
case .legacyGroup, .group: .put(key: "group_name", value: threadViewModel.displayName)
return "groupLeaveErrorFailed" .localized()
.put(key: "group_name", value: threadViewModel.displayName)
.localized() default:
return "communityLeaveError"
default: .put(key: "community_name", value: threadViewModel.displayName)
return "communityLeaveError" .localized()
.put(key: "community_name", value: threadViewModel.displayName)
.localized()
}
}()
navigatableStateHolder?.showToast(
text: toastBody,
backgroundColor: .backgroundSecondary
)
} }
throw error }()
} navigatableStateHolder?.showToast(
}, text: toastBody,
completion: { result in backgroundColor: .backgroundSecondary
switch result { )
case .failure: break
case .success: onActionSuccess?()
}
} }
) throw error
}
}
completionHandler(true) completionHandler(true)
}, },
@ -704,24 +649,15 @@ public extension UIContextualAction {
} }
}() }()
dependencies[singleton: .storage] dependencies[singleton: .storage].writeAsync { db in
.writeAsync( try SessionThread.deleteOrLeave(
updates: { db in db,
try SessionThread.deleteOrLeave( type: deletionType,
db, threadId: threadViewModel.threadId,
type: deletionType, threadVariant: threadViewModel.threadVariant,
threadId: threadViewModel.threadId, using: dependencies
threadVariant: threadViewModel.threadVariant,
using: dependencies
)
},
completion: { result in
switch result {
case .failure: break
case .success: onActionSuccess?()
}
}
) )
}
completionHandler(true) completionHandler(true)
}, },
@ -759,24 +695,15 @@ 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( try SessionThread.deleteOrLeave(
updates: { db in db,
try SessionThread.deleteOrLeave( type: .deleteContactConversationAndContact,
db, threadId: threadViewModel.threadId,
type: .deleteContactConversationAndContact, threadVariant: threadViewModel.threadVariant,
threadId: threadViewModel.threadId, using: dependencies
threadVariant: threadViewModel.threadVariant,
using: dependencies
)
},
completion: { result in
switch result {
case .failure: break
case .success: onActionSuccess?()
}
}
) )
}
completionHandler(true) completionHandler(true)
}, },

Loading…
Cancel
Save