mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
294 lines
11 KiB
Swift
294 lines
11 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import UIKit
|
|
import SignalUtilitiesKit
|
|
|
|
public class ConversationSearchController: NSObject {
|
|
public static let minimumSearchTextLength: UInt = 2
|
|
|
|
private let threadId: String
|
|
public weak var delegate: ConversationSearchControllerDelegate?
|
|
public let uiSearchController: UISearchController = UISearchController(searchResultsController: nil)
|
|
public let resultsBar: SearchResultsBar = SearchResultsBar()
|
|
|
|
private var lastSearchText: String?
|
|
|
|
// MARK: Initializer
|
|
|
|
public init(threadId: String) {
|
|
self.threadId = threadId
|
|
|
|
super.init()
|
|
|
|
self.resultsBar.resultsBarDelegate = self
|
|
self.uiSearchController.delegate = self
|
|
self.uiSearchController.searchResultsUpdater = self
|
|
|
|
self.uiSearchController.hidesNavigationBarDuringPresentation = false
|
|
self.uiSearchController.searchBar.inputAccessoryView = resultsBar
|
|
}
|
|
}
|
|
|
|
// MARK: - UISearchControllerDelegate
|
|
|
|
extension ConversationSearchController: UISearchControllerDelegate {
|
|
public func didPresentSearchController(_ searchController: UISearchController) {
|
|
delegate?.didPresentSearchController?(searchController)
|
|
}
|
|
|
|
public func didDismissSearchController(_ searchController: UISearchController) {
|
|
delegate?.didDismissSearchController?(searchController)
|
|
}
|
|
}
|
|
|
|
// MARK: - UISearchResultsUpdating
|
|
|
|
extension ConversationSearchController: UISearchResultsUpdating {
|
|
public func updateSearchResults(for searchController: UISearchController) {
|
|
Logger.verbose("searchBar.text: \( searchController.searchBar.text ?? "<blank>")")
|
|
|
|
guard
|
|
let searchText: String = searchController.searchBar.text?.stripped,
|
|
searchText.count >= ConversationSearchController.minimumSearchTextLength
|
|
else {
|
|
self.resultsBar.updateResults(results: nil)
|
|
self.delegate?.conversationSearchController(self, didUpdateSearchResults: nil, searchText: nil)
|
|
return
|
|
}
|
|
|
|
let threadId: String = self.threadId
|
|
let results: [Int64] = GRDBStorage.shared.read { db -> [Int64] in
|
|
try Interaction.idsForTermWithin(
|
|
threadId: threadId,
|
|
pattern: try SessionThreadViewModel.pattern(db, searchTerm: searchText)
|
|
)
|
|
.fetchAll(db)
|
|
}
|
|
.defaulting(to: [])
|
|
|
|
self.resultsBar.updateResults(results: results)
|
|
self.delegate?.conversationSearchController(self, didUpdateSearchResults: results, searchText: searchText)
|
|
}
|
|
}
|
|
|
|
// MARK: - SearchResultsBarDelegate
|
|
|
|
extension ConversationSearchController: SearchResultsBarDelegate {
|
|
func searchResultsBar(
|
|
_ searchResultsBar: SearchResultsBar,
|
|
setCurrentIndex currentIndex: Int,
|
|
results: [Int64]
|
|
) {
|
|
guard let interactionId: Int64 = results[safe: currentIndex] else { return }
|
|
|
|
self.delegate?.conversationSearchController(self, didSelectInteractionId: interactionId)
|
|
}
|
|
}
|
|
|
|
protocol SearchResultsBarDelegate: AnyObject {
|
|
func searchResultsBar(
|
|
_ searchResultsBar: SearchResultsBar,
|
|
setCurrentIndex currentIndex: Int,
|
|
results: [Int64]
|
|
)
|
|
}
|
|
|
|
public final class SearchResultsBar: UIView {
|
|
private var results: [Int64]?
|
|
var currentIndex: Int?
|
|
weak var resultsBarDelegate: SearchResultsBarDelegate?
|
|
|
|
public override var intrinsicContentSize: CGSize { CGSize.zero }
|
|
|
|
private lazy var label: UILabel = {
|
|
let result = UILabel()
|
|
result.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
|
result.textColor = Colors.text
|
|
return result
|
|
}()
|
|
|
|
private lazy var upButton: UIButton = {
|
|
let icon = #imageLiteral(resourceName: "ic_chevron_up").withRenderingMode(.alwaysTemplate)
|
|
let result = UIButton()
|
|
result.setImage(icon, for: UIControl.State.normal)
|
|
result.tintColor = Colors.accent
|
|
result.addTarget(self, action: #selector(handleUpButtonTapped), for: UIControl.Event.touchUpInside)
|
|
return result
|
|
}()
|
|
|
|
private lazy var downButton: UIButton = {
|
|
let icon = #imageLiteral(resourceName: "ic_chevron_down").withRenderingMode(.alwaysTemplate)
|
|
let result = UIButton()
|
|
result.setImage(icon, for: UIControl.State.normal)
|
|
result.tintColor = Colors.accent
|
|
result.addTarget(self, action: #selector(handleDownButtonTapped), for: UIControl.Event.touchUpInside)
|
|
return result
|
|
}()
|
|
|
|
private lazy var loadingIndicator: UIActivityIndicatorView = {
|
|
let result = UIActivityIndicatorView(style: .medium)
|
|
result.tintColor = Colors.text
|
|
result.alpha = 0.5
|
|
result.hidesWhenStopped = true
|
|
return result
|
|
}()
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
setUpViewHierarchy()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
super.init(coder: coder)
|
|
setUpViewHierarchy()
|
|
}
|
|
|
|
private func setUpViewHierarchy() {
|
|
autoresizingMask = .flexibleHeight
|
|
|
|
// Background & blur
|
|
let backgroundView = UIView()
|
|
backgroundView.backgroundColor = isLightMode ? .white : .black
|
|
backgroundView.alpha = Values.lowOpacity
|
|
addSubview(backgroundView)
|
|
backgroundView.pin(to: self)
|
|
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
|
|
addSubview(blurView)
|
|
blurView.pin(to: self)
|
|
|
|
// Separator
|
|
let separator = UIView()
|
|
separator.backgroundColor = Colors.text.withAlphaComponent(0.2)
|
|
separator.set(.height, to: 1 / UIScreen.main.scale)
|
|
addSubview(separator)
|
|
separator.pin([ UIView.HorizontalEdge.leading, UIView.VerticalEdge.top, UIView.HorizontalEdge.trailing ], to: self)
|
|
|
|
// Spacers
|
|
let spacer1 = UIView.hStretchingSpacer()
|
|
let spacer2 = UIView.hStretchingSpacer()
|
|
|
|
// Button containers
|
|
let upButtonContainer = UIView(wrapping: upButton, withInsets: UIEdgeInsets(top: 2, left: 0, bottom: 0, right: 0))
|
|
let downButtonContainer = UIView(wrapping: downButton, withInsets: UIEdgeInsets(top: 0, left: 0, bottom: 2, right: 0))
|
|
|
|
// Main stack view
|
|
let mainStackView = UIStackView(arrangedSubviews: [ upButtonContainer, downButtonContainer, spacer1, label, spacer2 ])
|
|
mainStackView.axis = .horizontal
|
|
mainStackView.spacing = Values.mediumSpacing
|
|
mainStackView.isLayoutMarginsRelativeArrangement = true
|
|
mainStackView.layoutMargins = UIEdgeInsets(top: Values.smallSpacing, leading: Values.largeSpacing, bottom: Values.smallSpacing, trailing: Values.largeSpacing)
|
|
addSubview(mainStackView)
|
|
|
|
mainStackView.pin(.top, to: .bottom, of: separator)
|
|
mainStackView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: self)
|
|
mainStackView.pin(.bottom, to: .bottom, of: self, withInset: -2)
|
|
|
|
addSubview(loadingIndicator)
|
|
loadingIndicator.pin(.left, to: .right, of: label, withInset: 10)
|
|
loadingIndicator.centerYAnchor.constraint(equalTo: label.centerYAnchor).isActive = true
|
|
|
|
// Remaining constraints
|
|
label.center(.horizontal, in: self)
|
|
}
|
|
|
|
// MARK: - Functions
|
|
|
|
@objc
|
|
public func handleUpButtonTapped() {
|
|
guard let results: [Int64] = results else { return }
|
|
guard let currentIndex: Int = currentIndex else { return }
|
|
guard currentIndex + 1 < results.count else { return }
|
|
|
|
let newIndex = currentIndex + 1
|
|
self.currentIndex = newIndex
|
|
updateBarItems()
|
|
resultsBarDelegate?.searchResultsBar(self, setCurrentIndex: newIndex, results: results)
|
|
}
|
|
|
|
@objc
|
|
public func handleDownButtonTapped() {
|
|
Logger.debug("")
|
|
guard let results: [Int64] = results else { return }
|
|
guard let currentIndex: Int = currentIndex, currentIndex > 0 else { return }
|
|
|
|
let newIndex = currentIndex - 1
|
|
self.currentIndex = newIndex
|
|
updateBarItems()
|
|
resultsBarDelegate?.searchResultsBar(self, setCurrentIndex: newIndex, results: results)
|
|
}
|
|
|
|
func updateResults(results: [Int64]?) {
|
|
currentIndex = {
|
|
guard let results: [Int64] = results, !results.isEmpty else { return nil }
|
|
|
|
if let currentIndex: Int = currentIndex {
|
|
return max(0, min(currentIndex, results.count - 1))
|
|
}
|
|
|
|
return 0
|
|
}()
|
|
|
|
self.results = results
|
|
|
|
updateBarItems()
|
|
|
|
if let currentIndex = currentIndex, let results = results {
|
|
resultsBarDelegate?.searchResultsBar(self, setCurrentIndex: currentIndex, results: results)
|
|
}
|
|
}
|
|
|
|
func updateBarItems() {
|
|
guard let results: [Int64] = results else {
|
|
label.text = ""
|
|
downButton.isEnabled = false
|
|
upButton.isEnabled = false
|
|
return
|
|
}
|
|
|
|
switch results.count {
|
|
case 0:
|
|
// Keyboard toolbar label when no messages match the search string
|
|
label.text = "CONVERSATION_SEARCH_NO_RESULTS".localized()
|
|
|
|
case 1:
|
|
// Keyboard toolbar label when exactly 1 message matches the search string
|
|
label.text = "CONVERSATION_SEARCH_ONE_RESULT".localized()
|
|
|
|
default:
|
|
// Keyboard toolbar label when more than 1 message matches the search string
|
|
//
|
|
// Embeds {{number/position of the 'currently viewed' result}} and
|
|
// the {{total number of results}}
|
|
let format = "CONVERSATION_SEARCH_RESULTS_FORMAT".localized()
|
|
|
|
guard let currentIndex: Int = currentIndex else { return }
|
|
|
|
label.text = String(format: format, currentIndex + 1, results.count)
|
|
}
|
|
|
|
if let currentIndex: Int = currentIndex {
|
|
downButton.isEnabled = currentIndex > 0
|
|
upButton.isEnabled = (currentIndex + 1 < results.count)
|
|
}
|
|
else {
|
|
downButton.isEnabled = false
|
|
upButton.isEnabled = false
|
|
}
|
|
}
|
|
|
|
public func startLoading() {
|
|
loadingIndicator.startAnimating()
|
|
}
|
|
|
|
public func stopLoading() {
|
|
loadingIndicator.stopAnimating()
|
|
}
|
|
}
|
|
|
|
// MARK: - ConversationSearchControllerDelegate
|
|
|
|
public protocol ConversationSearchControllerDelegate: UISearchControllerDelegate {
|
|
func conversationSearchController(_ conversationSearchController: ConversationSearchController, didUpdateSearchResults results: [Int64]?, searchText: String?)
|
|
func conversationSearchController(_ conversationSearchController: ConversationSearchController, didSelectInteractionId: Int64)
|
|
}
|