Implement rough conversation search
This commit is contained in:
parent
c4bd4cea6a
commit
d21d6836a9
|
@ -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 */,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -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]);
|
Loading…
Reference in New Issue