Implement rough conversation search

This commit is contained in:
Niels Andriesse 2021-02-19 10:50:18 +11:00
parent c4bd4cea6a
commit d21d6836a9
10 changed files with 278 additions and 145 deletions

View File

@ -2273,6 +2273,13 @@
B835246D25C38ABF0089A44F /* ConversationVC.swift */,
B8569AC225CB5D2900DBA3DB /* ConversationVC+Interaction.swift */,
3496744E2076ACCE00080B5F /* LongTextViewController.swift */,
4CC613352227A00400E21A3A /* ConversationSearch.swift */,
34D1F06F1F8678AA0066283D /* ConversationViewItem.h */,
34D1F0701F8678AA0066283D /* ConversationViewItem.m */,
34D1F0711F8678AA0066283D /* ConversationViewLayout.h */,
34D1F0721F8678AA0066283D /* ConversationViewLayout.m */,
341341ED2187467900192D59 /* ConversationViewModel.h */,
341341EE2187467900192D59 /* ConversationViewModel.m */,
B887C38125C7C79700E11DAE /* Input View */,
B835247725C38D190089A44F /* Message Cells */,
C328252E25CA54F70062D0A7 /* Context Menu */,
@ -2953,13 +2960,6 @@
B82B4093239DF15900A248E7 /* ConversationTitleView.swift */,
34D1F06D1F8678AA0066283D /* ConversationViewController.h */,
34D1F06E1F8678AA0066283D /* ConversationViewController.m */,
34D1F06F1F8678AA0066283D /* ConversationViewItem.h */,
34D1F0701F8678AA0066283D /* ConversationViewItem.m */,
34D1F0711F8678AA0066283D /* ConversationViewLayout.h */,
34D1F0721F8678AA0066283D /* ConversationViewLayout.m */,
341341ED2187467900192D59 /* ConversationViewModel.h */,
341341EE2187467900192D59 /* ConversationViewModel.m */,
4CC613352227A00400E21A3A /* ConversationSearch.swift */,
4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */,
4CB5F26820F7D060004D1B42 /* MessageActions.swift */,
34CA1C261F7156F300E51C51 /* MessageDetailViewController.swift */,

View File

