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
|
@ -338,7 +338,7 @@
|
||||||
45194F941FD7216000333B2C /* TSUnreadIndicatorInteraction.h in Headers */ = {isa = PBXBuildFile; fileRef = 34C42D641F4734ED0072EC04 /* TSUnreadIndicatorInteraction.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
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 */; };
|
45194F951FD7216600333B2C /* TSUnreadIndicatorInteraction.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C42D651F4734ED0072EC04 /* TSUnreadIndicatorInteraction.m */; };
|
||||||
451A13B11E13DED2000A50FD /* AppNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451A13B01E13DED2000A50FD /* AppNotifications.swift */; };
|
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 */; };
|
451F8A351FD710DE005CB9DA /* Searcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45360B8C1F9521F800FA666C /* Searcher.swift */; };
|
||||||
451F8A3B1FD71297005CB9DA /* UIUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = B97940261832BD2400BD66CB /* UIUtil.m */; };
|
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, ); }; };
|
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 */; };
|
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, ); }; };
|
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 */; };
|
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 */; };
|
4CEB78C92178EBAB00F315D2 /* OWSSessionResetJobRecord.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CEB78C82178EBAB00F315D2 /* OWSSessionResetJobRecord.m */; };
|
||||||
4CFE6B6C21F92BA700006701 /* LegacyNotificationsAdaptee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFE6B6B21F92BA700006701 /* LegacyNotificationsAdaptee.swift */; };
|
4CFE6B6C21F92BA700006701 /* LegacyNotificationsAdaptee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFE6B6B21F92BA700006701 /* LegacyNotificationsAdaptee.swift */; };
|
||||||
70377AAB1918450100CAF501 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70377AAA1918450100CAF501 /* MobileCoreServices.framework */; };
|
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; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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 */,
|
4C948FF62146EB4800349F0D /* BlockListCache.swift */,
|
||||||
343D3D991E9283F100165CA4 /* BlockListUIUtils.h */,
|
343D3D991E9283F100165CA4 /* BlockListUIUtils.h */,
|
||||||
343D3D9A1E9283F100165CA4 /* BlockListUIUtils.m */,
|
343D3D9A1E9283F100165CA4 /* BlockListUIUtils.m */,
|
||||||
451777C71FD61554001225FF /* ConversationSearcher.swift */,
|
451777C71FD61554001225FF /* FullTextSearcher.swift */,
|
||||||
3466087120E550F300AFFE73 /* ConversationStyle.swift */,
|
3466087120E550F300AFFE73 /* ConversationStyle.swift */,
|
||||||
34480B4D1FD0A7A300BC14EF /* DebugLogger.h */,
|
34480B4D1FD0A7A300BC14EF /* DebugLogger.h */,
|
||||||
34480B4E1FD0A7A300BC14EF /* DebugLogger.m */,
|
34480B4E1FD0A7A300BC14EF /* DebugLogger.m */,
|
||||||
|
@ -2074,6 +2076,7 @@
|
||||||
4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */,
|
4C090A1A210FD9C7001FD7F9 /* HapticFeedback.swift */,
|
||||||
34B3F8331E8DF1700035BE1A /* ViewControllers */,
|
34B3F8331E8DF1700035BE1A /* ViewControllers */,
|
||||||
76EB052B18170B33006006FC /* Views */,
|
76EB052B18170B33006006FC /* Views */,
|
||||||
|
4CC613352227A00400E21A3A /* ConversationSearch.swift */,
|
||||||
);
|
);
|
||||||
name = UserInterface;
|
name = UserInterface;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -3382,7 +3385,7 @@
|
||||||
452C7CA72037628B003D51A5 /* Weak.swift in Sources */,
|
452C7CA72037628B003D51A5 /* Weak.swift in Sources */,
|
||||||
34D5872F208E2C4200D2255A /* OWS109OutgoingMessageState.m in Sources */,
|
34D5872F208E2C4200D2255A /* OWS109OutgoingMessageState.m in Sources */,
|
||||||
34AC09F8211B39B100997B47 /* CountryCodeViewController.m in Sources */,
|
34AC09F8211B39B100997B47 /* CountryCodeViewController.m in Sources */,
|
||||||
451F8A341FD710C3005CB9DA /* ConversationSearcher.swift in Sources */,
|
451F8A341FD710C3005CB9DA /* FullTextSearcher.swift in Sources */,
|
||||||
346129FE1FD5F31400532771 /* OWS106EnsureProfileComplete.swift in Sources */,
|
346129FE1FD5F31400532771 /* OWS106EnsureProfileComplete.swift in Sources */,
|
||||||
34AC0A10211B39EA00997B47 /* TappableView.swift in Sources */,
|
34AC0A10211B39EA00997B47 /* TappableView.swift in Sources */,
|
||||||
346129F91FD5F31400532771 /* OWS104CreateRecipientIdentities.m in Sources */,
|
346129F91FD5F31400532771 /* OWS104CreateRecipientIdentities.m in Sources */,
|
||||||
|
@ -3541,6 +3544,7 @@
|
||||||
340FC8BC204DAC8D007AEB0F /* FingerprintViewController.m in Sources */,
|
340FC8BC204DAC8D007AEB0F /* FingerprintViewController.m in Sources */,
|
||||||
450DF2051E0D74AC003D14BE /* Platform.swift in Sources */,
|
450DF2051E0D74AC003D14BE /* Platform.swift in Sources */,
|
||||||
34A4C61E221613D00042EF2E /* OnboardingVerificationViewController.swift in Sources */,
|
34A4C61E221613D00042EF2E /* OnboardingVerificationViewController.swift in Sources */,
|
||||||
|
4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */,
|
||||||
340FC8B2204DAC8D007AEB0F /* AdvancedSettingsTableViewController.m in Sources */,
|
340FC8B2204DAC8D007AEB0F /* AdvancedSettingsTableViewController.m in Sources */,
|
||||||
452B999020A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift in Sources */,
|
452B999020A34B6B006F2F9E /* AddContactShareToExistingContactViewController.swift in Sources */,
|
||||||
346129991FD1E4DA00532771 /* SignalApp.m 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
|
- (void)didTapShowAddToContactUIForContactShare:(ContactShareViewModel *)contactShare
|
||||||
NS_SWIFT_NAME(didTapShowAddToContactUI(forContactShare:));
|
NS_SWIFT_NAME(didTapShowAddToContactUI(forContactShare:));
|
||||||
|
|
||||||
|
@property (nonatomic, readonly, nullable) NSString *lastSearchedText;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
#pragma mark -
|
#pragma mark -
|
||||||
|
|
|
@ -691,6 +691,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
||||||
}
|
}
|
||||||
[self.class loadForTextDisplay:self.bodyTextView
|
[self.class loadForTextDisplay:self.bodyTextView
|
||||||
text:self.displayableBodyText.displayText
|
text:self.displayableBodyText.displayText
|
||||||
|
searchText:self.delegate.lastSearchedText
|
||||||
textColor:self.bodyTextColor
|
textColor:self.bodyTextColor
|
||||||
font:self.textMessageFont
|
font:self.textMessageFont
|
||||||
shouldIgnoreEvents:shouldIgnoreEvents];
|
shouldIgnoreEvents:shouldIgnoreEvents];
|
||||||
|
@ -698,6 +699,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
||||||
|
|
||||||
+ (void)loadForTextDisplay:(OWSMessageTextView *)textView
|
+ (void)loadForTextDisplay:(OWSMessageTextView *)textView
|
||||||
text:(NSString *)text
|
text:(NSString *)text
|
||||||
|
searchText:(nullable NSString *)searchText
|
||||||
textColor:(UIColor *)textColor
|
textColor:(UIColor *)textColor
|
||||||
font:(UIFont *)font
|
font:(UIFont *)font
|
||||||
shouldIgnoreEvents:(BOOL)shouldIgnoreEvents
|
shouldIgnoreEvents:(BOOL)shouldIgnoreEvents
|
||||||
|
@ -713,8 +715,29 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
||||||
};
|
};
|
||||||
textView.shouldIgnoreEvents = shouldIgnoreEvents;
|
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.
|
// 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
|
- (BOOL)shouldShowSenderName
|
||||||
|
@ -1259,6 +1282,7 @@ const UIDataDetectorTypes kOWSAllowedDataDetectorTypes
|
||||||
|
|
||||||
[self.bodyTextView removeFromSuperview];
|
[self.bodyTextView removeFromSuperview];
|
||||||
self.bodyTextView.text = nil;
|
self.bodyTextView.text = nil;
|
||||||
|
self.bodyTextView.attributedText = nil;
|
||||||
self.bodyTextView.hidden = YES;
|
self.bodyTextView.hidden = YES;
|
||||||
|
|
||||||
self.bubbleView.bubbleColor = nil;
|
self.bubbleView.bubbleColor = nil;
|
||||||
|
|
|
@ -211,7 +211,7 @@ public class ConversationMessageMapping: NSObject {
|
||||||
// Tries to ensure that the load window includes a given item.
|
// Tries to ensure that the load window includes a given item.
|
||||||
// On success, returns the index path of that item.
|
// On success, returns the index path of that item.
|
||||||
// On failure, returns nil.
|
// On failure, returns nil.
|
||||||
@objc
|
@objc(ensureLoadWindowContainsUniqueId:transaction:)
|
||||||
public func ensureLoadWindowContains(uniqueId: String,
|
public func ensureLoadWindowContains(uniqueId: String,
|
||||||
transaction: YapDatabaseReadTransaction) -> IndexPath? {
|
transaction: YapDatabaseReadTransaction) -> IndexPath? {
|
||||||
if let oldIndex = loadedUniqueIds().firstIndex(of: uniqueId) {
|
if let oldIndex = loadedUniqueIds().firstIndex(of: uniqueId) {
|
||||||
|
|
|
@ -141,6 +141,7 @@ typedef enum : NSUInteger {
|
||||||
ConversationViewLayoutDelegate,
|
ConversationViewLayoutDelegate,
|
||||||
ConversationViewCellDelegate,
|
ConversationViewCellDelegate,
|
||||||
ConversationInputTextViewDelegate,
|
ConversationInputTextViewDelegate,
|
||||||
|
ConversationSearchControllerDelegate,
|
||||||
LongTextViewDelegate,
|
LongTextViewDelegate,
|
||||||
MessageActionsDelegate,
|
MessageActionsDelegate,
|
||||||
MessageDetailViewDelegate,
|
MessageDetailViewDelegate,
|
||||||
|
@ -195,6 +196,8 @@ typedef enum : NSUInteger {
|
||||||
@property (nonatomic) BOOL userHasScrolled;
|
@property (nonatomic) BOOL userHasScrolled;
|
||||||
@property (nonatomic, nullable) NSDate *lastMessageSentDate;
|
@property (nonatomic, nullable) NSDate *lastMessageSentDate;
|
||||||
|
|
||||||
|
@property (nonatomic, nullable) UIBarButtonItem *customBackButton;
|
||||||
|
|
||||||
@property (nonatomic) BOOL showLoadMoreHeader;
|
@property (nonatomic) BOOL showLoadMoreHeader;
|
||||||
@property (nonatomic) UILabel *loadMoreHeader;
|
@property (nonatomic) UILabel *loadMoreHeader;
|
||||||
@property (nonatomic) uint64_t lastVisibleSortId;
|
@property (nonatomic) uint64_t lastVisibleSortId;
|
||||||
|
@ -227,6 +230,10 @@ typedef enum : NSUInteger {
|
||||||
@property (nonatomic) ScrollContinuity scrollContinuity;
|
@property (nonatomic) ScrollContinuity scrollContinuity;
|
||||||
@property (nonatomic, nullable) NSTimer *autoLoadMoreTimer;
|
@property (nonatomic, nullable) NSTimer *autoLoadMoreTimer;
|
||||||
|
|
||||||
|
@property (nonatomic, readonly) ConversationSearchController *searchController;
|
||||||
|
@property (nonatomic, nullable) NSString *lastSearchedText;
|
||||||
|
@property (nonatomic) BOOL isShowingSearchUI;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
#pragma mark -
|
#pragma mark -
|
||||||
|
@ -509,6 +516,9 @@ typedef enum : NSUInteger {
|
||||||
|
|
||||||
[self updateConversationSnapshot];
|
[self updateConversationSnapshot];
|
||||||
|
|
||||||
|
_searchController = [[ConversationSearchController alloc] initWithThread:thread];
|
||||||
|
_searchController.delegate = self;
|
||||||
|
|
||||||
[self updateShouldObserveVMUpdates];
|
[self updateShouldObserveVMUpdates];
|
||||||
|
|
||||||
self.reloadTimer = [NSTimer weakScheduledTimerWithTimeInterval:1.f
|
self.reloadTimer = [NSTimer weakScheduledTimerWithTimeInterval:1.f
|
||||||
|
@ -1391,6 +1401,7 @@ typedef enum : NSUInteger {
|
||||||
- (void)createBackButton
|
- (void)createBackButton
|
||||||
{
|
{
|
||||||
UIBarButtonItem *backItem = [self createOWSBackButton];
|
UIBarButtonItem *backItem = [self createOWSBackButton];
|
||||||
|
self.customBackButton = backItem;
|
||||||
if (backItem.customView) {
|
if (backItem.customView) {
|
||||||
// This method gets called multiple times, so it's important we re-layout the unread badge
|
// This method gets called multiple times, so it's important we re-layout the unread badge
|
||||||
// with respect to the new backItem.
|
// with respect to the new backItem.
|
||||||
|
@ -1425,11 +1436,23 @@ typedef enum : NSUInteger {
|
||||||
|
|
||||||
- (void)updateBarButtonItems
|
- (void)updateBarButtonItems
|
||||||
{
|
{
|
||||||
|
self.navigationItem.hidesBackButton = NO;
|
||||||
|
if (self.customBackButton) {
|
||||||
|
self.navigationItem.leftBarButtonItem = self.customBackButton;
|
||||||
|
}
|
||||||
|
|
||||||
if (self.userLeftGroup) {
|
if (self.userLeftGroup) {
|
||||||
self.navigationItem.rightBarButtonItems = @[];
|
self.navigationItem.rightBarButtonItems = @[];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (self.isShowingSearchUI) {
|
||||||
|
self.navigationItem.rightBarButtonItems = @[];
|
||||||
|
self.navigationItem.leftBarButtonItem = nil;
|
||||||
|
self.navigationItem.hidesBackButton = YES;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const CGFloat kBarButtonSize = 44;
|
const CGFloat kBarButtonSize = 44;
|
||||||
NSMutableArray<UIBarButtonItem *> *barButtons = [NSMutableArray new];
|
NSMutableArray<UIBarButtonItem *> *barButtons = [NSMutableArray new];
|
||||||
if ([self canCall]) {
|
if ([self canCall]) {
|
||||||
|
@ -3944,19 +3967,107 @@ typedef enum : NSUInteger {
|
||||||
[self updateGroupModelTo:groupModel successCompletion:nil];
|
[self updateGroupModelTo:groupModel successCompletion:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)popAllConversationSettingsViews
|
- (void)popAllConversationSettingsViewsWithCompletion:(void (^_Nullable)(void))completionBlock
|
||||||
{
|
{
|
||||||
if (self.presentedViewController) {
|
if (self.presentedViewController) {
|
||||||
[self.presentedViewController
|
[self.presentedViewController dismissViewControllerAnimated:YES
|
||||||
dismissViewControllerAnimated:YES
|
completion:^{
|
||||||
completion:^{
|
[self.navigationController
|
||||||
[self.navigationController popToViewController:self animated:YES];
|
popToViewController:self
|
||||||
}];
|
animated:YES
|
||||||
|
completion:completionBlock];
|
||||||
|
}];
|
||||||
} else {
|
} 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
|
#pragma mark - ConversationViewLayoutDelegate
|
||||||
|
|
||||||
- (NSArray<id<ConversationViewLayoutItem>> *)layoutItems
|
- (NSArray<id<ConversationViewLayoutItem>> *)layoutItems
|
||||||
|
|
|
@ -109,6 +109,7 @@ typedef NS_ENUM(NSUInteger, ConversationUpdateItemType) {
|
||||||
- (BOOL)canLoadMoreItems;
|
- (BOOL)canLoadMoreItems;
|
||||||
|
|
||||||
- (nullable NSIndexPath *)ensureLoadWindowContainsQuotedReply:(OWSQuotedReplyModel *)quotedReply;
|
- (nullable NSIndexPath *)ensureLoadWindowContainsQuotedReply:(OWSQuotedReplyModel *)quotedReply;
|
||||||
|
- (nullable NSIndexPath *)ensureLoadWindowContainsInteractionId:(NSString *)interactionId;
|
||||||
|
|
||||||
- (void)appendUnsavedOutgoingTextMessage:(TSOutgoingMessage *)outgoingMessage;
|
- (void)appendUnsavedOutgoingTextMessage:(TSOutgoingMessage *)outgoingMessage;
|
||||||
|
|
||||||
|
|
|
@ -1595,8 +1595,32 @@ static const int kYapDatabaseRangeMaxLength = 25000;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
indexPath = [self.messageMapping ensureLoadWindowContainsWithUniqueId:quotedInteraction.uniqueId
|
indexPath =
|
||||||
transaction:transaction];
|
[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];
|
self.collapseCutoffDate = [NSDate new];
|
||||||
|
|
|
@ -37,8 +37,8 @@ class ConversationSearchViewController: UITableViewController, BlockListCacheDel
|
||||||
return OWSPrimaryStorage.shared().uiDatabaseConnection
|
return OWSPrimaryStorage.shared().uiDatabaseConnection
|
||||||
}
|
}
|
||||||
|
|
||||||
var searcher: ConversationSearcher {
|
var searcher: FullTextSearcher {
|
||||||
return ConversationSearcher.shared
|
return FullTextSearcher.shared
|
||||||
}
|
}
|
||||||
|
|
||||||
private var contactsManager: OWSContactsManager {
|
private var contactsManager: OWSContactsManager {
|
||||||
|
|
|
@ -763,6 +763,10 @@ class MessageDetailViewController: OWSViewController, MediaGalleryDataSourceDele
|
||||||
UIPasteboard.general.string = messageTimestamp
|
UIPasteboard.general.string = messageTimestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lastSearchedText: String? {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// MediaGalleryDataSourceDelegate
|
// MediaGalleryDataSourceDelegate
|
||||||
|
|
||||||
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, willDelete items: [MediaGalleryItem], initiatedBy: AnyObject) {
|
func mediaGalleryDataSource(_ mediaGalleryDataSource: MediaGalleryDataSource, willDelete items: [MediaGalleryItem], initiatedBy: AnyObject) {
|
||||||
|
|
|
@ -45,7 +45,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
MFMessageComposeViewControllerDelegate>
|
MFMessageComposeViewControllerDelegate>
|
||||||
|
|
||||||
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
|
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
|
||||||
@property (nonatomic, readonly) ConversationSearcher *conversationSearcher;
|
@property (nonatomic, readonly) FullTextSearcher *fullTextSearcher;
|
||||||
|
|
||||||
@property (nonatomic, readonly) UIView *noSignalContactsView;
|
@property (nonatomic, readonly) UIView *noSignalContactsView;
|
||||||
|
|
||||||
|
@ -75,9 +75,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
#pragma mark - Dependencies
|
#pragma mark - Dependencies
|
||||||
|
|
||||||
- (ConversationSearcher *)conversationSearcher
|
- (FullTextSearcher *)fullTextSearcher
|
||||||
{
|
{
|
||||||
return ConversationSearcher.shared;
|
return FullTextSearcher.shared;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (YapDatabaseConnection *)uiDatabaseConnection
|
- (YapDatabaseConnection *)uiDatabaseConnection
|
||||||
|
@ -903,9 +903,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
[self.uiDatabaseConnection
|
[self.uiDatabaseConnection
|
||||||
asyncReadWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
|
asyncReadWithBlock:^(YapDatabaseReadTransaction *_Nonnull transaction) {
|
||||||
self.searchResults = [self.conversationSearcher searchForComposeScreenWithSearchText:searchText
|
self.searchResults = [self.fullTextSearcher searchForComposeScreenWithSearchText:searchText
|
||||||
transaction:transaction
|
transaction:transaction
|
||||||
contactsManager:self.contactsManager];
|
contactsManager:self.contactsManager];
|
||||||
}
|
}
|
||||||
completionBlock:^{
|
completionBlock:^{
|
||||||
__typeof(self) strongSelf = weakSelf;
|
__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>
|
#import <SignalMessaging/OWSViewController.h>
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@interface NewGroupViewController : OWSViewController
|
@interface NewGroupViewController : OWSViewController
|
||||||
|
|
||||||
@property (nonatomic, weak) id<OWSConversationSettingsViewDelegate> delegate;
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_END
|
NS_ASSUME_NONNULL_END
|
||||||
|
|
|
@ -321,13 +321,6 @@ const CGFloat kIconViewLength = 24;
|
||||||
mainSection.customHeaderView = [self mainSectionHeader];
|
mainSection.customHeaderView = [self mainSectionHeader];
|
||||||
mainSection.customHeaderHeight = @(100.f);
|
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
|
if ([self.thread isKindOfClass:[TSContactThread class]] && self.contactsManager.supportsContactEditing
|
||||||
&& !self.hasExistingContact) {
|
&& !self.hasExistingContact) {
|
||||||
[mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{
|
[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) {
|
if (!isNoteToSelf && !self.isGroupThread && self.thread.hasSafetyNumbers) {
|
||||||
[mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{
|
[mainSection addItem:[OWSTableItem itemWithCustomCellBlock:^{
|
||||||
return [weakSelf
|
return [weakSelf
|
||||||
|
@ -1331,6 +1346,11 @@ const CGFloat kIconViewLength = 24;
|
||||||
[mediaGallery pushTileViewFromNavController:(OWSNavigationController *)self.navigationController];
|
[mediaGallery pushTileViewFromNavController:(OWSNavigationController *)self.navigationController];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void)tappedConversationSearch
|
||||||
|
{
|
||||||
|
[self.conversationSettingsViewDelegate conversationSettingsDidRequestConversationSearch:self];
|
||||||
|
}
|
||||||
|
|
||||||
#pragma mark - Notifications
|
#pragma mark - Notifications
|
||||||
|
|
||||||
- (void)identityStateDidChange:(NSNotification *)notification
|
- (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
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
|
@class OWSConversationSettingsViewController;
|
||||||
@class TSGroupModel;
|
@class TSGroupModel;
|
||||||
|
|
||||||
@protocol OWSConversationSettingsViewDelegate <NSObject>
|
@protocol OWSConversationSettingsViewDelegate <NSObject>
|
||||||
|
|
||||||
- (void)conversationColorWasUpdated;
|
- (void)conversationColorWasUpdated;
|
||||||
- (void)groupWasUpdated:(TSGroupModel *)groupModel;
|
- (void)groupWasUpdated:(TSGroupModel *)groupModel;
|
||||||
|
- (void)conversationSettingsDidRequestConversationSearch:(OWSConversationSettingsViewController *)conversationSettingsViewController;
|
||||||
|
|
||||||
- (void)popAllConversationSettingsViews;
|
- (void)popAllConversationSettingsViewsWithCompletion:(void (^_Nullable)(void))completionBlock;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|
|
@ -431,7 +431,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
[self updateGroup];
|
[self updateGroup];
|
||||||
|
|
||||||
[self.conversationSettingsViewDelegate popAllConversationSettingsViews];
|
[self.conversationSettingsViewDelegate
|
||||||
|
popAllConversationSettingsViewsWithCompletion:nil];
|
||||||
}]];
|
}]];
|
||||||
[controller addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"ALERT_DONT_SAVE",
|
[controller addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"ALERT_DONT_SAVE",
|
||||||
@"The label for the 'don't save' button in action sheets.")
|
@"The label for the 'don't save' button in action sheets.")
|
||||||
|
@ -448,7 +449,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
[self updateGroup];
|
[self updateGroup];
|
||||||
|
|
||||||
[self.conversationSettingsViewDelegate popAllConversationSettingsViews];
|
[self.conversationSettingsViewDelegate popAllConversationSettingsViewsWithCompletion:nil];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)groupNameDidChange:(id)sender
|
- (void)groupNameDidChange:(id)sender
|
||||||
|
|
|
@ -8,7 +8,7 @@ import XCTest
|
||||||
|
|
||||||
// TODO: We might be able to merge this with OWSFakeContactsManager.
|
// TODO: We might be able to merge this with OWSFakeContactsManager.
|
||||||
@objc
|
@objc
|
||||||
class ConversationSearcherContactsManager: NSObject, ContactsManagerProtocol {
|
class FullTextSearcherContactsManager: NSObject, ContactsManagerProtocol {
|
||||||
func displayName(forPhoneIdentifier recipientId: String?, transaction: YapDatabaseReadTransaction) -> String {
|
func displayName(forPhoneIdentifier recipientId: String?, transaction: YapDatabaseReadTransaction) -> String {
|
||||||
return self.displayName(forPhoneIdentifier: recipientId)
|
return self.displayName(forPhoneIdentifier: recipientId)
|
||||||
}
|
}
|
||||||
|
@ -57,11 +57,11 @@ class ConversationSearcherContactsManager: NSObject, ContactsManagerProtocol {
|
||||||
private let bobRecipientId = "+49030183000"
|
private let bobRecipientId = "+49030183000"
|
||||||
private let aliceRecipientId = "+12345678900"
|
private let aliceRecipientId = "+12345678900"
|
||||||
|
|
||||||
class ConversationSearcherTest: SignalBaseTest {
|
class FullTextSearcherTest: SignalBaseTest {
|
||||||
|
|
||||||
// MARK: - Dependencies
|
// MARK: - Dependencies
|
||||||
var searcher: ConversationSearcher {
|
var searcher: FullTextSearcher {
|
||||||
return ConversationSearcher.shared
|
return FullTextSearcher.shared
|
||||||
}
|
}
|
||||||
|
|
||||||
var dbConnection: YapDatabaseConnection {
|
var dbConnection: YapDatabaseConnection {
|
||||||
|
@ -80,7 +80,7 @@ class ConversationSearcherTest: SignalBaseTest {
|
||||||
FullTextSearchFinder.ensureDatabaseExtensionRegistered(storage: OWSPrimaryStorage.shared())
|
FullTextSearchFinder.ensureDatabaseExtensionRegistered(storage: OWSPrimaryStorage.shared())
|
||||||
|
|
||||||
// Replace this singleton.
|
// Replace this singleton.
|
||||||
SSKEnvironment.shared.contactsManager = ConversationSearcherContactsManager()
|
SSKEnvironment.shared.contactsManager = FullTextSearcherContactsManager()
|
||||||
|
|
||||||
self.dbConnection.readWrite { transaction in
|
self.dbConnection.readWrite { transaction in
|
||||||
let bookModel = TSGroupModel(title: "Book Club", memberIds: [aliceRecipientId, bobRecipientId], image: nil, groupId: Randomness.generateRandomBytes(kGroupIdLength))
|
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. */
|
/* Title for the 'conversation delete confirmation' alert. */
|
||||||
"CONVERSATION_DELETE_CONFIRMATION_ALERT_TITLE" = "Delete Conversation?";
|
"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 */
|
/* title for conversation settings screen */
|
||||||
"CONVERSATION_SETTINGS" = "Conversation Settings";
|
"CONVERSATION_SETTINGS" = "Conversation Settings";
|
||||||
|
|
||||||
|
@ -620,6 +629,9 @@
|
||||||
/* Label for 'new contact' button in conversation settings view. */
|
/* Label for 'new contact' button in conversation settings view. */
|
||||||
"CONVERSATION_SETTINGS_NEW_CONTACT" = "Create New Contact";
|
"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. */
|
/* Label for button that opens conversation settings. */
|
||||||
"CONVERSATION_SETTINGS_TAP_TO_CHANGE" = "Tap to Change";
|
"CONVERSATION_SETTINGS_TAP_TO_CHANGE" = "Tap to Change";
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
NewNonContactConversationViewControllerDelegate>
|
NewNonContactConversationViewControllerDelegate>
|
||||||
|
|
||||||
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
|
@property (nonatomic, readonly) ContactsViewHelper *contactsViewHelper;
|
||||||
@property (nonatomic, readonly) ConversationSearcher *conversationSearcher;
|
@property (nonatomic, readonly) FullTextSearcher *fullTextSearcher;
|
||||||
@property (nonatomic, readonly) ThreadViewHelper *threadViewHelper;
|
@property (nonatomic, readonly) ThreadViewHelper *threadViewHelper;
|
||||||
@property (nonatomic, readonly) YapDatabaseConnection *uiDatabaseConnection;
|
@property (nonatomic, readonly) YapDatabaseConnection *uiDatabaseConnection;
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
self.view.backgroundColor = Theme.backgroundColor;
|
self.view.backgroundColor = Theme.backgroundColor;
|
||||||
|
|
||||||
_contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self];
|
_contactsViewHelper = [[ContactsViewHelper alloc] initWithDelegate:self];
|
||||||
_conversationSearcher = ConversationSearcher.shared;
|
_fullTextSearcher = FullTextSearcher.shared;
|
||||||
_threadViewHelper = [ThreadViewHelper new];
|
_threadViewHelper = [ThreadViewHelper new];
|
||||||
_threadViewHelper.delegate = self;
|
_threadViewHelper.delegate = self;
|
||||||
|
|
||||||
|
@ -341,7 +341,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
{
|
{
|
||||||
NSString *searchTerm = [[self.searchBar text] ows_stripped];
|
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
|
- (NSArray<SignalAccount *> *)filteredSignalAccountsWithSearchText
|
||||||
|
|
|
@ -32,7 +32,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
@property (nonatomic) BOOL shouldNotifyDelegateOfUpdatedContacts;
|
@property (nonatomic) BOOL shouldNotifyDelegateOfUpdatedContacts;
|
||||||
@property (nonatomic) BOOL hasUpdatedContactsAtLeastOnce;
|
@property (nonatomic) BOOL hasUpdatedContactsAtLeastOnce;
|
||||||
@property (nonatomic) OWSProfileManager *profileManager;
|
@property (nonatomic) OWSProfileManager *profileManager;
|
||||||
@property (nonatomic, readonly) ConversationSearcher *conversationSearcher;
|
@property (nonatomic, readonly) FullTextSearcher *fullTextSearcher;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
_blockListCache = [OWSBlockListCache new];
|
_blockListCache = [OWSBlockListCache new];
|
||||||
[_blockListCache startObservingAndSyncStateWithDelegate:self];
|
[_blockListCache startObservingAndSyncStateWithDelegate:self];
|
||||||
|
|
||||||
_conversationSearcher = ConversationSearcher.shared;
|
_fullTextSearcher = FullTextSearcher.shared;
|
||||||
|
|
||||||
_contactsManager = Environment.shared.contactsManager;
|
_contactsManager = Environment.shared.contactsManager;
|
||||||
_profileManager = [OWSProfileManager sharedManager];
|
_profileManager = [OWSProfileManager sharedManager];
|
||||||
|
@ -210,8 +210,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
NSMutableArray<SignalAccount *> *signalAccountsToSearch = [self.signalAccounts mutableCopy];
|
NSMutableArray<SignalAccount *> *signalAccountsToSearch = [self.signalAccounts mutableCopy];
|
||||||
SignalAccount *selfAccount = [[SignalAccount alloc] initWithRecipientId:self.localNumber];
|
SignalAccount *selfAccount = [[SignalAccount alloc] initWithRecipientId:self.localNumber];
|
||||||
[signalAccountsToSearch addObject:selfAccount];
|
[signalAccountsToSearch addObject:selfAccount];
|
||||||
return [self.conversationSearcher filterSignalAccounts:signalAccountsToSearch
|
return [self.fullTextSearcher filterSignalAccounts:signalAccountsToSearch withSearchText:searchText];
|
||||||
withSearchText:searchText];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL)doesContact:(Contact *)contact matchSearchTerm:(NSString *)searchTerm
|
- (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
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|
||||||
@interface OWSSearchBar : UISearchBar
|
@interface OWSSearchBar : UISearchBar
|
||||||
|
|
||||||
|
+ (void)applyThemeToSearchBar:(UISearchBar *)searchBar;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_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"
|
#import "OWSSearchBar.h"
|
||||||
|
@ -54,36 +54,41 @@ NS_ASSUME_NONNULL_BEGIN
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)ows_applyTheme
|
- (void)ows_applyTheme
|
||||||
|
{
|
||||||
|
[self.class applyThemeToSearchBar:self];
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (void)applyThemeToSearchBar:(UISearchBar *)searchBar
|
||||||
{
|
{
|
||||||
OWSAssertIsOnMainThread();
|
OWSAssertIsOnMainThread();
|
||||||
|
|
||||||
UIColor *foregroundColor = Theme.placeholderColor;
|
UIColor *foregroundColor = Theme.placeholderColor;
|
||||||
self.barTintColor = Theme.backgroundColor;
|
searchBar.barTintColor = Theme.backgroundColor;
|
||||||
self.barStyle = Theme.barStyle;
|
searchBar.barStyle = Theme.barStyle;
|
||||||
|
|
||||||
// Hide searchBar border.
|
// Hide searchBar border.
|
||||||
// Alternatively we could hide the border by using `UISearchBarStyleMinimal`, but that causes an issue when toggling
|
// 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
|
// from light -> dark -> light theme wherein the textField background color appears darker than it should
|
||||||
// (regardless of our re-setting textfield.backgroundColor below).
|
// (regardless of our re-setting textfield.backgroundColor below).
|
||||||
self.backgroundImage = [UIImage new];
|
searchBar.backgroundImage = [UIImage new];
|
||||||
|
|
||||||
if (Theme.isDarkThemeEnabled) {
|
if (Theme.isDarkThemeEnabled) {
|
||||||
UIImage *clearImage = [UIImage imageNamed:@"searchbar_clear"];
|
UIImage *clearImage = [UIImage imageNamed:@"searchbar_clear"];
|
||||||
[self setImage:[clearImage asTintedImageWithColor:foregroundColor]
|
[searchBar setImage:[clearImage asTintedImageWithColor:foregroundColor]
|
||||||
forSearchBarIcon:UISearchBarIconClear
|
forSearchBarIcon:UISearchBarIconClear
|
||||||
state:UIControlStateNormal];
|
state:UIControlStateNormal];
|
||||||
|
|
||||||
UIImage *searchImage = [UIImage imageNamed:@"searchbar_search"];
|
UIImage *searchImage = [UIImage imageNamed:@"searchbar_search"];
|
||||||
[self setImage:[searchImage asTintedImageWithColor:foregroundColor]
|
[searchBar setImage:[searchImage asTintedImageWithColor:foregroundColor]
|
||||||
forSearchBarIcon:UISearchBarIconSearch
|
forSearchBarIcon:UISearchBarIconSearch
|
||||||
state:UIControlStateNormal];
|
state:UIControlStateNormal];
|
||||||
} else {
|
} 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]]) {
|
if ([view isKindOfClass:[UITextField class]]) {
|
||||||
UITextField *textField = (UITextField *)view;
|
UITextField *textField = (UITextField *)view;
|
||||||
textField.backgroundColor = Theme.searchFieldBackgroundColor;
|
textField.backgroundColor = Theme.searchFieldBackgroundColor;
|
||||||
|
|
|
@ -16,7 +16,7 @@ public extension UIEdgeInsets {
|
||||||
@objc
|
@objc
|
||||||
public extension UINavigationController {
|
public extension UINavigationController {
|
||||||
@objc
|
@objc
|
||||||
public func pushViewController(viewController: UIViewController,
|
public func pushViewController(_ viewController: UIViewController,
|
||||||
animated: Bool,
|
animated: Bool,
|
||||||
completion: (() -> Void)?) {
|
completion: (() -> Void)?) {
|
||||||
CATransaction.begin()
|
CATransaction.begin()
|
||||||
|
@ -35,7 +35,7 @@ public extension UINavigationController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
public func popToViewController(viewController: UIViewController,
|
public func popToViewController(_ viewController: UIViewController,
|
||||||
animated: Bool,
|
animated: Bool,
|
||||||
completion: (() -> Void)?) {
|
completion: (() -> Void)?) {
|
||||||
CATransaction.begin()
|
CATransaction.begin()
|
||||||
|
|
|
@ -17,10 +17,10 @@ import Foundation
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
public func BenchAsync(title: String, block: (@escaping () -> Void) -> Void) {
|
public func BenchAsync(title: String, block: (@escaping () -> Void) -> Void) {
|
||||||
let startTime = CFAbsoluteTimeGetCurrent()
|
let startTime = CACurrentMediaTime()
|
||||||
|
|
||||||
block {
|
block {
|
||||||
let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
|
let timeElapsed = CACurrentMediaTime() - startTime
|
||||||
let formattedTime = String(format: "%0.2fms", timeElapsed * 1000)
|
let formattedTime = String(format: "%0.2fms", timeElapsed * 1000)
|
||||||
Logger.debug("[Bench] title: \(title), duration: \(formattedTime)")
|
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
|
/// crosses multiple classes, you can use the BenchEvent tools
|
||||||
///
|
///
|
||||||
/// // in one class
|
/// // in one class
|
||||||
/// let message = getMessage()
|
|
||||||
/// BenchEventStart(title: "message sending", eventId: message.id)
|
/// BenchEventStart(title: "message sending", eventId: message.id)
|
||||||
|
/// beginTheWork()
|
||||||
///
|
///
|
||||||
/// ...
|
/// ...
|
||||||
///
|
///
|
||||||
/// // in another class
|
/// // in another class
|
||||||
/// BenchEventComplete(title: "message sending", eventId: message.id)
|
/// doTheLastThing()
|
||||||
|
/// BenchEventComplete(eventId: message.id)
|
||||||
///
|
///
|
||||||
/// Or in objc
|
/// Or in objc
|
||||||
///
|
///
|
||||||
/// [BenchManager startEventWithTitle:"message sending" eventId:message.id]
|
/// [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) {
|
public func BenchEventStart(title: String, eventId: BenchmarkEventId) {
|
||||||
BenchAsync(title: title) { finish in
|
BenchAsync(title: title) { finish in
|
||||||
runningEvents[eventId] = Event(title: title, eventId: eventId, completion: finish)
|
runningEvents[eventId] = Event(title: title, eventId: eventId, completion: finish)
|
||||||
|
|
|
@ -162,7 +162,57 @@ public class ComposeScreenSearchResultSet: NSObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc
|
@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
|
// MARK: - Dependencies
|
||||||
|
|
||||||
|
@ -175,7 +225,7 @@ public class ConversationSearcher: NSObject {
|
||||||
private let finder: FullTextSearchFinder
|
private let finder: FullTextSearchFinder
|
||||||
|
|
||||||
@objc
|
@objc
|
||||||
public static let shared: ConversationSearcher = ConversationSearcher()
|
public static let shared: FullTextSearcher = FullTextSearcher()
|
||||||
override private init() {
|
override private init() {
|
||||||
finder = FullTextSearchFinder()
|
finder = FullTextSearchFinder()
|
||||||
super.init()
|
super.init()
|
||||||
|
@ -279,6 +329,39 @@ public class ConversationSearcher: NSObject {
|
||||||
return HomeScreenSearchResultSet(searchText: searchText, conversations: conversations, contacts: otherContacts, messages: messages)
|
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:)
|
@objc(filterThreads:withSearchText:)
|
||||||
public func filterThreads(_ threads: [TSThread], searchText: String) -> [TSThread] {
|
public func filterThreads(_ threads: [TSThread], searchText: String) -> [TSThread] {
|
||||||
guard searchText.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 else {
|
guard searchText.trimmingCharacters(in: .whitespacesAndNewlines).count > 0 else {
|
Loading…
Reference in a new issue