diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 22293bea9..82fc2a3d7 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -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 */, diff --git a/Session/Conversations/ConversationSearch.swift b/Session/Conversations V2/ConversationSearch.swift similarity index 58% rename from Session/Conversations/ConversationSearch.swift rename to Session/Conversations V2/ConversationSearch.swift index 5989dc851..7d01ded41 100644 --- a/Session/Conversations/ConversationSearch.swift +++ b/Session/Conversations V2/ConversationSearch.swift @@ -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 } } } diff --git a/Session/Conversations V2/ConversationVC+Interaction.swift b/Session/Conversations V2/ConversationVC+Interaction.swift index 5209835eb..7aff55ce1 100644 --- a/Session/Conversations V2/ConversationVC+Interaction.swift +++ b/Session/Conversations V2/ConversationVC+Interaction.swift @@ -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 } diff --git a/Session/Conversations V2/ConversationVC.swift b/Session/Conversations V2/ConversationVC.swift index 800f55446..0a90f8c55 100644 --- a/Session/Conversations V2/ConversationVC.swift +++ b/Session/Conversations V2/ConversationVC.swift @@ -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 = { + lazy var mediaCache: NSCache = { let result = NSCache() 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) + } } diff --git a/Session/Conversations/ConversationViewItem.h b/Session/Conversations V2/ConversationViewItem.h similarity index 100% rename from Session/Conversations/ConversationViewItem.h rename to Session/Conversations V2/ConversationViewItem.h diff --git a/Session/Conversations/ConversationViewItem.m b/Session/Conversations V2/ConversationViewItem.m similarity index 99% rename from Session/Conversations/ConversationViewItem.m rename to Session/Conversations V2/ConversationViewItem.m index a6df0ce38..98499ce11 100644 --- a/Session/Conversations/ConversationViewItem.m +++ b/Session/Conversations V2/ConversationViewItem.m @@ -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); diff --git a/Session/Conversations/ConversationViewLayout.h b/Session/Conversations V2/ConversationViewLayout.h similarity index 100% rename from Session/Conversations/ConversationViewLayout.h rename to Session/Conversations V2/ConversationViewLayout.h diff --git a/Session/Conversations/ConversationViewLayout.m b/Session/Conversations V2/ConversationViewLayout.m similarity index 100% rename from Session/Conversations/ConversationViewLayout.m rename to Session/Conversations V2/ConversationViewLayout.m diff --git a/Session/Conversations/ConversationViewModel.h b/Session/Conversations V2/ConversationViewModel.h similarity index 100% rename from Session/Conversations/ConversationViewModel.h rename to Session/Conversations V2/ConversationViewModel.h diff --git a/Session/Conversations/ConversationViewModel.m b/Session/Conversations V2/ConversationViewModel.m similarity index 99% rename from Session/Conversations/ConversationViewModel.m rename to Session/Conversations V2/ConversationViewModel.m index 021ad6b13..432f4d8a7 100644 --- a/Session/Conversations/ConversationViewModel.m +++ b/Session/Conversations V2/ConversationViewModel.m @@ -622,13 +622,6 @@ static const int kYapDatabaseRangeMaxLength = 25000; NSMutableSet *diffRemovedItemIds = [diff.removedItemIds mutableCopy]; NSMutableSet *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]);