@ -17,7 +17,7 @@ public protocol ConversationSearchControllerDelegate: UISearchControllerDelegate
}
@objc
public class ConversationSearchController: NSObject {
public class ConversationSearchController : NSObject {
@objc
public static let kMinimumSearchTextLength: UInt = 2
@ -31,7 +31,7 @@ public class ConversationSearchController: NSObject {
let thread: TSThread
@objc
public let resultsBar: SearchResultsBar = SearchResultsBar(frame: .zero)
public let resultsBar: SearchResultsBarV2 = SearchResultsBarV2()
// MARK: Initializer
@ -45,14 +45,12 @@ public class ConversationSearchController: NSObject {
uiSearchController.searchResultsUpdater = self
uiSearchController.hidesNavigationBarDuringPresentation = false
uiSearchController.dimsBackgroundDuringPresentation = false
if #available(iOS 13, *) {
// Do nothing
} else {
uiSearchController.dimsBackgroundDuringPresentation = false
}
uiSearchController.searchBar.inputAccessoryView = resultsBar
applyTheme()
}
func applyTheme() {
OWSSearchBar.applyTheme(to: uiSearchController.searchBar)
}
// MARK: Dependencies
@ -62,7 +60,8 @@ public class ConversationSearchController: NSObject {
}
}
extension ConversationSearchController: UISearchControllerDelegate {
extension ConversationSearchController : UISearchControllerDelegate {
public func didPresentSearchController(_ searchController: UISearchController) {
Logger.verbose("")
delegate?.didPresentSearchController?(searchController)
@ -74,7 +73,8 @@ extension ConversationSearchController: UISearchControllerDelegate {
}
}
extension ConversationSearchController: UISearchResultsUpdating {
extension ConversationSearchController : UISearchResultsUpdating {
var dbSearcher: FullTextSearcher {
return FullTextSearcher.shared
}
@ -88,7 +88,6 @@ extension ConversationSearchController: UISearchResultsUpdating {
return
}
let searchText = FullTextSearchFinder.normalize(text: rawSearchText)
BenchManager.startEvent(title: "Conversation Search", eventId: searchText)
guard searchText.count >= ConversationSearchController.kMinimumSearchTextLength else {
self.resultsBar.updateResults(resultSet: nil)
@ -112,8 +111,9 @@ extension ConversationSearchController: UISearchResultsUpdating {
}
}
extension ConversationSearchController: SearchResultsBarDelegate {
func searchResultsBar(_ searchResultsBar: SearchResultsBar,
extension ConversationSearchController : SearchResultsBarDelegate {
func searchResultsBar(_ searchResultsBar: SearchResultsBarV2,
setCurrentIndex currentIndex: Int,
resultSet: ConversationScreenSearchResultSet) {
guard let searchResult = resultSet.messages[safe: currentIndex] else {
@ -126,68 +126,95 @@ extension ConversationSearchController: SearchResultsBarDelegate {
}
}
protocol SearchResultsBarDelegate: AnyObject {
func searchResultsBar(_ searchResultsBar: SearchResultsBar,
protocol SearchResultsBarDelegate : AnyObject {
func searchResultsBar(_ searchResultsBar: SearchResultsBarV2,
setCurrentIndex currentIndex: Int,
resultSet: ConversationScreenSearchResultSet)
}
public class SearchResultsBar: UIToolbar {
public final class SearchResultsBarV2 : UIView {
private var resultSet: ConversationScreenSearchResultSet?
var currentIndex: Int?
weak var resultsBarDelegate: SearchResultsBarDelegate?
var showLessRecentButton: UIBarButtonItem!
var showMoreRecentButton: UIBarButtonItem!
let labelItem: UIBarButtonItem
var resultSet: ConversationScreenSearchResultSet?
public override var intrinsicContentSize: CGSize { CGSize.zero }
private lazy var label: UILabel = {
let result = UILabel()
result.text = "Test"
result.font = .boldSystemFont(ofSize: Values.smallFontSize)
result.textColor = Colors.text
return result
}()
private lazy var upButton: UIButton = {
let icon = #imageLiteral(resourceName: "ic_chevron_up").withRenderingMode(.alwaysTemplate)
let result = UIButton()
result.setImage(icon, for: UIControl.State.normal)
result.tintColor = Colors.accent
result.addTarget(self, action: #selector(handleUpButtonTapped), for: UIControl.Event.touchUpInside)
return result
}()
private lazy var downButton: UIButton = {
let icon = #imageLiteral(resourceName: "ic_chevron_down").withRenderingMode(.alwaysTemplate)
let result = UIButton()
result.setImage(icon, for: UIControl.State.normal)
result.tintColor = Colors.accent
result.addTarget(self, action: #selector(handleDownButtonTapped), for: UIControl.Event.touchUpInside)
return result
}()
override init(frame: CGRect) {
labelItem = UIBarButtonItem(title: nil, style: .plain, target: nil, action: nil)
labelItem.setTitleTextAttributes([ .font : UIFont.systemFont(ofSize: Values.mediumFontSize) ], for: UIControl.State.normal)
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 = Colors.accent
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 = Colors.accent
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 = Colors.navigationBarBackground
self.autoresizingMask = .flexibleHeight
self.translatesAutoresizingMaskIntoConstraints = false
setUpViewHierarchy()
}
required init?(coder aDecoder: NSCoder) {
notImplemented()
required init?(coder: NSCoder) {
super.init(coder: coder)
setUpViewHierarchy()
}
private func setUpViewHierarchy() {
autoresizingMask = .flexibleHeight
// Background & blur
let backgroundView = UIView()
backgroundView.backgroundColor = isLightMode ? .white : .black
backgroundView.alpha = Values.lowOpacity
addSubview(backgroundView)
backgroundView.pin(to: self)
let blurView = UIVisualEffectView(effect: UIBlurEffect(style: .regular))
addSubview(blurView)
blurView.pin(to: self)
// Separator
let separator = UIView()
separator.backgroundColor = Colors.text.withAlphaComponent(0.2)
separator.set(.height, to: 1 / UIScreen.main.scale)
addSubview(separator)
separator.pin([ UIView.HorizontalEdge.leading, UIView.VerticalEdge.top, UIView.HorizontalEdge.trailing ], to: self)
// Spacers
let spacer1 = UIView.hStretchingSpacer()
let spacer2 = UIView.hStretchingSpacer()
// Button containers
let upButtonContainer = UIView(wrapping: upButton, withInsets: UIEdgeInsets(top: 2, left: 0, bottom: 0, right: 0))
let downButtonContainer = UIView(wrapping: downButton, withInsets: UIEdgeInsets(top: 0, left: 0, bottom: 2, right: 0))
// Main stack view
let mainStackView = UIStackView(arrangedSubviews: [ upButtonContainer, downButtonContainer, spacer1, label, spacer2 ])
mainStackView.axis = .horizontal
mainStackView.spacing = Values.mediumSpacing
mainStackView.isLayoutMarginsRelativeArrangement = true
mainStackView.layoutMargins = UIEdgeInsets(top: Values.smallSpacing, leading: Values.largeSpacing, bottom: Values.smallSpacing, trailing: Values.largeSpacing)
addSubview(mainStackView)
mainStackView.pin(.top, to: .bottom, of: separator)
mainStackView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing ], to: self)
mainStackView.pin(.bottom, to: .bottom, of: self, withInset: -2)
// Remaining constraints
label.center(.horizontal, in: self)
}
@objc
public func didTapShowLessRecent() {
public func handleUpButtonTapped() {
Logger.debug("")
guard let resultSet = resultSet else {
owsFailDebug("resultSet was unexpectedly nil")
@ -211,7 +238,7 @@ public class SearchResultsBar: UIToolbar {
}
@objc
public func didTapShowMoreRecent() {
public func handleDownButtonTapped() {
Logger.debug("")
guard let resultSet = resultSet else {
owsFailDebug("resultSet was unexpectedly nil")
@ -234,10 +261,6 @@ public class SearchResultsBar: UIToolbar {
resultsBarDelegate?.searchResultsBar(self, setCurrentIndex: newIndex, resultSet: resultSet)
}
var currentIndex: Int?
// MARK:
func updateResults(resultSet: ConversationScreenSearchResultSet?) {
if let resultSet = resultSet {
if resultSet.messages.count > 0 {
@ -259,17 +282,17 @@ public class SearchResultsBar: UIToolbar {
func updateBarItems() {
guard let resultSet = resultSet else {
labelItem.title = nil
showMoreRecentButton.isEnabled = false
showLessRecentButton.isEnabled = false
label.text = ""
downButton.isEnabled = false
upButton.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")
label.text = 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")
label.text = NSLocalizedString("CONVERSATION_SEARCH_ONE_RESULT", comment: "keyboard toolbar label when exactly 1 message matches the search string")
default:
let format = NSLocalizedString("CONVERSATION_SEARCH_RESULTS_FORMAT",
comment: "keyboard toolbar label when more than 1 message matches the search string. Embeds {{number/position of the 'currently viewed' result}} and the {{total number of results}}")
@ -278,15 +301,15 @@ public class SearchResultsBar: UIToolbar {
owsFailDebug("currentIndex was unexpectedly nil")
return
}
labelItem.title = String(format: format, currentIndex + 1, resultSet.messages.count)
label.text = String(format: format, currentIndex + 1, resultSet.messages.count)
}
if let currentIndex = currentIndex {
showMoreRecentButton.isEnabled = currentIndex > 0
showLessRecentButton.isEnabled = currentIndex + 1 < resultSet.messages.count
downButton.isEnabled = currentIndex > 0
upButton.isEnabled = currentIndex + 1 < resultSet.messages.count
} else {
showMoreRecentButton.isEnabled = false
showLessRecentButton.isEnabled = false
downButton.isEnabled = false
upButton.isEnabled = false
}
}
}

View File

@ -7,6 +7,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
@objc func openSettings() {
let settingsVC = OWSConversationSettingsViewController()
settingsVC.configure(with: thread, uiDatabaseConnection: OWSPrimaryStorage.shared().uiDatabaseConnection)
settingsVC.conversationSettingsViewDelegate = self
navigationController!.pushViewController(settingsVC, animated: true, completion: nil)
}
@ -25,7 +26,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
})
}
private func showBlockedModalIfNeeded() -> Bool {
func showBlockedModalIfNeeded() -> Bool {
guard let thread = thread as? TSContactThread else { return false }
let publicKey = thread.contactIdentifier()
guard OWSBlockingManager.shared().isRecipientIdBlocked(publicKey) else { return false }
@ -157,12 +158,12 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
showAttachmentApprovalDialog(for: [ attachment ])
}
private func showAttachmentApprovalDialog(for attachments: [SignalAttachment]) {
func showAttachmentApprovalDialog(for attachments: [SignalAttachment]) {
let navController = AttachmentApprovalViewController.wrappedInNavController(attachments: attachments, approvalDelegate: self)
present(navController, animated: true, completion: nil)
}
private func showAttachmentApprovalDialogAfterProcessingVideo(at url: URL, with fileName: String) {
func showAttachmentApprovalDialogAfterProcessingVideo(at url: URL, with fileName: String) {
ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: true, message: nil) { [weak self] modalActivityIndicator in
let dataSource = DataSourcePath.dataSource(with: url, shouldDeleteOnDeallocation: false)!
dataSource.sourceFilename = fileName
@ -264,7 +265,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
}
// MARK: Mentions
private func updateMentions(for newText: String) {
func updateMentions(for newText: String) {
if newText.count < oldText.count {
currentMentionStartIndex = nil
snInputView.hideMentionsUI()
@ -299,13 +300,13 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
oldText = newText
}
private func resetMentions() {
func resetMentions() {
oldText = ""
currentMentionStartIndex = nil
mentions = []
}
private func replaceMentions(in text: String) -> String {
func replaceMentions(in text: String) -> String {
var result = text
for mention in mentions {
guard let range = result.range(of: "@\(mention.displayName)") else { continue }

View File

@ -7,12 +7,13 @@
// Photo rounding
// Disappearing messages timer
// Scroll button behind mentions view
// Search...
// Remaining search bugs
final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewDataSource, UITableViewDelegate {
final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversationSettingsViewDelegate, ConversationSearchControllerDelegate, UITableViewDataSource, UITableViewDelegate {
let thread: TSThread
private let focusedMessageID: String?
private var didConstrainScrollButton = false
let focusedMessageID: String?
var didConstrainScrollButton = false
var isShowingSearchUI = false
// Audio playback & recording
var audioPlayer: OWSAudioPlayer?
var audioRecorder: AVAudioRecorder?
@ -25,30 +26,30 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewD
var currentMentionStartIndex: String.Index?
var mentions: [Mention] = []
// Scrolling & paging
private var isUserScrolling = false
private var didFinishInitialLayout = false
private var isLoadingMore = false
private var scrollDistanceToBottomBeforeUpdate: CGFloat?
var isUserScrolling = false
var didFinishInitialLayout = false
var isLoadingMore = false
var scrollDistanceToBottomBeforeUpdate: CGFloat?
var audioSession: OWSAudioSession { Environment.shared.audioSession }
private var dbConnection: YapDatabaseConnection { OWSPrimaryStorage.shared().uiDatabaseConnection }
var dbConnection: YapDatabaseConnection { OWSPrimaryStorage.shared().uiDatabaseConnection }
var viewItems: [ConversationViewItem] { viewModel.viewState.viewItems }
func conversationStyle() -> ConversationStyle { return ConversationStyle(thread: thread) }
override var inputAccessoryView: UIView? { snInputView }
override var inputAccessoryView: UIView? { isShowingSearchUI ? searchController.resultsBar : snInputView }
override var canBecomeFirstResponder: Bool { true }
private var tableViewUnobscuredHeight: CGFloat {
var tableViewUnobscuredHeight: CGFloat {
let bottomInset = messagesTableView.adjustedContentInset.bottom
return messagesTableView.bounds.height - bottomInset
}
private var lastPageTop: CGFloat {
var lastPageTop: CGFloat {
return messagesTableView.contentSize.height - tableViewUnobscuredHeight
}
lazy var viewModel = ConversationViewModel(thread: thread, focusMessageIdOnOpen: focusedMessageID, delegate: self)
private lazy var mediaCache: NSCache<NSString, AnyObject> = {
lazy var mediaCache: NSCache<NSString, AnyObject> = {
let result = NSCache<NSString, AnyObject>()
result.countLimit = 40
return result
@ -56,8 +57,14 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewD
lazy var recordVoiceMessageActivity = AudioActivity(audioDescription: "Voice message", behavior: .playAndRecord)
lazy var searchController: ConversationSearchController = {
let result = ConversationSearchController(thread: thread)
result.delegate = self
return result
}()
// MARK: UI Components
private lazy var titleView = ConversationTitleViewV2(thread: thread)
lazy var titleView = ConversationTitleViewV2(thread: thread)
lazy var messagesTableView: MessagesTableView = {
let result = MessagesTableView()
@ -86,12 +93,12 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewD
}()
// MARK: Settings
private static let bottomInset = Values.mediumSpacing
private static let loadMoreThreshold: CGFloat = 120
static let bottomInset = Values.mediumSpacing
static let loadMoreThreshold: CGFloat = 120
/// The button will be fully visible once the user has scrolled this amount from the bottom of the table view.
private static let scrollButtonFullVisibilityThreshold: CGFloat = 80
static let scrollButtonFullVisibilityThreshold: CGFloat = 80
/// The button will be invisible until the user has scrolled at least this amount from the bottom of the table view.
private static let scrollButtonNoVisibilityThreshold: CGFloat = 20
static let scrollButtonNoVisibilityThreshold: CGFloat = 20
// MARK: Lifecycle
init(thread: TSThread, focusedMessageID: String? = nil) {
@ -168,28 +175,33 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewD
}
// MARK: Updating
private func updateNavBarButtons() {
let rightBarButtonItem: UIBarButtonItem
if thread is TSContactThread {
let size = Values.verySmallProfilePictureSize
let profilePictureView = ProfilePictureView()
profilePictureView.accessibilityLabel = "Settings button"
profilePictureView.size = size
profilePictureView.update(for: thread)
profilePictureView.set(.width, to: size)
profilePictureView.set(.height, to: size)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openSettings))
profilePictureView.addGestureRecognizer(tapGestureRecognizer)
rightBarButtonItem = UIBarButtonItem(customView: profilePictureView)
func updateNavBarButtons() {
navigationItem.hidesBackButton = isShowingSearchUI
if isShowingSearchUI {
navigationItem.rightBarButtonItems = []
} else {
rightBarButtonItem = UIBarButtonItem(image: UIImage(named: "Gear"), style: .plain, target: self, action: #selector(openSettings))
let rightBarButtonItem: UIBarButtonItem
if thread is TSContactThread {
let size = Values.verySmallProfilePictureSize
let profilePictureView = ProfilePictureView()
profilePictureView.accessibilityLabel = "Settings button"
profilePictureView.size = size
profilePictureView.update(for: thread)
profilePictureView.set(.width, to: size)
profilePictureView.set(.height, to: size)
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(openSettings))
profilePictureView.addGestureRecognizer(tapGestureRecognizer)
rightBarButtonItem = UIBarButtonItem(customView: profilePictureView)
} else {
rightBarButtonItem = UIBarButtonItem(image: UIImage(named: "Gear"), style: .plain, target: self, action: #selector(openSettings))
}
rightBarButtonItem.accessibilityLabel = "Settings button"
rightBarButtonItem.isAccessibilityElement = true
navigationItem.rightBarButtonItem = rightBarButtonItem
}
rightBarButtonItem.accessibilityLabel = "Settings button"
rightBarButtonItem.isAccessibilityElement = true
navigationItem.rightBarButtonItem = rightBarButtonItem
}
@objc private func handleKeyboardWillChangeFrameNotification(_ notification: Notification) {
@objc func handleKeyboardWillChangeFrameNotification(_ notification: Notification) {
guard let newHeight = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.size.height else { return }
if !didConstrainScrollButton {
// Bit of a hack to do this here, but it works out.
@ -202,7 +214,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewD
}
}
@objc private func handleKeyboardWillHideNotification(_ notification: Notification) {
@objc func handleKeyboardWillHideNotification(_ notification: Notification) {
UIView.animate(withDuration: 0.25) {
self.messagesTableView.keyboardHeight = 0
self.scrollButton.alpha = self.getScrollButtonOpacity()
@ -298,7 +310,8 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewD
}
@objc private func addOrRemoveBlockedBanner() {
// MARK: General
@objc func addOrRemoveBlockedBanner() {
func detach() {
blockedBanner.removeFromSuperview()
}
@ -311,7 +324,6 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewD
}
}
// MARK: General
func markAllAsRead() {
guard let lastSortID = viewItems.last?.interaction.sortId else { return }
OWSReadReceiptManager.shared().markAsReadLocally(beforeSortId: lastSortID, thread: thread)
@ -353,7 +365,7 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewD
autoLoadMoreIfNeeded()
}
private func autoLoadMoreIfNeeded() {
func autoLoadMoreIfNeeded() {
let isMainAppAndActive = CurrentAppContext().isMainAppAndActive
guard isMainAppAndActive && viewModel.canLoadMoreItems() && !isLoadingMore
&& messagesTableView.contentOffset.y < ConversationVC.loadMoreThreshold else { return }
@ -361,11 +373,118 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, UITableViewD
viewModel.loadAnotherPageOfMessages()
}
// MARK: Convenience
func getScrollButtonOpacity() -> CGFloat {
let contentOffsetY = messagesTableView.contentOffset.y
let x = (lastPageTop - ConversationVC.bottomInset - contentOffsetY).clamp(0, .greatestFiniteMagnitude)
let a = 1 / (ConversationVC.scrollButtonFullVisibilityThreshold - ConversationVC.scrollButtonNoVisibilityThreshold)
return a * x
}
func groupWasUpdated(_ groupModel: TSGroupModel) {
// Do nothing
}
// MARK: Search
func conversationSettingsDidRequestConversationSearch(_ conversationSettingsViewController: OWSConversationSettingsViewController) {
showSearchUI()
popAllConversationSettingsViews {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.searchController.uiSearchController.searchBar.becomeFirstResponder()
}
}
}
func popAllConversationSettingsViews(completion completionBlock: (() -> Void)? = nil) {
if presentedViewController != nil {
dismiss(animated: true) {
self.navigationController!.popToViewController(self, animated: true, completion: completionBlock)
}
} else {
navigationController!.popToViewController(self, animated: true, completion: completionBlock)
}
}
func showSearchUI() {
isShowingSearchUI = true
// Search bar
let searchBar = searchController.uiSearchController.searchBar
searchBar.searchBarStyle = .minimal
searchBar.barStyle = .black
searchBar.tintColor = Colors.accent
let searchIcon = UIImage(named: "searchbar_search")!.asTintedImage(color: Colors.searchBarPlaceholder)
searchBar.setImage(searchIcon, for: .search, state: UIControl.State.normal)
let clearIcon = UIImage(named: "searchbar_clear")!.asTintedImage(color: Colors.searchBarPlaceholder)
searchBar.setImage(clearIcon, for: .clear, state: UIControl.State.normal)
let searchTextField: UITextField
if #available(iOS 13, *) {
searchTextField = searchBar.searchTextField
} else {
searchTextField = searchBar.value(forKey: "_searchField") as! UITextField
}
searchTextField.backgroundColor = Colors.searchBarBackground
searchTextField.textColor = Colors.text
searchTextField.attributedPlaceholder = NSAttributedString(string: "Search", attributes: [ .foregroundColor : Colors.searchBarPlaceholder ])
searchTextField.keyboardAppearance = isLightMode ? .default : .dark
searchBar.setPositionAdjustment(UIOffset(horizontal: 4, vertical: 0), for: .search)
searchBar.searchTextPositionAdjustment = UIOffset(horizontal: 2, vertical: 0)
searchBar.setPositionAdjustment(UIOffset(horizontal: -4, vertical: 0), for: .clear)
navigationItem.titleView = searchBar
// Nav bar buttons
updateNavBarButtons()
// Hack so that the ResultsBar stays on the screen when dismissing the search field
// keyboard.
//
// Details:
//
// When the search UI is activated, both the SearchField and the ConversationVC
// have the resultsBar as their inputAccessoryView.
//
// So when the SearchField is first responder, the ResultsBar is shown on top of the keyboard.
// When the ConversationVC is first responder, the ResultsBar is shown at the bottom of the
// screen.
//
// When the user swipes to dismiss the keyboard, trying to see more of the content while
// searching, we want the ResultsBar to stay at the bottom of the screen - that is, we
// want the ConversationVC to becomeFirstResponder.
//
// If the SearchField were a subview of ConversationVC.view, this would all be automatic,
// as first responder status is percolated up the responder chain via `nextResponder`, which
// basically travereses each superView, until you're at a rootView, at which point the next
// responder is the ViewController which controls that View.
//
// However, because SearchField lives in the Navbar, it's "controlled" by the
// NavigationController, not the ConversationVC.
//
// So here we stub the next responder on the navBar so that when the searchBar resigns
// first responder, the ConversationVC will be in it's responder chain - keeeping the
// ResultsBar on the bottom of the screen after dismissing the keyboard.
let navBar = navigationController!.navigationBar as! OWSNavigationBar
navBar.stubbedNextResponder = self
}
func hideSearchUI() {
isShowingSearchUI = false
navigationItem.titleView = titleView
updateNavBarButtons()
let navBar = navigationController!.navigationBar as! OWSNavigationBar
navBar.stubbedNextResponder = nil
becomeFirstResponder()
}
func didDismissSearchController(_ searchController: UISearchController) {
hideSearchUI()
}
func conversationSearchController(_ conversationSearchController: ConversationSearchController, didUpdateSearchResults resultSet: ConversationScreenSearchResultSet?) {
messagesTableView.reloadRows(at: messagesTableView.indexPathsForVisibleRows ?? [], with: UITableView.RowAnimation.none)
}
func conversationSearchController(_ conversationSearchController: ConversationSearchController, didSelectMessageId interactionID: String) {
scrollToInteraction(with: interactionID)
}
private func scrollToInteraction(with interactionID: String) {
guard let indexPath = viewModel.ensureLoadWindowContainsInteractionId(interactionID) else { return }
messagesTableView.scrollToRow(at: indexPath, at: UITableView.ScrollPosition.middle, animated: true)
}
}

View File

@ -669,9 +669,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
if (self.hasBodyText) {
if (self.messageCellType == OWSMessageCellType_Unknown) {
// OWSAssertDebug(message.attachmentIds.count == 0
// || (message.attachmentIds.count == 1 &&
// [message oversizeTextAttachmentWithTransaction:transaction] != nil));
self.messageCellType = OWSMessageCellType_TextOnlyMessage;
}
OWSAssertDebug(self.displayableBodyText);

View File

@ -622,13 +622,6 @@ static const int kYapDatabaseRangeMaxLength = 25000;
NSMutableSet<NSString *> *diffRemovedItemIds = [diff.removedItemIds mutableCopy];
NSMutableSet<NSString *> *diffUpdatedItemIds = [diff.updatedItemIds mutableCopy];
for (TSOutgoingMessage *unsavedOutgoingMessage in self.unsavedOutgoingMessages) {
// unsavedOutgoingMessages should only exist for a short period (usually 30-50ms) before
// they are saved and moved into the `persistedViewItems`
// Loki: Original code
// ========
// OWSAssertDebug(unsavedOutgoingMessage.timestamp >= ([NSDate ows_millisecondTimeStamp] - 1 * kSecondInMs));
// ========
BOOL isFound = ([diff.addedItemIds containsObject:unsavedOutgoingMessage.uniqueId] ||
[diff.removedItemIds containsObject:unsavedOutgoingMessage.uniqueId] ||
[diff.updatedItemIds containsObject:unsavedOutgoingMessage.uniqueId]);