mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
in-conversation search
- use MediaTime for computing benchmarks
This commit is contained in:
parent
6095500f80
commit
71dd4eb151
32 changed files with 697 additions and 70 deletions
|
@ -338,7 +338,7 @@
|
|||
45194F941FD7216000333B2C /* TSUnreadIndicatorInteraction.h in Headers */ = {isa = PBXBuildFile; fileRef = 34C42D641F4734ED0072EC04 /* TSUnreadIndicatorInteraction.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
45194F951FD7216600333B2C /* TSUnreadIndicatorInteraction.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C42D651F4734ED0072EC04 /* TSUnreadIndicatorInteraction.m */; };
|
||||
451A13B11E13DED2000A50FD /* AppNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451A13B01E13DED2000A50FD /* AppNotifications.swift */; };
|
||||
451F8A341FD710C3005CB9DA /* ConversationSearcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451777C71FD61554001225FF /* ConversationSearcher.swift */; };
|
||||
451F8A341FD710C3005CB9DA /* FullTextSearcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451777C71FD61554001225FF /* FullTextSearcher.swift */; };
|
||||
451F8A351FD710DE005CB9DA /* Searcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45360B8C1F9521F800FA666C /* Searcher.swift */; };
|
||||
451F8A3B1FD71297005CB9DA /* UIUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = B97940261832BD2400BD66CB /* UIUtil.m */; };
|
||||
451F8A3C1FD71392005CB9DA /* UIUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = B97940251832BD2400BD66CB /* UIUtil.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
|
@ -490,6 +490,7 @@
|
|||
4CC0B59C20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC0B59B20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift */; };
|
||||
4CC1ECF9211A47CE00CC13BE /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; };
|
||||
4CC1ECFB211A553000CC13BE /* AppUpdateNag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */; };
|
||||
4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC613352227A00400E21A3A /* ConversationSearch.swift */; };
|
||||
4CEB78C92178EBAB00F315D2 /* OWSSessionResetJobRecord.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CEB78C82178EBAB00F315D2 /* OWSSessionResetJobRecord.m */; };
|
||||
4CFE6B6C21F92BA700006701 /* LegacyNotificationsAdaptee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFE6B6B21F92BA700006701 /* LegacyNotificationsAdaptee.swift */; };
|
||||
70377AAB1918450100CAF501 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70377AAA1918450100CAF501 /* MobileCoreServices.framework */; };
|
||||
|
@ -1055,7 +1056,7 @@
|
|||
450DF2081E0DD2C6003D14BE /* UserNotificationsAdaptee.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = UserNotificationsAdaptee.swift; path = UserInterface/Notifications/UserNotificationsAdaptee.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
451166BF1FD86B98000739BA /* AccountManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManager.swift; sourceTree = "<group>"; };
|
||||
451764291DE939FD00EDB8B9 /* ContactCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactCell.swift; sourceTree = "<group>"; };
|
||||
451777C71FD61554001225FF /* ConversationSearcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationSearcher.swift; sourceTree = "<group>"; };
|
||||
451777C71FD61554001225FF /* FullTextSearcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FullTextSearcher.swift; sourceTree = "<group>"; };
|
||||
451A13B01E13DED2000A50FD /* AppNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = AppNotifications.swift; path = UserInterface/Notifications/AppNotifications.swift; sourceTree = "<group>"; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
452037CF1EE84975004E4CDF /* DebugUISessionState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUISessionState.h; sourceTree = "<group>"; };
|
||||
452037D01EE84975004E4CDF /* DebugUISessionState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUISessionState.m; sourceTree = "<group>"; };
|
||||
|
@ -1223,6 +1224,7 @@
|
|||
4CC0B59B20EC5F2E00CF6EE0 /* ConversationConfigurationSyncOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationConfigurationSyncOperation.swift; sourceTree = "<group>"; };
|
||||
4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
|
||||
4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateNag.swift; sourceTree = "<group>"; };
|
||||
4CC613352227A00400E21A3A /* ConversationSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationSearch.swift; sourceTree = "<group>"; };
|
||||
4CEB78C72178EBAB00F315D2 /* OWSSessionResetJobRecord.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OWSSessionResetJobRecord.h; sourceTree = "<group>"; };
|
||||
4CEB78C82178EBAB00F315D2 /* OWSSessionResetJobRecord.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OWSSessionResetJobRecord.m; sourceTree = "<group>"; };
|
||||
4CFB4E9B220BC56D00ECB4DE /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = translations/nb.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
|
@ -1577,7 +1579,7 @@
|
|||
4C948FF62146EB4800349F0D /* BlockListCache.swift */,
|
||||
343D3D991E9283F100165CA4 /* BlockListUIUtils.h */,
|
||||
343D3D9A1E9283F100165CA4 /* BlockListUIUtils.m */,
|
||||
451777C71FD61554001225FF /* ConversationSearcher.swift */,
|
||||
451777C71FD61554001225FF /* FullTextSearcher.swift */,
|
||||
3466087120E550F300AFFE73 /* ConversationStyle.swift */,
|
||||
34480B4D1FD0A7A300BC14EF /* DebugLogger.h */,
|
||||
34480B4E1FD0A7A300BC14EF /* DebugLogger.m */,
|
||||
|
@ -2074,6 +2076,7 @@
|
|||
4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */,
|
||||
34B3F8331E8DF1700035BE1A /* ViewControllers */,
|
||||
76EB052B18170B33006006FC /* Views */,
|
||||
4CC613352227A00400E21A3A /* ConversationSearch.swift */,
|
||||
);
|
||||
name = UserInterface;
|
||||
sourceTree = "<group>";
|
||||
|
@ -3382,7 +3385,7 @@
|
|||
452C7CA72037628B003D51A5 /* Weak.swift in Sources */,
|
||||
34D5872F208E2C4200D2255A /* OWS109OutgoingMessageState.m in Sources */,
|
||||
34AC09F8211B39B100997B47 /* CountryCodeViewController.m in Sources */,
|
||||
451F8A341FD710C3005CB9DA /* ConversationSearcher.swift in Sources */,
|
||||
451F8A341FD710C3005CB9DA /* FullTextSearcher.swift in Sources */,
|
||||
346129FE1FD5F31400532771 /* OWS106EnsureProfileComplete.swift in Sources */,
|
||||
34AC0A10211B39EA00997B47 /* TappableView.swift in Sources */,
|
||||
346129F91FD5F31400532771 /* OWS104CreateRecipientIdentities.m in Sources */,
|
||||
|
@ -3541,6 +3544,7 @@
|
|||
340FC8BC204DAC8D007AEB0F /* FingerprintViewController.m in Sources */,
|
||||
450DF2051E0D74AC003D14BE /* Platform.swift in Sources */,
|
||||
34A4C61E221613D00042EF2E /* OnboardingVerificationViewController.swift in Sources */,
|
||||
4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */,
|
||||
340FC8B2204DAC8D007AEB0F /* AdvancedSettingsTableViewController.m in Sources */,
|
||||
452B999020A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift in Sources */,
|
||||
346129991FD1E4DA00532771 /* SignalApp.m in Sources */,
|
||||
|
|
23
Signal/Images.xcassets/ic_chevron_down.imageset/Contents.json
vendored
Normal file
23
Signal/Images.xcassets/ic_chevron_down.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "chevron-down-24@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "chevron-down-24@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "chevron-down-24@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Signal/Images.xcassets/ic_chevron_down.imageset/chevron-down-24@1x.png
vendored
Normal file
BIN
Signal/Images.xcassets/ic_chevron_down.imageset/chevron-down-24@1x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 234 B |
BIN
Signal/Images.xcassets/ic_chevron_down.imageset/chevron-down-24@2x.png
vendored
Normal file
BIN
Signal/Images.xcassets/ic_chevron_down.imageset/chevron-down-24@2x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 359 B |
BIN
Signal/Images.xcassets/ic_chevron_down.imageset/chevron-down-24@3x.png
vendored
Normal file
BIN
Signal/Images.xcassets/ic_chevron_down.imageset/chevron-down-24@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 521 B |
23
Signal/Images.xcassets/ic_chevron_up.imageset/Contents.json
vendored
Normal file
23
Signal/Images.xcassets/ic_chevron_up.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "chevron-up-24@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "chevron-up-24@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "chevron-up-24@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
BIN
Signal/Images.xcassets/ic_chevron_up.imageset/chevron-up-24@1x.png
vendored
Normal file
BIN
Signal/Images.xcassets/ic_chevron_up.imageset/chevron-up-24@1x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 235 B |
BIN
Signal/Images.xcassets/ic_chevron_up.imageset/chevron-up-24@2x.png
vendored
Normal file
BIN
Signal/Images.xcassets/ic_chevron_up.imageset/chevron-up-24@2x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 351 B |
BIN
Signal/Images.xcassets/ic_chevron_up.imageset/chevron-up-24@3x.png
vendored
Normal file
BIN
Signal/Images.xcassets/ic_chevron_up.imageset/chevron-up-24@3x.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 516 B |
289
Signal/src/ConversationSearch.swift
Normal file
289
Signal/src/ConversationSearch.swift
Normal file
|
@ -0,0 +1,289 @@
|
|||
//
|
||||
// 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
|
||||
|
||||
let resultsBar: SearchResultsBar = SearchResultsBar(frame: .zero)
|
||||
|
||||
// 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
|
||||
uiSearchController.dimsBackgroundDuringPresentation = false
|
||||
uiSearchController.searchBar.inputAccessoryView = resultsBar
|
||||
|
||||
applyTheme()
|
||||
}
|
||||
|
||||
func applyTheme() {
|
||||
OWSSearchBar.applyTheme(to: uiSearchController.searchBar)
|
||||
}
|
||||
|
||||
// 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 searchText = searchController.searchBar.text?.stripped else {
|
||||
self.resultsBar.updateResults(resultSet: nil)
|
||||
self.delegate?.conversationSearchController(self, didUpdateSearchResults: nil)
|
||||
return
|
||||
}
|
||||
BenchManager.startEvent(title: "Conversation Search", eventId: searchText)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
class SearchResultsBar: UIToolbar {
|
||||
|
||||
weak var resultsBarDelegate: SearchResultsBarDelegate?
|
||||
|
||||
var showLessRecentButton: UIBarButtonItem!
|
||||
var showMoreRecentButton: UIBarButtonItem!
|
||||
let labelItem: UIBarButtonItem
|
||||
|
||||
var resultSet: ConversationScreenSearchResultSet?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
||||
labelItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil)
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
let leftExteriorChevronMargin: CGFloat
|
||||
let leftInteriorChevronMargin: CGFloat
|
||||
if CurrentAppContext().isRTL {
|
||||
leftExteriorChevronMargin = 8
|
||||
leftInteriorChevronMargin = 0
|
||||
} else {
|
||||
leftExteriorChevronMargin = 0
|
||||
leftInteriorChevronMargin = 8
|
||||
}
|
||||
|
||||
let upChevron = #imageLiteral(resourceName: "ic_chevron_up").withRenderingMode(.alwaysTemplate)
|
||||
showLessRecentButton = UIBarButtonItem(image: upChevron, style: .plain, target: self, action: #selector(didTapShowLessRecent))
|
||||
showLessRecentButton.imageInsets = UIEdgeInsets(top: 2, left: leftExteriorChevronMargin, bottom: 2, right: leftInteriorChevronMargin)
|
||||
showLessRecentButton.tintColor = UIColor.ows_systemPrimaryButton
|
||||
|
||||
let downChevron = #imageLiteral(resourceName: "ic_chevron_down").withRenderingMode(.alwaysTemplate)
|
||||
showMoreRecentButton = UIBarButtonItem(image: downChevron, style: .plain, target: self, action: #selector(didTapShowMoreRecent))
|
||||
showMoreRecentButton.imageInsets = UIEdgeInsets(top: 2, left: leftInteriorChevronMargin, bottom: 2, right: leftExteriorChevronMargin)
|
||||
showMoreRecentButton.tintColor = UIColor.ows_systemPrimaryButton
|
||||
|
||||
let spacer1 = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
||||
let spacer2 = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
|
||||
|
||||
self.items = [showLessRecentButton, showMoreRecentButton, spacer1, labelItem, spacer2]
|
||||
|
||||
self.isTranslucent = false
|
||||
self.isOpaque = true
|
||||
self.barTintColor = Theme.toolbarBackgroundColor
|
||||
|
||||
self.autoresizingMask = .flexibleHeight
|
||||
self.translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
@objc
|
||||
public func didTapShowLessRecent() {
|
||||
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 didTapShowMoreRecent() {
|
||||
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)
|
||||
}
|
||||
|
||||
var currentIndex: Int?
|
||||
|
||||
// MARK:
|
||||
|
||||
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 {
|
||||
labelItem.title = nil
|
||||
showMoreRecentButton.isEnabled = false
|
||||
showLessRecentButton.isEnabled = false
|
||||
return
|
||||
}
|
||||
|
||||
switch resultSet.messages.count {
|
||||
case 0:
|
||||
labelItem.title = NSLocalizedString("CONVERSATION_SEARCH_NO_RESULTS", comment: "keyboard toolbar label when no messages match the search string")
|
||||
case 1:
|
||||
labelItem.title = 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
|
||||
}
|
||||
labelItem.title = String(format: format, currentIndex + 1, resultSet.messages.count)
|
||||
}
|
||||
|
||||
if let currentIndex = currentIndex {
|
||||
showMoreRecentButton.isEnabled = currentIndex > 0
|
||||
showLessRecentButton.isEnabled = currentIndex + 1 < resultSet.messages.count
|
||||
} else {
|
||||
showMoreRecentButton.isEnabled = false
|
||||
showLessRecentButton.isEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -59,6 +59,8 @@ extern const UIDataDetectorTypes kOWSAllowedDataDetectorTypes;
|
|||
- (void)didTapShowAddToContactUIForContactShare:(ContactShareViewModel *)contactShare
|
||||
NS_SWIFT_NAME(didTapShowAddToContactUI(forContactShare:));
|
||||
|
||||
@property (nonatomic, readonly, nullable) NSString *lastSearchedText;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
|
|
@ -691,6 +691,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
|||
}
|
||||
[self.class loadForTextDisplay:self.bodyTextView
|
||||
text:self.displayableBodyText.displayText
|
||||
searchText:self.delegate.lastSearchedText
|
||||
textColor:self.bodyTextColor
|
||||
font:self.textMessageFont
|
||||
shouldIgnoreEvents:shouldIgnoreEvents];
|
||||
|
@ -698,6 +699,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
|||
|
||||
+ (void)loadForTextDisplay:(OWSMessageTextView *)textView
|
||||
text:(NSString *)text
|
||||
searchText:(nullable NSString *)searchText
|
||||
textColor:(UIColor *)textColor
|
||||
font:(UIFont *)font
|
||||
shouldIgnoreEvents:(BOOL)shouldIgnoreEvents
|
||||
|
@ -713,8 +715,29 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
|||
};
|
||||
textView.shouldIgnoreEvents = shouldIgnoreEvents;
|
||||
|
||||
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc]
|
||||
initWithString:text
|
||||
attributes:@{ NSFontAttributeName : font, NSForegroundColorAttributeName : textColor }];
|
||||
if (searchText.length >= ConversationSearchController.kMinimumSearchTextLength) {
|
||||
NSError *error;
|
||||
NSRegularExpression *regex = [[NSRegularExpression alloc] initWithPattern:searchText
|
||||
options:NSRegularExpressionCaseInsensitive
|
||||
error:&error];
|
||||
OWSAssertDebug(error == nil);
|
||||
for (NSTextCheckingResult *match in
|
||||
[regex matchesInString:text options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(0, text.length)]) {
|
||||
|
||||
OWSAssertDebug(match.range.length >= ConversationSearchController.kMinimumSearchTextLength);
|
||||
[attributedText addAttribute:NSBackgroundColorAttributeName value:UIColor.yellowColor range:match.range];
|
||||
[attributedText addAttribute:NSForegroundColorAttributeName value:UIColor.ows_blackColor range:match.range];
|
||||
}
|
||||
}
|
||||
|
||||
// For perf, set text last. Otherwise changing font/color is more expensive.
|
||||
textView.text = text;
|
||||
|
||||
// We use attributedText even when we're not highlighting searched text to esnure any lingering
|
||||
// attributes are reset.
|
||||
textView.attributedText = attributedText;
|
||||
}
|
||||
|
||||
- (BOOL)shouldShowSenderName
|
||||
|
@ -1259,6 +1282,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
|||
|
||||
[self.bodyTextView removeFromSuperview];
|
||||
self.bodyTextView.text = nil;
|
||||
self.bodyTextView.attributedText = nil;
|
||||
self.bodyTextView.hidden = YES;
|
||||
|
||||
self.bubbleView.bubbleColor = nil;
|
||||
|
|
|
@ -211,7 +211,7 @@ public class ConversationMessageMapping: NSObject {
|
|||
// Tries to ensure that the load window includes a given item.
|
||||
// On success, returns the index path of that item.
|
||||
// On failure, returns nil.
|
||||
@objc
|
||||
@objc(ensureLoadWindowContainsUniqueId:transaction:)
|
||||
public func ensureLoadWindowContains(uniqueId: String,
|
||||
transaction: YapDatabaseReadTransaction) -> IndexPath? {
|
||||
if let oldIndex = loadedUniqueIds().firstIndex(of: uniqueId) {
|
||||
|
|
|
@ -141,6 +141,7 @@ typedef enum : NSUInteger {
|
|||
ConversationViewLayoutDelegate,
|
||||
ConversationViewCellDelegate,
|
||||
ConversationInputTextViewDelegate,
|
||||
ConversationSearchControllerDelegate,
|
||||
LongTextViewDelegate,
|
||||
MessageActionsDelegate,
|
||||
MessageDetailViewDelegate,
|
||||
|
@ -195,6 +196,8 @@ typedef enum : NSUInteger {
|
|||
@property (nonatomic) BOOL userHasScrolled;
|
||||
@property (nonatomic, nullable) NSDate *lastMessageSentDate;
|
||||
|
||||
@property (nonatomic, nullable) UIBarButtonItem *customBackButton;
|
||||
|
||||
@property (nonatomic) BOOL showLoadMoreHeader;
|
||||
@property (nonatomic) UILabel *loadMoreHeader;
|
||||
@property (nonatomic) uint64_t lastVisibleSortId;
|
||||
|
@ -227,6 +230,10 @@ typedef enum : NSUInteger {
|
|||
@property (nonatomic) ScrollContinuity scrollContinuity;
|
||||
@property (nonatomic, nullable) NSTimer *autoLoadMoreTimer;
|
||||
|
||||
@property (nonatomic, readonly) ConversationSearchController *searchController;
|
||||
@property (nonatomic, nullable) NSString *lastSearchedText;
|
||||
@property (nonatomic) BOOL isShowingSearchUI;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
@ -509,6 +516,9 @@ typedef enum : NSUInteger {
|
|||
|
||||
[self updateConversationSnapshot];
|
||||
|
||||
_searchController = [[ConversationSearchController alloc] initWithThread:thread];
|
||||
_searchController.delegate = self;
|
||||
|
||||
[self updateShouldObserveVMUpdates];
|
||||
|
||||
self.reloadTimer = [NSTimer weakScheduledTimerWithTimeInterval:1.f
|
||||
|
@ -1391,6 +1401,7 @@ typedef enum : NSUInteger {
|
|||
- (void)createBackButton
|
||||
{
|
||||
UIBarButtonItem *backItem = [self createOWSBackButton];
|
||||
self.customBackButton = backItem;
|
||||
if (backItem.customView) {
|
||||
// This method gets called multiple times, so it's important we re-layout the unread badge
|
||||
// with respect to the new backItem.
|
||||
|
@ -1425,11 +1436,23 @@ typedef enum : NSUInteger {
|
|||
|
||||
- (void)updateBarButtonItems
|
||||
{
|
||||
self.navigationItem.hidesBackButton = NO;
|
||||
if (self.customBackButton) {
|
||||
self.navigationItem.leftBarButtonItem = self.customBackButton;
|
||||
}
|
||||
|
||||
if (self.userLeftGroup) {
|
||||
self.navigationItem.rightBarButtonItems = @[];
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.isShowingSearchUI) {
|
||||
self.navigationItem.rightBarButtonItems = @[];
|
||||
self.navigationItem.leftBarButtonItem = nil;
|
||||
self.navigationItem.hidesBackButton = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
const CGFloat kBarButtonSize = 44;
|
||||
NSMutableArray<UIBarButtonItem *> *barButtons = [NSMutableArray new];
|
||||
if ([self canCall]) {
|
||||
|
@ -3944,19 +3967,107 @@ typedef enum : NSUInteger {
|
|||
[self updateGroupModelTo:groupModel successCompletion:nil];
|
||||
}
|
||||
|
||||
- (void)popAllConversationSettingsViews
|
||||
- (void)popAllConversationSettingsViewsWithCompletion:(void (^_Nullable)(void))completionBlock
|
||||
{
|
||||
if (self.presentedViewController) {
|
||||
[self.presentedViewController
|
||||
dismissViewControllerAnimated:YES
|
||||
completion:^{
|
||||
[self.navigationController popToViewController:self animated:YES];
|
||||
}];
|
||||
[self.presentedViewController dismissViewControllerAnimated:YES
|
||||
completion:^{
|
||||
[self.navigationController
|
||||
popToViewController:self
|
||||
animated:YES
|
||||
completion:completionBlock];
|
||||
}];
|
||||
} else {
|
||||
[self.navigationController popToViewController:self animated:YES];
|
||||
[self.navigationController popToViewController:self animated:YES completion:completionBlock];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Conversation Search
|
||||
|
||||
- (void)conversationSettingsDidRequestConversationSearch:(OWSConversationSettingsViewController *)conversationSettingsViewController
|
||||
{
|
||||
[self showSearchUI];
|
||||
[self popAllConversationSettingsViewsWithCompletion:^{
|
||||
// This delay is unfortunate, but without it, self.searchController.uiSearchController.searchBar
|
||||
// isn't yet ready to become first responder. Presumably we're still mid transition.
|
||||
// A hardcorded constant like this isn't great because it's either too slow, making our users
|
||||
// wait, or too fast, and fails to wait long enough to be ready to become first responder.
|
||||
// Luckily in this case the stakes aren't catastrophic. In the case that we're too aggressive
|
||||
// the user will just have to manually tap into the search field before typing.
|
||||
|
||||
// Leaving this assert in as proof that we're not ready to become first responder yet.
|
||||
// If this assert fails, *great* maybe we can get rid of this delay.
|
||||
OWSAssertDebug(![self.searchController.uiSearchController.searchBar canBecomeFirstResponder]);
|
||||
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
||||
[self.searchController.uiSearchController.searchBar becomeFirstResponder];
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)showSearchUI
|
||||
{
|
||||
self.isShowingSearchUI = YES;
|
||||
self.navigationItem.titleView = self.searchController.uiSearchController.searchBar;
|
||||
[self updateBarButtonItems];
|
||||
}
|
||||
|
||||
- (void)hideSearchUI
|
||||
{
|
||||
self.isShowingSearchUI = NO;
|
||||
|
||||
self.navigationItem.titleView = self.headerView;
|
||||
[self updateBarButtonItems];
|
||||
|
||||
// restore first responder to VC
|
||||
[self becomeFirstResponder];
|
||||
}
|
||||
|
||||
#pragma mark ConversationSearchControllerDelegate
|
||||
|
||||
- (void)didDismissSearchController:(UISearchController *)searchController
|
||||
{
|
||||
OWSLogVerbose(@"");
|
||||
OWSAssertIsOnMainThread();
|
||||
[self hideSearchUI];
|
||||
}
|
||||
|
||||
- (void)conversationSearchController:(ConversationSearchController *)conversationSearchController
|
||||
didUpdateSearchResults:(nullable ConversationScreenSearchResultSet *)conversationScreenSearchResultSet
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogInfo(@"conversationScreenSearchResultSet: %@", conversationScreenSearchResultSet.debugDescription);
|
||||
self.lastSearchedText = conversationScreenSearchResultSet.searchText;
|
||||
[UIView performWithoutAnimation:^{
|
||||
[self.collectionView reloadItemsAtIndexPaths:self.collectionView.indexPathsForVisibleItems];
|
||||
}];
|
||||
if (conversationScreenSearchResultSet) {
|
||||
[BenchManager completeEventWithEventId:self.lastSearchedText];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)conversationSearchController:(ConversationSearchController *)conversationSearchController
|
||||
didSelectMessageId:(NSString *)messageId
|
||||
{
|
||||
OWSLogDebug(@"messageId: %@", messageId);
|
||||
[self scrollToInteractionId:messageId];
|
||||
[BenchManager completeEventWithEventId:[NSString stringWithFormat:@"Conversation Search Nav: %@", messageId]];
|
||||
}
|
||||
|
||||
- (void)scrollToInteractionId:(NSString *)interactionId
|
||||
{
|
||||
NSIndexPath *_Nullable indexPath = [self.conversationViewModel ensureLoadWindowContainsInteractionId:interactionId];
|
||||
if (!indexPath) {
|
||||
OWSFailDebug(@"unable to find indexPath");
|
||||
return;
|
||||
}
|
||||
|
||||
[self.collectionView scrollToItemAtIndexPath:indexPath
|
||||
atScrollPosition:UICollectionViewScrollPositionCenteredVertically
|
||||
animated:YES];
|
||||
}
|
||||
|
||||
#pragma mark - ConversationViewLayoutDelegate
|
||||
|
||||
- (NSArray<id<ConversationViewLayoutItem>> *)layoutItems
|
||||
|
|
|
@ -109,6 +109,7 @@ typedef NS_ENUM(NSUInteger, ConversationUpdateItemType) {
|
|||
- (BOOL)canLoadMoreItems;
|
||||
|
||||
- (nullable NSIndexPath *)ensureLoadWindowContainsQuotedReply:(OWSQuotedReplyModel *)quotedReply;
|
||||
- (nullable NSIndexPath *)ensureLoadWindowContainsInteractionId:(NSString *)interactionId;
|
||||
|
||||
- (void)appendUnsavedOutgoingTextMessage:(TSOutgoingMessage *)outgoingMessage;
|
||||
|
||||
|
|
|
@ -1595,8 +1595,32 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
|||
return;
|
||||
}
|
||||
|
||||
indexPath = [self.messageMapping ensureLoadWindowContainsWithUniqueId:quotedInteraction.uniqueId
|
||||
transaction:transaction];
|
||||
indexPath =
|
||||
[self.messageMapping ensureLoadWindowContainsUniqueId:quotedInteraction.uniqueId transaction:transaction];
|
||||
}];
|
||||
|
||||
self.collapseCutoffDate = [NSDate new];
|
||||
|
||||
[self ensureDynamicInteractionsAndUpdateIfNecessary:NO];
|
||||
|
||||
if (![self reloadViewItems]) {
|
||||
OWSFailDebug(@"failed to reload view items in resetMapping.");
|
||||
}
|
||||
|
||||
[self.delegate conversationViewModelDidUpdate:ConversationUpdate.reloadUpdate];
|
||||
[self.delegate conversationViewModelRangeDidChange];
|
||||
|
||||
return indexPath;
|
||||
}
|
||||
|
||||
- (nullable NSIndexPath *)ensureLoadWindowContainsInteractionId:(NSString *)interactionId
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(interactionId);
|
||||
|
||||
__block NSIndexPath *_Nullable indexPath = nil;
|
||||
[self.uiDatabaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
indexPath = [self.messageMapping ensureLoadWindowContainsUniqueId:interactionId transaction:transaction];
|
||||
}];
|
||||
|
||||
self.collapseCutoffDate = [NSDate new];
|
||||
|
|
|
@ -37,8 +37,8 @@ class ConversationSearchViewController: UITableViewController, BlockListCacheDel
|
|||
return OWSPrimaryStorage.shared().uiDatabaseConnection
|
||||
}
|
||||
|
||||
var searcher: ConversationSearcher {
|
||||
return ConversationSearcher.shared
|
||||
var searcher: FullTextSearcher {
|
||||
return FullTextSearcher.shared
|
||||
}
|
||||
|
||||
private var contactsManager: OWSContactsManager {
|
||||
|
|
|
@ -763,6 +763,10 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
|
|||
UIPasteboard.general.string = messageTimestamp
|
||||
}
|
||||
|
||||
var lastSearchedText: String? {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MediaGalleryDataSourceDelegate
|
||||
|
||||
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, willDelete items: [MediaGalleryItem], initiatedBy: AnyObject) {
|
||||
|
|
|
@ -45,7 +45,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
MFMessageComposeViewControllerDelegate>
|
||||
|
||||
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
|
||||
@property (nonatomic, readonly) ConversationSearcher *conversationSearcher;
|
||||
@property (nonatomic, readonly) FullTextSearcher *fullTextSearcher;
|
||||
|
||||
@property (nonatomic, readonly) UIView *noSignalContactsView;
|
||||
|
||||
|
@ -75,9 +75,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
#pragma mark - Dependencies
|
||||
|
||||
- (ConversationSearcher *)conversationSearcher
|
||||
- (FullTextSearcher *)fullTextSearcher
|
||||
{
|
||||
return ConversationSearcher.shared;
|
||||
return FullTextSearcher.shared;
|
||||
}
|
||||
|
||||
- (YapDatabaseConnection *)uiDatabaseConnection
|
||||
|
@ -903,9 +903,9 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
[self.uiDatabaseConnection
|
||||
asyncReadWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
|
||||
self.searchResults = [self.conversationSearcher searchForComposeScreenWithSearchText:searchText
|
||||
transaction:transaction
|
||||
contactsManager:self.contactsManager];
|
||||
self.searchResults = [self.fullTextSearcher searchForComposeScreenWithSearchText:searchText
|
||||
transaction:transaction
|
||||
contactsManager:self.contactsManager];
|
||||
}
|
||||
completionBlock:^{
|
||||
__typeof(self) strongSelf = weakSelf;
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSConversationSettingsViewDelegate.h"
|
||||
#import <SignalMessaging/OWSViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NewGroupViewController : OWSViewController
|
||||
|
||||
@property (nonatomic, weak) id<OWSConversationSettingsViewDelegate> delegate;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -321,13 +321,6 @@ const CGFloat kIconViewLength = 24;
|
|||
mainSection.customHeaderView = [self mainSectionHeader];
|
||||
mainSection.customHeaderHeight = @(100.f);
|
||||
|
||||
[mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{
|
||||
return [weakSelf disclosureCellWithName:MediaStrings.allMedia iconName:@"actionsheet_camera_roll_black"];
|
||||
}
|
||||
actionBlock:^{
|
||||
[weakSelf showMediaGallery];
|
||||
}]];
|
||||
|
||||
if ([self.thread isKindOfClass:[TSContactThread class]] && self.contactsManager.supportsContactEditing
|
||||
&& !self.hasExistingContact) {
|
||||
[mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{
|
||||
|
@ -354,6 +347,28 @@ const CGFloat kIconViewLength = 24;
|
|||
}]];
|
||||
}
|
||||
|
||||
[mainSection addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
return [weakSelf disclosureCellWithName:MediaStrings.allMedia
|
||||
iconName:@"actionsheet_camera_roll_black"];
|
||||
}
|
||||
actionBlock:^{
|
||||
[weakSelf showMediaGallery];
|
||||
}]];
|
||||
|
||||
// TODO icon
|
||||
[mainSection addItem:[OWSTableItem
|
||||
itemWithCustomCellBlock:^{
|
||||
NSString *title = NSLocalizedString(@"CONVERSATION_SETTINGS_SEARCH",
|
||||
@"Table cell label in conversation settings which returns the user to the "
|
||||
@"conversation with 'search mode' activated");
|
||||
return
|
||||
[weakSelf disclosureCellWithName:title iconName:@"actionsheet_camera_roll_black"];
|
||||
}
|
||||
actionBlock:^{
|
||||
[weakSelf tappedConversationSearch];
|
||||
}]];
|
||||
|
||||
if (!isNoteToSelf && !self.isGroupThread && self.thread.hasSafetyNumbers) {
|
||||
[mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{
|
||||
return [weakSelf
|
||||
|
@ -1331,6 +1346,11 @@ const CGFloat kIconViewLength = 24;
|
|||
[mediaGallery pushTileViewFromNavController:(OWSNavigationController *)self.navigationController];
|
||||
}
|
||||
|
||||
- (void)tappedConversationSearch
|
||||
{
|
||||
[self.conversationSettingsViewDelegate conversationSettingsDidRequestConversationSearch:self];
|
||||
}
|
||||
|
||||
#pragma mark - Notifications
|
||||
|
||||
- (void)identityStateDidChange:(NSNotification *)notification
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class OWSConversationSettingsViewController;
|
||||
@class TSGroupModel;
|
||||
|
||||
@protocol OWSConversationSettingsViewDelegate <NSObject>
|
||||
|
||||
- (void)conversationColorWasUpdated;
|
||||
- (void)groupWasUpdated:(TSGroupModel *)groupModel;
|
||||
- (void)conversationSettingsDidRequestConversationSearch:(OWSConversationSettingsViewController *)conversationSettingsViewController;
|
||||
|
||||
- (void)popAllConversationSettingsViews;
|
||||
- (void)popAllConversationSettingsViewsWithCompletion:(void (^_Nullable)(void))completionBlock;
|
||||
|
||||
@end
|
||||
|
||||
|
|
|
@ -431,7 +431,8 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
[self updateGroup];
|
||||
|
||||
[self.conversationSettingsViewDelegate popAllConversationSettingsViews];
|
||||
[self.conversationSettingsViewDelegate
|
||||
popAllConversationSettingsViewsWithCompletion:nil];
|
||||
}]];
|
||||
[controller addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"ALERT_DONT_SAVE",
|
||||
@"The label for the 'don't save' button in action sheets.")
|
||||
|
@ -448,7 +449,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
[self updateGroup];
|
||||
|
||||
[self.conversationSettingsViewDelegate popAllConversationSettingsViews];
|
||||
[self.conversationSettingsViewDelegate popAllConversationSettingsViewsWithCompletion:nil];
|
||||
}
|
||||
|
||||
- (void)groupNameDidChange:(id)sender
|
||||
|
|
|
@ -8,7 +8,7 @@ import XCTest
|
|||
|
||||
// TODO: We might be able to merge this with OWSFakeContactsManager.
|
||||
@objc
|
||||
class ConversationSearcherContactsManager: NSObject, ContactsManagerProtocol {
|
||||
class FullTextSearcherContactsManager: NSObject, ContactsManagerProtocol {
|
||||
func displayName(forPhoneIdentifier recipientId: String?, transaction: YapDatabaseReadTransaction) -> String {
|
||||
return self.displayName(forPhoneIdentifier: recipientId)
|
||||
}
|
||||
|
@ -57,11 +57,11 @@ class ConversationSearcherContactsManager: NSObject, ContactsManagerProtocol {
|
|||
private let bobRecipientId = "+49030183000"
|
||||
private let aliceRecipientId = "+12345678900"
|
||||
|
||||
class ConversationSearcherTest: SignalBaseTest {
|
||||
class FullTextSearcherTest: SignalBaseTest {
|
||||
|
||||
// MARK: - Dependencies
|
||||
var searcher: ConversationSearcher {
|
||||
return ConversationSearcher.shared
|
||||
var searcher: FullTextSearcher {
|
||||
return FullTextSearcher.shared
|
||||
}
|
||||
|
||||
var dbConnection: YapDatabaseConnection {
|
||||
|
@ -80,7 +80,7 @@ class ConversationSearcherTest: SignalBaseTest {
|
|||
FullTextSearchFinder.ensureDatabaseExtensionRegistered(storage: OWSPrimaryStorage.shared())
|
||||
|
||||
// Replace this singleton.
|
||||
SSKEnvironment.shared.contactsManager = ConversationSearcherContactsManager()
|
||||
SSKEnvironment.shared.contactsManager = FullTextSearcherContactsManager()
|
||||
|
||||
self.dbConnection.readWrite { transaction in
|
||||
let bookModel = TSGroupModel(title: "Book Club", memberIds: [aliceRecipientId, bobRecipientId], image: nil, groupId: Randomness.generateRandomBytes(kGroupIdLength))
|
||||
|
|
|
@ -569,6 +569,15 @@
|
|||
/* Title for the 'conversation delete confirmation' alert. */
|
||||
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Delete Conversation?";
|
||||
|
||||
/* keyboard toolbar label when no messages match the search string */
|
||||
"CONVERSATION_SEARCH_NO_RESULTS" = "No matches";
|
||||
|
||||
/* keyboard toolbar label when exactly 1 message matches the search string */
|
||||
"CONVERSATION_SEARCH_ONE_RESULT" = "1 match";
|
||||
|
||||
/* 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}} */
|
||||
"CONVERSATION_SEARCH_RESULTS_FORMAT" = "%d of %d matches";
|
||||
|
||||
/* title for conversation settings screen */
|
||||
"CONVERSATION_SETTINGS" = "Conversation Settings";
|
||||
|
||||
|
@ -620,6 +629,9 @@
|
|||
/* Label for 'new contact' button in conversation settings view. */
|
||||
"CONVERSATION_SETTINGS_NEW_CONTACT" = "Create New Contact";
|
||||
|
||||
/* Table cell label in conversation settings which returns the user to the conversation with 'search mode' activated */
|
||||
"CONVERSATION_SETTINGS_SEARCH" = "Search Conversation";
|
||||
|
||||
/* Label for button that opens conversation settings. */
|
||||
"CONVERSATION_SETTINGS_TAP_TO_CHANGE" = "Tap to Change";
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
NewNonContactConversationViewControllerDelegate>
|
||||
|
||||
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
|
||||
@property (nonatomic, readonly) ConversationSearcher *conversationSearcher;
|
||||
@property (nonatomic, readonly) FullTextSearcher *fullTextSearcher;
|
||||
@property (nonatomic, readonly) ThreadViewHelper *threadViewHelper;
|
||||
@property (nonatomic, readonly) YapDatabaseConnection *uiDatabaseConnection;
|
||||
|
||||
|
@ -59,7 +59,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
self.view.backgroundColor = Theme.backgroundColor;
|
||||
|
||||
_contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self];
|
||||
_conversationSearcher = ConversationSearcher.shared;
|
||||
_fullTextSearcher = FullTextSearcher.shared;
|
||||
_threadViewHelper = [ThreadViewHelper new];
|
||||
_threadViewHelper.delegate = self;
|
||||
|
||||
|
@ -341,7 +341,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
{
|
||||
NSString *searchTerm = [[self.searchBar text] ows_stripped];
|
||||
|
||||
return [self.conversationSearcher filterThreads:self.threadViewHelper.threads withSearchText:searchTerm];
|
||||
return [self.fullTextSearcher filterThreads:self.threadViewHelper.threads withSearchText:searchTerm];
|
||||
}
|
||||
|
||||
- (NSArray<SignalAccount *> *)filteredSignalAccountsWithSearchText
|
||||
|
|
|
@ -32,7 +32,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
@property (nonatomic) BOOL shouldNotifyDelegateOfUpdatedContacts;
|
||||
@property (nonatomic) BOOL hasUpdatedContactsAtLeastOnce;
|
||||
@property (nonatomic) OWSProfileManager *profileManager;
|
||||
@property (nonatomic, readonly) ConversationSearcher *conversationSearcher;
|
||||
@property (nonatomic, readonly) FullTextSearcher *fullTextSearcher;
|
||||
|
||||
@end
|
||||
|
||||
|
@ -54,7 +54,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
_blockListCache = [OWSBlockListCache new];
|
||||
[_blockListCache startObservingAndSyncStateWithDelegate:self];
|
||||
|
||||
_conversationSearcher = ConversationSearcher.shared;
|
||||
_fullTextSearcher = FullTextSearcher.shared;
|
||||
|
||||
_contactsManager = Environment.shared.contactsManager;
|
||||
_profileManager = [OWSProfileManager sharedManager];
|
||||
|
@ -210,8 +210,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
NSMutableArray<SignalAccount *> *signalAccountsToSearch = [self.signalAccounts mutableCopy];
|
||||
SignalAccount *selfAccount = [[SignalAccount alloc] initWithRecipientId:self.localNumber];
|
||||
[signalAccountsToSearch addObject:selfAccount];
|
||||
return [self.conversationSearcher filterSignalAccounts:signalAccountsToSearch
|
||||
withSearchText:searchText];
|
||||
return [self.fullTextSearcher filterSignalAccounts:signalAccountsToSearch withSearchText:searchText];
|
||||
}
|
||||
|
||||
- (BOOL)doesContact:(Contact *)contact matchSearchTerm:(NSString *)searchTerm
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSSearchBar : UISearchBar
|
||||
|
||||
+ (void)applyThemeToSearchBar:(UISearchBar *)searchBar;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSSearchBar.h"
|
||||
|
@ -54,36 +54,41 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
}
|
||||
|
||||
- (void)ows_applyTheme
|
||||
{
|
||||
[self.class applyThemeToSearchBar:self];
|
||||
}
|
||||
|
||||
+ (void)applyThemeToSearchBar:(UISearchBar *)searchBar
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
UIColor *foregroundColor = Theme.placeholderColor;
|
||||
self.barTintColor = Theme.backgroundColor;
|
||||
self.barStyle = Theme.barStyle;
|
||||
searchBar.barTintColor = Theme.backgroundColor;
|
||||
searchBar.barStyle = Theme.barStyle;
|
||||
|
||||
// Hide searchBar border.
|
||||
// Alternatively we could hide the border by using `UISearchBarStyleMinimal`, but that causes an issue when toggling
|
||||
// from light -> dark -> light theme wherein the textField background color appears darker than it should
|
||||
// (regardless of our re-setting textfield.backgroundColor below).
|
||||
self.backgroundImage = [UIImage new];
|
||||
searchBar.backgroundImage = [UIImage new];
|
||||
|
||||
if (Theme.isDarkThemeEnabled) {
|
||||
UIImage *clearImage = [UIImage imageNamed:@"searchbar_clear"];
|
||||
[self setImage:[clearImage asTintedImageWithColor:foregroundColor]
|
||||
[searchBar setImage:[clearImage asTintedImageWithColor:foregroundColor]
|
||||
forSearchBarIcon:UISearchBarIconClear
|
||||
state:UIControlStateNormal];
|
||||
|
||||
UIImage *searchImage = [UIImage imageNamed:@"searchbar_search"];
|
||||
[self setImage:[searchImage asTintedImageWithColor:foregroundColor]
|
||||
[searchBar setImage:[searchImage asTintedImageWithColor:foregroundColor]
|
||||
forSearchBarIcon:UISearchBarIconSearch
|
||||
state:UIControlStateNormal];
|
||||
} else {
|
||||
[self setImage:nil forSearchBarIcon:UISearchBarIconClear state:UIControlStateNormal];
|
||||
[searchBar setImage:nil forSearchBarIcon:UISearchBarIconClear state:UIControlStateNormal];
|
||||
|
||||
[self setImage:nil forSearchBarIcon:UISearchBarIconSearch state:UIControlStateNormal];
|
||||
[searchBar setImage:nil forSearchBarIcon:UISearchBarIconSearch state:UIControlStateNormal];
|
||||
}
|
||||
|
||||
[self traverseViewHierarchyWithVisitor:^(UIView *view) {
|
||||
[searchBar traverseViewHierarchyWithVisitor:^(UIView *view) {
|
||||
if ([view isKindOfClass:[UITextField class]]) {
|
||||
UITextField *textField = (UITextField *)view;
|
||||
textField.backgroundColor = Theme.searchFieldBackgroundColor;
|
||||
|
|
|
@ -16,7 +16,7 @@ public extension UIEdgeInsets {
|
|||
@objc
|
||||
public extension UINavigationController {
|
||||
@objc
|
||||
public func pushViewController(viewController: UIViewController,
|
||||
public func pushViewController(_ viewController: UIViewController,
|
||||
animated: Bool,
|
||||
completion: (() -> Void)?) {
|
||||
CATransaction.begin()
|
||||
|
@ -35,7 +35,7 @@ public extension UINavigationController {
|
|||
}
|
||||
|
||||
@objc
|
||||
public func popToViewController(viewController: UIViewController,
|
||||
public func popToViewController(_ viewController: UIViewController,
|
||||
animated: Bool,
|
||||
completion: (() -> Void)?) {
|
||||
CATransaction.begin()
|
||||
|
|
|
@ -17,10 +17,10 @@ import Foundation
|
|||
/// }
|
||||
/// }
|
||||
public func BenchAsync(title: String, block: (@escaping () -> Void) -> Void) {
|
||||
let startTime = CFAbsoluteTimeGetCurrent()
|
||||
let startTime = CACurrentMediaTime()
|
||||
|
||||
block {
|
||||
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
|
||||
let timeElapsed = CACurrentMediaTime() - startTime
|
||||
let formattedTime = String(format: "%0.2fms", timeElapsed * 1000)
|
||||
Logger.debug("[Bench] title: \(title), duration: \(formattedTime)")
|
||||
}
|
||||
|
@ -52,19 +52,20 @@ public func Bench(title: String, block: () throws -> Void) throws {
|
|||
/// crosses multiple classes, you can use the BenchEvent tools
|
||||
///
|
||||
/// // in one class
|
||||
/// let message = getMessage()
|
||||
/// BenchEventStart(title: "message sending", eventId: message.id)
|
||||
/// beginTheWork()
|
||||
///
|
||||
/// ...
|
||||
/// ...
|
||||
///
|
||||
/// // in another class
|
||||
/// BenchEventComplete(title: "message sending", eventId: message.id)
|
||||
/// doTheLastThing()
|
||||
/// BenchEventComplete(eventId: message.id)
|
||||
///
|
||||
/// Or in objc
|
||||
///
|
||||
/// [BenchManager startEventWithTitle:"message sending" eventId:message.id]
|
||||
/// ...
|
||||
/// [BenchManager startEventWithTitle:"message sending" eventId:message.id]
|
||||
/// [BenchManager completeEventWithEventId:eventId:message.id]
|
||||
public func BenchEventStart(title: String, eventId: BenchmarkEventId) {
|
||||
BenchAsync(title: title) { finish in
|
||||
runningEvents[eventId] = Event(title: title, eventId: eventId, completion: finish)
|
||||
|
|
|
@ -162,7 +162,57 @@ public class ComposeScreenSearchResultSet: NSObject {
|
|||
}
|
||||
|
||||
@objc
|
||||
public class ConversationSearcher: NSObject {
|
||||
public class MessageSearchResult: NSObject, Comparable {
|
||||
|
||||
public let messageId: String
|
||||
public let sortId: UInt64
|
||||
|
||||
init(messageId: String, sortId: UInt64) {
|
||||
self.messageId = messageId
|
||||
self.sortId = sortId
|
||||
}
|
||||
|
||||
// MARK: - Comparable
|
||||
|
||||
public static func < (lhs: MessageSearchResult, rhs: MessageSearchResult) -> Bool {
|
||||
return lhs.sortId < rhs.sortId
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public class ConversationScreenSearchResultSet: NSObject {
|
||||
|
||||
@objc
|
||||
public let searchText: String
|
||||
|
||||
@objc
|
||||
public let messages: [MessageSearchResult]
|
||||
|
||||
@objc
|
||||
public lazy var messageSortIds: [UInt64] = {
|
||||
return messages.map { $0.sortId }
|
||||
}()
|
||||
|
||||
// MARK: Static members
|
||||
|
||||
public static let empty: ConversationScreenSearchResultSet = ConversationScreenSearchResultSet(searchText: "", messages: [])
|
||||
|
||||
// MARK: Init
|
||||
|
||||
public init(searchText: String, messages: [MessageSearchResult]) {
|
||||
self.searchText = searchText
|
||||
self.messages = messages
|
||||
}
|
||||
|
||||
// MARK: - CustomDebugStringConvertible
|
||||
|
||||
override public var debugDescription: String {
|
||||
return "ConversationScreenSearchResultSet(searchText: \(searchText), messages: [\(messages.count) matches])"
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public class FullTextSearcher: NSObject {
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
|
@ -175,7 +225,7 @@ public class ConversationSearcher: NSObject {
|
|||
private let finder: FullTextSearchFinder
|
||||
|
||||
@objc
|
||||
public static let shared: ConversationSearcher = ConversationSearcher()
|
||||
public static let shared: FullTextSearcher = FullTextSearcher()
|
||||
override private init() {
|
||||
finder = FullTextSearchFinder()
|
||||
super.init()
|
||||
|
@ -279,6 +329,39 @@ public class ConversationSearcher: NSObject {
|
|||
return HomeScreenSearchResultSet(searchText: searchText, conversations: conversations, contacts: otherContacts, messages: messages)
|
||||
}
|
||||
|
||||
public func searchWithinConversation(thread: TSThread,
|
||||
searchText: String,
|
||||
transaction: YapDatabaseReadTransaction) -> ConversationScreenSearchResultSet {
|
||||
|
||||
var messages: [MessageSearchResult] = []
|
||||
|
||||
guard let threadId = thread.uniqueId else {
|
||||
owsFailDebug("threadId was unexpectedly nil")
|
||||
return ConversationScreenSearchResultSet.empty
|
||||
}
|
||||
|
||||
self.finder.enumerateObjects(searchText: searchText, transaction: transaction) { (match: Any, snippet: String?) in
|
||||
if let message = match as? TSMessage {
|
||||
guard message.uniqueThreadId == threadId else {
|
||||
return
|
||||
}
|
||||
|
||||
guard let messageId = message.uniqueId else {
|
||||
owsFailDebug("messageId was unexpectedly nil")
|
||||
return
|
||||
}
|
||||
|
||||
let searchResult = MessageSearchResult(messageId: messageId, sortId: message.sortId)
|
||||
messages.append(searchResult)
|
||||
}
|
||||
}
|
||||
|
||||
// We want most recent first
|
||||
messages.sort(by: >)
|
||||
|
||||
return ConversationScreenSearchResultSet(searchText: searchText, messages: messages)
|
||||
}
|
||||
|
||||
@objc(filterThreads:withSearchText:)
|
||||
public func filterThreads(_ threads: [TSThread], searchText: String) -> [TSThread] {
|
||||
guard searchText.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 else {
|
Loading…
Reference in a new issue