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.
		
		
		
		
		
			
		
			
				
	
	
		
			316 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			316 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Swift
		
	
//
 | 
						|
//  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
 | 
						|
//
 | 
						|
 | 
						|
import Foundation
 | 
						|
 | 
						|
@objc
 | 
						|
public protocol ConversationSearchControllerDelegate: UISearchControllerDelegate {
 | 
						|
 | 
						|
    @objc
 | 
						|
    func conversationSearchController(_ conversationSearchController: ConversationSearchController,
 | 
						|
                                      didUpdateSearchResults resultSet: ConversationScreenSearchResultSet?)
 | 
						|
 | 
						|
    @objc
 | 
						|
    func conversationSearchController(_ conversationSearchController: ConversationSearchController,
 | 
						|
                                      didSelectMessageId: String)
 | 
						|
}
 | 
						|
 | 
						|
@objc
 | 
						|
public class ConversationSearchController : NSObject {
 | 
						|
 | 
						|
    @objc
 | 
						|
    public static let kMinimumSearchTextLength: UInt = 2
 | 
						|
 | 
						|
    @objc
 | 
						|
    public let uiSearchController =  UISearchController(searchResultsController: nil)
 | 
						|
 | 
						|
    @objc
 | 
						|
    public weak var delegate: ConversationSearchControllerDelegate?
 | 
						|
 | 
						|
    let thread: TSThread
 | 
						|
 | 
						|
    @objc
 | 
						|
    public let resultsBar: SearchResultsBar = SearchResultsBar()
 | 
						|
 | 
						|
    // MARK: Initializer
 | 
						|
 | 
						|
    @objc
 | 
						|
    required public init(thread: TSThread) {
 | 
						|
        self.thread = thread
 | 
						|
        super.init()
 | 
						|
 | 
						|
        resultsBar.resultsBarDelegate = self
 | 
						|
        uiSearchController.delegate = self
 | 
						|
        uiSearchController.searchResultsUpdater = self
 | 
						|
 | 
						|
        uiSearchController.hidesNavigationBarDuringPresentation = false
 | 
						|
        if #available(iOS 13, *) {
 | 
						|
            // Do nothing
 | 
						|
        } else {
 | 
						|
            uiSearchController.dimsBackgroundDuringPresentation = false
 | 
						|
        }
 | 
						|
        uiSearchController.searchBar.inputAccessoryView = resultsBar
 | 
						|
    }
 | 
						|
 | 
						|
    // MARK: Dependencies
 | 
						|
 | 
						|
    var dbReadConnection: YapDatabaseConnection {
 | 
						|
        return OWSPrimaryStorage.shared().dbReadConnection
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
extension ConversationSearchController : UISearchControllerDelegate {
 | 
						|
    
 | 
						|
    public func didPresentSearchController(_ searchController: UISearchController) {
 | 
						|
        Logger.verbose("")
 | 
						|
        delegate?.didPresentSearchController?(searchController)
 | 
						|
    }
 | 
						|
 | 
						|
    public func didDismissSearchController(_ searchController: UISearchController) {
 | 
						|
        Logger.verbose("")
 | 
						|
        delegate?.didDismissSearchController?(searchController)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
extension ConversationSearchController : UISearchResultsUpdating {
 | 
						|
    
 | 
						|
    var dbSearcher: FullTextSearcher {
 | 
						|
        return FullTextSearcher.shared
 | 
						|
    }
 | 
						|
 | 
						|
    public func updateSearchResults(for searchController: UISearchController) {
 | 
						|
        Logger.verbose("searchBar.text: \( searchController.searchBar.text ?? "<blank>")")
 | 
						|
 | 
						|
        guard let rawSearchText = searchController.searchBar.text?.stripped else {
 | 
						|
            self.resultsBar.updateResults(resultSet: nil)
 | 
						|
            self.delegate?.conversationSearchController(self, didUpdateSearchResults: nil)
 | 
						|
            return
 | 
						|
        }
 | 
						|
        let searchText = FullTextSearchFinder.normalize(text: rawSearchText)
 | 
						|
 | 
						|
        guard searchText.count >= ConversationSearchController.kMinimumSearchTextLength else {
 | 
						|
            self.resultsBar.updateResults(resultSet: nil)
 | 
						|
            self.delegate?.conversationSearchController(self, didUpdateSearchResults: nil)
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        var resultSet: ConversationScreenSearchResultSet?
 | 
						|
        self.dbReadConnection.asyncRead({ [weak self] transaction in
 | 
						|
            guard let self = self else {
 | 
						|
                return
 | 
						|
            }
 | 
						|
            resultSet = self.dbSearcher.searchWithinConversation(thread: self.thread, searchText: searchText, transaction: transaction)
 | 
						|
        }, completionBlock: { [weak self] in
 | 
						|
            guard let self = self else {
 | 
						|
                return
 | 
						|
            }
 | 
						|
            self.resultsBar.updateResults(resultSet: resultSet)
 | 
						|
            self.delegate?.conversationSearchController(self, didUpdateSearchResults: resultSet)
 | 
						|
        })
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
extension ConversationSearchController : SearchResultsBarDelegate {
 | 
						|
    
 | 
						|
    func searchResultsBar(_ searchResultsBar: SearchResultsBar,
 | 
						|
                          setCurrentIndex currentIndex: Int,
 | 
						|
                          resultSet: ConversationScreenSearchResultSet) {
 | 
						|
        guard let searchResult = resultSet.messages[safe: currentIndex] else {
 | 
						|
            owsFailDebug("messageId was unexpectedly nil")
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        BenchEventStart(title: "Conversation Search Nav", eventId: "Conversation Search Nav: \(searchResult.messageId)")
 | 
						|
        self.delegate?.conversationSearchController(self, didSelectMessageId: searchResult.messageId)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
protocol SearchResultsBarDelegate : AnyObject {
 | 
						|
    
 | 
						|
    func searchResultsBar(_ searchResultsBar: SearchResultsBar,
 | 
						|
                          setCurrentIndex currentIndex: Int,
 | 
						|
                          resultSet: ConversationScreenSearchResultSet)
 | 
						|
}
 | 
						|
 | 
						|
public final class SearchResultsBar : UIView {
 | 
						|
    private var resultSet: ConversationScreenSearchResultSet?
 | 
						|
    var currentIndex: Int?
 | 
						|
    weak var resultsBarDelegate: SearchResultsBarDelegate?
 | 
						|
    
 | 
						|
    public override var intrinsicContentSize: CGSize { CGSize.zero }
 | 
						|
    
 | 
						|
    private lazy var label: UILabel = {
 | 
						|
        let result = UILabel()
 | 
						|
        result.text = "Test"
 | 
						|
        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
 | 
						|
    }()
 | 
						|
    
 | 
						|
    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)
 | 
						|
        // Remaining constraints
 | 
						|
        label.center(.horizontal, in: self)
 | 
						|
    }
 | 
						|
    
 | 
						|
    @objc
 | 
						|
    public func handleUpButtonTapped() {
 | 
						|
        Logger.debug("")
 | 
						|
        guard let resultSet = resultSet else {
 | 
						|
            owsFailDebug("resultSet was unexpectedly nil")
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        guard let currentIndex = currentIndex else {
 | 
						|
            owsFailDebug("currentIndex was unexpectedly nil")
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        guard currentIndex + 1 < resultSet.messages.count else {
 | 
						|
            owsFailDebug("showLessRecent button should be disabled")
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        let newIndex = currentIndex + 1
 | 
						|
        self.currentIndex = newIndex
 | 
						|
        updateBarItems()
 | 
						|
        resultsBarDelegate?.searchResultsBar(self, setCurrentIndex: newIndex, resultSet: resultSet)
 | 
						|
    }
 | 
						|
 | 
						|
    @objc
 | 
						|
    public func handleDownButtonTapped() {
 | 
						|
        Logger.debug("")
 | 
						|
        guard let resultSet = resultSet else {
 | 
						|
            owsFailDebug("resultSet was unexpectedly nil")
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        guard let currentIndex = currentIndex else {
 | 
						|
            owsFailDebug("currentIndex was unexpectedly nil")
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        guard currentIndex > 0 else {
 | 
						|
            owsFailDebug("showMoreRecent button should be disabled")
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        let newIndex = currentIndex - 1
 | 
						|
        self.currentIndex = newIndex
 | 
						|
        updateBarItems()
 | 
						|
        resultsBarDelegate?.searchResultsBar(self, setCurrentIndex: newIndex, resultSet: resultSet)
 | 
						|
    }
 | 
						|
 | 
						|
    func updateResults(resultSet: ConversationScreenSearchResultSet?) {
 | 
						|
        if let resultSet = resultSet {
 | 
						|
            if resultSet.messages.count > 0 {
 | 
						|
                currentIndex = min(currentIndex ?? 0, resultSet.messages.count - 1)
 | 
						|
            } else {
 | 
						|
                currentIndex = nil
 | 
						|
            }
 | 
						|
        } else {
 | 
						|
            currentIndex = nil
 | 
						|
        }
 | 
						|
 | 
						|
        self.resultSet = resultSet
 | 
						|
 | 
						|
        updateBarItems()
 | 
						|
        if let currentIndex = currentIndex, let resultSet = resultSet {
 | 
						|
            resultsBarDelegate?.searchResultsBar(self, setCurrentIndex: currentIndex, resultSet: resultSet)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    func updateBarItems() {
 | 
						|
        guard let resultSet = resultSet else {
 | 
						|
            label.text = ""
 | 
						|
            downButton.isEnabled = false
 | 
						|
            upButton.isEnabled = false
 | 
						|
            return
 | 
						|
        }
 | 
						|
 | 
						|
        switch resultSet.messages.count {
 | 
						|
        case 0:
 | 
						|
            label.text = NSLocalizedString("CONVERSATION_SEARCH_NO_RESULTS", comment: "keyboard toolbar label when no messages match the search string")
 | 
						|
        case 1:
 | 
						|
            label.text = NSLocalizedString("CONVERSATION_SEARCH_ONE_RESULT", comment: "keyboard toolbar label when exactly 1 message matches the search string")
 | 
						|
        default:
 | 
						|
            let format = NSLocalizedString("CONVERSATION_SEARCH_RESULTS_FORMAT",
 | 
						|
                                           comment: "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}}")
 | 
						|
 | 
						|
            guard let currentIndex = currentIndex else {
 | 
						|
                owsFailDebug("currentIndex was unexpectedly nil")
 | 
						|
                return
 | 
						|
            }
 | 
						|
            label.text = String(format: format, currentIndex + 1, resultSet.messages.count)
 | 
						|
        }
 | 
						|
 | 
						|
        if let currentIndex = currentIndex {
 | 
						|
            downButton.isEnabled = currentIndex > 0
 | 
						|
            upButton.isEnabled = currentIndex + 1 < resultSet.messages.count
 | 
						|
        } else {
 | 
						|
            downButton.isEnabled = false
 | 
						|
            upButton.isEnabled = false
 | 
						|
        }
 | 
						|
    }
 | 
						|
}
 |