Port CVC to Swift.
This commit is contained in:
parent
be48bb75ca
commit
c093739463
@ -341,6 +341,8 @@
|
||||
34A4D8872678F24300A794E7 /* ConversationViewController+Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A4D8862678F24300A794E7 /* ConversationViewController+Delegates.swift */; };
|
||||
34A4D8892679296600A794E7 /* ConversationViewController+UIScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A4D8882679296600A794E7 /* ConversationViewController+UIScrollView.swift */; };
|
||||
34A4D88B26795AF800A794E7 /* ConversationViewController+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A4D88A26795AF800A794E7 /* ConversationViewController+UI.swift */; };
|
||||
34A4D88D267A363A00A794E7 /* ConversationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A4D88C267A363A00A794E7 /* ConversationViewController.swift */; };
|
||||
34A4D88F267A402100A794E7 /* ConversationViewController+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A4D88E267A402100A794E7 /* ConversationViewController+Notifications.swift */; };
|
||||
34A6C28021E503E700B5B12E /* OWSImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */; };
|
||||
34A8B3512190A40E00218A25 /* CVMediaAlbumView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A8B3502190A40E00218A25 /* CVMediaAlbumView.swift */; };
|
||||
34AC09DD211B39B100997B47 /* ViewControllerUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 34AC09BF211B39AE00997B47 /* ViewControllerUtils.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
@ -1397,6 +1399,8 @@
|
||||
34A4D8862678F24300A794E7 /* ConversationViewController+Delegates.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConversationViewController+Delegates.swift"; sourceTree = "<group>"; };
|
||||
34A4D8882679296600A794E7 /* ConversationViewController+UIScrollView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConversationViewController+UIScrollView.swift"; sourceTree = "<group>"; };
|
||||
34A4D88A26795AF800A794E7 /* ConversationViewController+UI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConversationViewController+UI.swift"; sourceTree = "<group>"; };
|
||||
34A4D88C267A363A00A794E7 /* ConversationViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationViewController.swift; sourceTree = "<group>"; };
|
||||
34A4D88E267A402100A794E7 /* ConversationViewController+Notifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConversationViewController+Notifications.swift"; sourceTree = "<group>"; };
|
||||
34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSImagePickerController.swift; sourceTree = "<group>"; };
|
||||
34A8B3502190A40E00218A25 /* CVMediaAlbumView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CVMediaAlbumView.swift; sourceTree = "<group>"; };
|
||||
34AC09BF211B39AE00997B47 /* ViewControllerUtils.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ViewControllerUtils.h; sourceTree = "<group>"; };
|
||||
@ -2546,6 +2550,7 @@
|
||||
343A65961FC4CFE6000477A1 /* ConversationScrollButton.m */,
|
||||
34D1F06D1F8678AA0066283D /* ConversationViewController.h */,
|
||||
34D1F06E1F8678AA0066283D /* ConversationViewController.m */,
|
||||
34A4D88C267A363A00A794E7 /* ConversationViewController.swift */,
|
||||
3405C06D25435048008B24EF /* ConversationViewController+Banners.swift */,
|
||||
34E93F9126668F7500CE6FA5 /* ConversationViewController+BodyTextItems.swift */,
|
||||
347342F62548587800D440CD /* ConversationViewController+BottomBar.swift */,
|
||||
@ -2561,6 +2566,7 @@
|
||||
3426A367255D7BCA0036407F /* ConversationViewController+MessageActionsDelegate.swift */,
|
||||
3457811A23EB56B300CE01C3 /* ConversationViewController+MessageRequest.swift */,
|
||||
34A4D8822677C59300A794E7 /* ConversationViewController+Misc.swift */,
|
||||
34A4D88E267A402100A794E7 /* ConversationViewController+Notifications.swift */,
|
||||
4CD675C422E7CF22008010D2 /* ConversationViewController+OWS.swift */,
|
||||
88FE237D249C22080041670F /* ConversationViewController+Scroll.swift */,
|
||||
3426A369255D9D7C0036407F /* ConversationViewController+Selection.swift */,
|
||||
@ -5601,6 +5607,7 @@
|
||||
88A4CC19246CE6810082211F /* DeviceTransferInitialViewController.swift in Sources */,
|
||||
3406D32425DD4A8F00885B14 /* PaymentsQRScanViewController.swift in Sources */,
|
||||
8835DE01230DBF7E00DC6B66 /* ComposeViewController.swift in Sources */,
|
||||
34A4D88D267A363A00A794E7 /* ConversationViewController.swift in Sources */,
|
||||
3495FF0F25F9538900959D6E /* PaymentsViewPassphraseSplashViewController.swift in Sources */,
|
||||
3406D32225DD40F000885B14 /* PaymentsTransferOutViewController.swift in Sources */,
|
||||
34A17D81253F7237009F8C02 /* ConversationSettingsViewController+LegacyGroups.swift in Sources */,
|
||||
@ -5872,6 +5879,7 @@
|
||||
32ECD08A24BFBF9500EDDED0 /* ContactSupportViewController.swift in Sources */,
|
||||
3448E15E221333F5004B052E /* OnboardingController.swift in Sources */,
|
||||
88A4CC10246CE2760082211F /* TransferProgressView.swift in Sources */,
|
||||
34A4D88F267A402100A794E7 /* ConversationViewController+Notifications.swift in Sources */,
|
||||
32E958AA25C12B3800BF12AD /* AnimatedProgressView.swift in Sources */,
|
||||
3470C8802555F25200F5847C /* CVContactShareView.swift in Sources */,
|
||||
32B3286524C6957B00E4F974 /* ComposeSupportEmailOperation.swift in Sources */,
|
||||
|
||||
@ -46,8 +46,6 @@ struct CVUpdateToken {
|
||||
let lastMessageForInboxSortId: UInt64?
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
@objc
|
||||
public class CVLoadCoordinator: NSObject {
|
||||
|
||||
@ -87,17 +85,12 @@ public class CVLoadCoordinator: NSObject {
|
||||
|
||||
private var loadDidLandResolver: Resolver<Void>?
|
||||
|
||||
required init(delegate: CVLoadCoordinatorDelegate,
|
||||
componentDelegate: CVComponentDelegate,
|
||||
conversationStyle: ConversationStyle,
|
||||
focusMessageIdOnOpen: String?) {
|
||||
self.delegate = delegate
|
||||
self.componentDelegate = componentDelegate
|
||||
self.viewState = delegate.viewState
|
||||
required init(viewState: CVViewState) {
|
||||
self.viewState = viewState
|
||||
let threadViewModel = viewState.threadViewModel
|
||||
self.threadUniqueId = threadViewModel.threadRecord.uniqueId
|
||||
self.thread = threadViewModel.threadRecord
|
||||
self.conversationStyle = conversationStyle
|
||||
self.conversationStyle = viewState.conversationStyle
|
||||
|
||||
let viewStateSnapshot = CVViewStateSnapshot.snapshot(viewState: viewState,
|
||||
typingIndicatorsSender: nil,
|
||||
@ -108,6 +101,13 @@ public class CVLoadCoordinator: NSObject {
|
||||
self.messageMapping = CVMessageMapping(thread: threadViewModel.threadRecord)
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
func configure(delegate: CVLoadCoordinatorDelegate,
|
||||
componentDelegate: CVComponentDelegate,
|
||||
focusMessageIdOnOpen: String?) {
|
||||
self.delegate = delegate
|
||||
self.componentDelegate = componentDelegate
|
||||
|
||||
Self.databaseStorage.appendUIDatabaseSnapshotDelegate(self)
|
||||
|
||||
|
||||
@ -467,7 +467,7 @@ private class NameCollisionBanner: UIView {
|
||||
imageName: "x-circle-16",
|
||||
tintColor: Theme.secondaryTextAndIconColor)
|
||||
button.accessibilityLabel = NSLocalizedString("BANNER_CLOSE_ACCESSIBILITY_LABEL",
|
||||
comment: "Accessibility label for banner close button")
|
||||
comment: "Accessibility label for banner close button")
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.setCompressionResistanceHigh()
|
||||
button.setContentHuggingHigh()
|
||||
|
||||
@ -180,7 +180,7 @@ public extension ConversationViewController {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let messageDraft = (Self.databaseStorage.uiRead { transaction in
|
||||
self.thread.currentDraft(with: transaction)
|
||||
self.thread.currentDraft(with: transaction)
|
||||
}) else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -86,15 +86,6 @@ extension ConversationViewController: CVLoadCoordinatorDelegate {
|
||||
updateConversationStyle()
|
||||
}
|
||||
|
||||
@objc
|
||||
public func buildLoadCoordinator(conversationStyle: ConversationStyle,
|
||||
focusMessageIdOnOpen: String?) -> CVLoadCoordinator {
|
||||
CVLoadCoordinator(delegate: self,
|
||||
componentDelegate: self,
|
||||
conversationStyle: conversationStyle,
|
||||
focusMessageIdOnOpen: focusMessageIdOnOpen)
|
||||
}
|
||||
|
||||
func willUpdateWithNewRenderState(_ renderState: CVRenderState) -> CVUpdateToken {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
@ -240,7 +231,7 @@ extension ConversationViewController: CVLoadCoordinatorDelegate {
|
||||
DispatchQueue.main.async {
|
||||
let benchSteps = BenchSteps()
|
||||
Self.databaseStorage.uiRead { transaction in
|
||||
self.reloadReactionsDetailSheet(with: transaction)
|
||||
self.reloadReactionsDetailSheet(transaction: transaction)
|
||||
self.updateUnreadMessageFlag(transaction: transaction)
|
||||
}
|
||||
if hasViewDidAppearEverCompleted {
|
||||
|
||||
@ -293,9 +293,9 @@ extension ConversationViewController: ConversationInputToolbarDelegate {
|
||||
|
||||
let didShowSNAlert = self.showSafetyNumberConfirmationIfNecessary(
|
||||
confirmationText: SafetyNumberStrings.confirmSendButton) { [weak self] didConfirmIdentity in
|
||||
if didConfirmIdentity {
|
||||
if didConfirmIdentity {
|
||||
self?.tryToSendAttachments(attachments, messageBody: messageBody)
|
||||
}
|
||||
}
|
||||
}
|
||||
if didShowSNAlert {
|
||||
return
|
||||
|
||||
@ -119,7 +119,7 @@ extension ConversationViewController: ContactsPickerDelegate {
|
||||
contactShareRecord.isProfileAvatar = isProfileAvatar
|
||||
|
||||
let contactShare = ContactShareViewModel(contactShareRecord: contactShareRecord,
|
||||
avatarImageData: avatarImageData)
|
||||
avatarImageData: avatarImageData)
|
||||
|
||||
let approveContactShare = ContactShareApprovalViewController(contactShare: contactShare)
|
||||
approveContactShare.delegate = self
|
||||
@ -167,7 +167,7 @@ extension ConversationViewController: ContactShareApprovalViewControllerDelegate
|
||||
private func send(contactShare: ContactShareViewModel) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
Logger.verbose("Sending contact share.")
|
||||
Logger.verbose("Sending contact share.")
|
||||
|
||||
let thread = self.thread
|
||||
Self.databaseStorage.asyncWrite { transaction in
|
||||
@ -212,7 +212,7 @@ extension ConversationViewController: ContactShareApprovalViewControllerDelegate
|
||||
public func contactApprovalRecipientsDescription(_ contactApproval: ContactShareApprovalViewController) -> String? {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
Logger.info("")
|
||||
Logger.info("")
|
||||
|
||||
return databaseStorage.read { transaction in
|
||||
Self.contactsManager.displayName(for: self.thread, transaction: transaction)
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@ -134,4 +134,43 @@ extension ConversationViewController {
|
||||
// Check whether there is still a view item for this interaction.
|
||||
return self.indexPath(forInteractionUniqueId: messageActionInteractionId) == nil
|
||||
}
|
||||
|
||||
public func reloadReactionsDetailSheet(transaction: SDSAnyReadTransaction) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let reactionsDetailSheet = self.reactionsDetailSheet else {
|
||||
return
|
||||
}
|
||||
|
||||
let messageId = reactionsDetailSheet.messageId
|
||||
|
||||
guard let indexPath = self.indexPath(forInteractionUniqueId: messageId),
|
||||
let renderItem = self.renderItem(forIndex: indexPath.row) else {
|
||||
// The message no longer exists, dismiss the sheet.
|
||||
dismissReactionsDetailSheet(animated: true)
|
||||
return
|
||||
}
|
||||
guard let reactionState = renderItem.reactionState,
|
||||
reactionState.hasReactions else {
|
||||
// There are no longer reactions on this message, dismiss the sheet.
|
||||
dismissReactionsDetailSheet(animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
// Update the detail sheet with the latest reaction
|
||||
// state, in case the reactions have changed.
|
||||
reactionsDetailSheet.setReactionState(reactionState, transaction: transaction)
|
||||
}
|
||||
|
||||
public func dismissReactionsDetailSheet(animated: Bool) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let reactionsDetailSheet = self.reactionsDetailSheet else {
|
||||
return
|
||||
}
|
||||
|
||||
reactionsDetailSheet.dismiss(animated: animated) {
|
||||
self.reactionsDetailSheet = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -238,14 +238,14 @@ extension ConversationViewController: NameCollisionResolutionDelegate {
|
||||
let actionSheetMessage: String
|
||||
if thread.isGroupThread {
|
||||
actionSheetTitleFormat = NSLocalizedString("MESSAGE_REQUEST_BLOCK_GROUP_TITLE_FORMAT",
|
||||
comment: "Action sheet title to confirm blocking a group via a message request. Embeds {{group name}}")
|
||||
comment: "Action sheet title to confirm blocking a group via a message request. Embeds {{group name}}")
|
||||
actionSheetMessage = NSLocalizedString("MESSAGE_REQUEST_BLOCK_GROUP_MESSAGE",
|
||||
comment: "Action sheet message to confirm blocking a group via a message request.")
|
||||
comment: "Action sheet message to confirm blocking a group via a message request.")
|
||||
} else {
|
||||
actionSheetTitleFormat = NSLocalizedString("MESSAGE_REQUEST_BLOCK_CONVERSATION_TITLE_FORMAT",
|
||||
comment: "Action sheet title to confirm blocking a contact via a message request. Embeds {{contact name or phone number}}")
|
||||
comment: "Action sheet title to confirm blocking a contact via a message request. Embeds {{contact name or phone number}}")
|
||||
actionSheetMessage = NSLocalizedString("MESSAGE_REQUEST_BLOCK_CONVERSATION_MESSAGE",
|
||||
comment: "Action sheet message to confirm blocking a conversation via a message request.")
|
||||
comment: "Action sheet message to confirm blocking a conversation via a message request.")
|
||||
}
|
||||
|
||||
let threadName = contactsManager.displayNameWithSneakyTransaction(thread: thread)
|
||||
@ -253,9 +253,9 @@ extension ConversationViewController: NameCollisionResolutionDelegate {
|
||||
let actionSheet = ActionSheetController(title: actionSheetTitle, message: actionSheetMessage)
|
||||
|
||||
let blockActionTitle = NSLocalizedString("MESSAGE_REQUEST_BLOCK_ACTION",
|
||||
comment: "Action sheet action to confirm blocking a thread via a message request.")
|
||||
comment: "Action sheet action to confirm blocking a thread via a message request.")
|
||||
let blockAndDeleteActionTitle = NSLocalizedString("MESSAGE_REQUEST_BLOCK_AND_DELETE_ACTION",
|
||||
comment: "Action sheet action to confirm blocking and deleting a thread via a message request.")
|
||||
comment: "Action sheet action to confirm blocking and deleting a thread via a message request.")
|
||||
|
||||
actionSheet.addAction(ActionSheetAction(title: blockActionTitle) { [weak self] _ in
|
||||
self?.blockThread()
|
||||
@ -287,14 +287,14 @@ extension ConversationViewController: NameCollisionResolutionDelegate {
|
||||
actionSheetMessage = NSLocalizedString("MESSAGE_REQUEST_LEAVE_AND_DELETE_GROUP_MESSAGE",
|
||||
comment: "Action sheet message to confirm deleting a group via a message request.")
|
||||
confirmationText = NSLocalizedString("MESSAGE_REQUEST_LEAVE_AND_DELETE_GROUP_ACTION",
|
||||
comment: "Action sheet action to confirm deleting a group via a message request.")
|
||||
comment: "Action sheet action to confirm deleting a group via a message request.")
|
||||
} else { // either 1:1 thread, or a group of which I'm not a member
|
||||
actionSheetTitle = NSLocalizedString("MESSAGE_REQUEST_DELETE_CONVERSATION_TITLE",
|
||||
comment: "Action sheet title to confirm deleting a conversation via a message request.")
|
||||
actionSheetMessage = NSLocalizedString("MESSAGE_REQUEST_DELETE_CONVERSATION_MESSAGE",
|
||||
comment: "Action sheet message to confirm deleting a conversation via a message request.")
|
||||
confirmationText = NSLocalizedString("MESSAGE_REQUEST_DELETE_CONVERSATION_ACTION",
|
||||
comment: "Action sheet action to confirm deleting a conversation via a message request.")
|
||||
comment: "Action sheet action to confirm deleting a conversation via a message request.")
|
||||
}
|
||||
|
||||
let actionSheet = ActionSheetController(title: actionSheetTitle, message: actionSheetMessage)
|
||||
|
||||
@ -8,6 +8,26 @@ import ContactsUI
|
||||
@objc
|
||||
public extension ConversationViewController {
|
||||
|
||||
func updateV2GroupIfNecessary() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let groupThread = thread as? TSGroupThread,
|
||||
thread.isGroupV2Thread else {
|
||||
return
|
||||
}
|
||||
// Try to update the v2 group to latest from the service.
|
||||
// This will help keep us in sync if we've missed any group updates, etc.
|
||||
groupV2UpdatesObjc.tryToRefreshV2GroupUpToCurrentRevisionAfterMessageProcessingWithThrottling(groupThread)
|
||||
}
|
||||
|
||||
func presentAddThreadToProfileWhitelist(success: @escaping () -> Void) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
profileManagerImpl.presentAddThread(toProfileWhitelist: thread,
|
||||
from: self,
|
||||
success: success)
|
||||
}
|
||||
|
||||
func showUnblockConversationUI(completion: BlockActionCompletionBlock?) {
|
||||
self.userHasScrolled = false
|
||||
|
||||
@ -447,10 +467,10 @@ extension ConversationViewController {
|
||||
|
||||
readTimer?.invalidate()
|
||||
let readTimer = Timer.weakTimer(withTimeInterval: 0.1,
|
||||
target: self,
|
||||
selector: #selector(readTimerDidFire),
|
||||
userInfo: nil,
|
||||
repeats: true)
|
||||
target: self,
|
||||
selector: #selector(readTimerDidFire),
|
||||
userInfo: nil,
|
||||
repeats: true)
|
||||
self.readTimer = readTimer
|
||||
RunLoop.main.add(readTimer, forMode: .common)
|
||||
}
|
||||
@ -501,8 +521,8 @@ extension ConversationViewController {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
if isUserScrolling || !isViewCompletelyAppeared || !isViewVisible
|
||||
|| !CurrentAppContext().isAppForegroundAndActive() || !viewHasEverAppeared
|
||||
|| isPresentingMessageActions {
|
||||
|| !CurrentAppContext().isAppForegroundAndActive() || !viewHasEverAppeared
|
||||
|| isPresentingMessageActions {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,179 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension ConversationViewController {
|
||||
func addNotificationListeners() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(blockListDidChange),
|
||||
name: .blockListDidChange,
|
||||
object: nil)
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(windowManagerCallDidChange),
|
||||
name: .OWSWindowManagerCallDidChange,
|
||||
object: nil)
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(identityStateDidChange),
|
||||
name: .identityStateDidChange,
|
||||
object: nil)
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(didChangePreferredContentSize),
|
||||
name: UIContentSizeCategory.didChangeNotification,
|
||||
object: nil)
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(applicationWillEnterForeground),
|
||||
name: .OWSApplicationWillEnterForeground,
|
||||
object: nil)
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(applicationDidEnterBackground),
|
||||
name: .OWSApplicationDidEnterBackground,
|
||||
object: nil)
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(applicationWillResignActive),
|
||||
name: .OWSApplicationWillResignActive,
|
||||
object: nil)
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(applicationDidBecomeActive),
|
||||
name: .OWSApplicationDidBecomeActive,
|
||||
object: nil)
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(otherUsersProfileDidChange),
|
||||
name: .otherUsersProfileDidChange,
|
||||
object: nil)
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(profileWhitelistDidChange),
|
||||
name: .profileWhitelistDidChange,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func otherUsersProfileDidChange(_ notification: NSNotification) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
if let address = notification.userInfo?[kNSNotificationKey_ProfileAddress] as? SignalServiceAddress,
|
||||
address.isValid,
|
||||
thread.recipientAddresses.contains(address) {
|
||||
if thread is TSContactThread {
|
||||
// update title with profile name
|
||||
updateNavigationTitle()
|
||||
}
|
||||
|
||||
// Reload all cells if this is a group conversation,
|
||||
// since we may need to update the sender names on the messages.
|
||||
// Use a DebounceEvent to de-bounce.
|
||||
if isGroupConversation {
|
||||
otherUsersProfileDidChangeEvent?.requestNotify()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func profileWhitelistDidChange(_ notification: NSNotification) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
// If profile whitelist just changed, we may want to hide a profile whitelist offer.
|
||||
if let address = notification.userInfo?[kNSNotificationKey_ProfileAddress] as? SignalServiceAddress,
|
||||
address.isValid,
|
||||
thread.recipientAddresses.contains(address) {
|
||||
ensureBannerState()
|
||||
showMessageRequestDialogIfRequired()
|
||||
} else if let groupId = notification.userInfo?[kNSNotificationKey_ProfileGroupId] as? Data,
|
||||
!groupId.isEmpty,
|
||||
let groupThread = thread as? TSGroupThread,
|
||||
groupThread.groupModel.groupId == groupId {
|
||||
ensureBannerState()
|
||||
showMessageRequestDialogIfRequired()
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func blockListDidChange(_ notification: NSNotification) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
ensureBannerState()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func identityStateDidChange(_ notification: NSNotification) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
updateNavigationBarSubtitleLabel()
|
||||
ensureBannerState()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func windowManagerCallDidChange(_ notification: NSNotification) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
updateBarButtonItems()
|
||||
}
|
||||
|
||||
/**
|
||||
Called whenever the user manually changes the dynamic type options inside Settings.
|
||||
|
||||
@param notification NSNotification with the dynamic type change information.
|
||||
*/
|
||||
@objc
|
||||
private func didChangePreferredContentSize(_ notification: NSNotification) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
Logger.info("didChangePreferredContentSize")
|
||||
|
||||
resetForSizeOrOrientationChange()
|
||||
|
||||
guard hasViewWillAppearEverBegun else {
|
||||
return
|
||||
}
|
||||
guard let inputToolbar = inputToolbar else {
|
||||
owsFailDebug("Missing inputToolbar.")
|
||||
return
|
||||
}
|
||||
|
||||
inputToolbar.updateFontSizes()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func applicationWillEnterForeground(_ notification: NSNotification) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
startReadTimer()
|
||||
updateCellsVisible()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func applicationDidEnterBackground(_ notification: NSNotification) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
updateCellsVisible()
|
||||
mediaCache.removeAllObjects()
|
||||
cancelReadTimer()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func applicationWillResignActive(_ notification: NSNotification) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
finishRecordingVoiceMessage(sendImmediately: false)
|
||||
self.isUserScrolling = false
|
||||
self.isWaitingForDeceleration = false
|
||||
saveDraft()
|
||||
markVisibleMessagesAsRead()
|
||||
mediaCache.removeAllObjects()
|
||||
cancelReadTimer()
|
||||
dismissPresentedViewControllerIfNecessary()
|
||||
saveLastVisibleSortIdAndOnScreenPercentage()
|
||||
|
||||
self.dismissKeyBoard()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func applicationDidBecomeActive(_ notification: NSNotification) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
startReadTimer()
|
||||
}
|
||||
}
|
||||
@ -667,7 +667,7 @@ extension ConversationViewController {
|
||||
return CGPoint(x: 0, y: cellFrame.origin.y - messageActionsOriginalFocusY)
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
// MARK: -
|
||||
|
||||
private struct LastVisibleInteraction {
|
||||
public let interaction: TSInteraction
|
||||
@ -689,8 +689,8 @@ extension ConversationViewController {
|
||||
private func lastVisibleInteractionWithSneakyTransaction(thread: TSThread) -> LastVisibleInteraction? {
|
||||
databaseStorage.read { transaction in
|
||||
guard let lastVisibleInteraction = thread.lastVisibleInteraction(transaction: transaction),
|
||||
let interaction = thread.firstInteraction(atOrAroundSortId: lastVisibleInteraction.sortId,
|
||||
transaction: transaction) else {
|
||||
let interaction = thread.firstInteraction(atOrAroundSortId: lastVisibleInteraction.sortId,
|
||||
transaction: transaction) else {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -147,6 +147,10 @@ extension ConversationViewController {
|
||||
|
||||
@objc
|
||||
public func updateSelectionButtons() {
|
||||
guard let selectionToolbar = self.selectionToolbar else {
|
||||
owsFailDebug("Missing selectionToolbar.")
|
||||
return
|
||||
}
|
||||
guard let deleteButton = selectionToolbar.buttonItem(for: .delete) else {
|
||||
owsFailDebug("deleteButton was unexpectedly nil")
|
||||
return
|
||||
|
||||
@ -227,10 +227,10 @@ extension ConversationViewController {
|
||||
subtitleText.appendTemplatedImage(named: "timer-outline-16", font: subtitleFont)
|
||||
subtitleText.append(iconSpacer, attributes: attributes)
|
||||
subtitleText.append(NSString.formatDurationSeconds(
|
||||
disappearingMessagesConfiguration.durationSeconds,
|
||||
useShortFormat: true
|
||||
),
|
||||
attributes: attributes)
|
||||
disappearingMessagesConfiguration.durationSeconds,
|
||||
useShortFormat: true
|
||||
),
|
||||
attributes: attributes)
|
||||
}
|
||||
|
||||
if isVerified {
|
||||
@ -279,3 +279,78 @@ extension ConversationViewController {
|
||||
return inputToolbar
|
||||
}
|
||||
}
|
||||
// MARK: - Keyboard Shortcuts
|
||||
|
||||
public extension ConversationViewController {
|
||||
func focusInputToolbar() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard hasViewWillAppearEverBegun else {
|
||||
owsFailDebug("InputToolbar not yet ready.")
|
||||
return
|
||||
}
|
||||
guard let inputToolbar = inputToolbar else {
|
||||
owsFailDebug("Missing inputToolbar.")
|
||||
return
|
||||
}
|
||||
|
||||
inputToolbar.clearDesiredKeyboard()
|
||||
self.popKeyBoard()
|
||||
}
|
||||
|
||||
func openAllMedia() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard hasViewWillAppearEverBegun else {
|
||||
owsFailDebug("InputToolbar not yet ready.")
|
||||
return
|
||||
}
|
||||
|
||||
self.showConversationSettingsAndShowAllMedia()
|
||||
}
|
||||
|
||||
func openStickerKeyboard() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard hasViewWillAppearEverBegun else {
|
||||
owsFailDebug("InputToolbar not yet ready.")
|
||||
return
|
||||
}
|
||||
guard let inputToolbar = inputToolbar else {
|
||||
owsFailDebug("Missing inputToolbar.")
|
||||
return
|
||||
}
|
||||
|
||||
inputToolbar.showStickerKeyboard()
|
||||
}
|
||||
|
||||
func openAttachmentKeyboard() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard hasViewWillAppearEverBegun else {
|
||||
owsFailDebug("InputToolbar not yet ready.")
|
||||
return
|
||||
}
|
||||
guard let inputToolbar = inputToolbar else {
|
||||
owsFailDebug("Missing inputToolbar.")
|
||||
return
|
||||
}
|
||||
|
||||
inputToolbar.showAttachmentKeyboard()
|
||||
}
|
||||
|
||||
func openGifSearch() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard hasViewWillAppearEverBegun else {
|
||||
owsFailDebug("InputToolbar not yet ready.")
|
||||
return
|
||||
}
|
||||
guard nil != inputToolbar else {
|
||||
owsFailDebug("Missing inputToolbar.")
|
||||
return
|
||||
}
|
||||
|
||||
self.showGifPicker()
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,9 +51,9 @@ extension ConversationViewController {
|
||||
}
|
||||
|
||||
let scrollSpaceToBottom = (safeContentHeight + collectionView.contentInset.bottom
|
||||
- (collectionView.contentOffset.y + collectionView.frame.height))
|
||||
- (collectionView.contentOffset.y + collectionView.frame.height))
|
||||
let pageHeight = (collectionView.frame.height
|
||||
- (collectionView.contentInset.top + collectionView.contentInset.bottom))
|
||||
- (collectionView.contentInset.top + collectionView.contentInset.bottom))
|
||||
let isScrolledUpOnePage = scrollSpaceToBottom > pageHeight * 1.0
|
||||
|
||||
let hasLaterMessageOffscreen = (lastSortIdInLoadedWindow > lastVisibleSortId) || canLoadNewerItems
|
||||
@ -165,10 +165,10 @@ extension ConversationViewController: UIScrollViewDelegate {
|
||||
// We need to manually schedule this timer using NSRunLoopCommonModes
|
||||
// or it won't fire during scrolling.
|
||||
let scrollUpdateTimer = Timer.weakTimer(withTimeInterval: 0.1,
|
||||
target: self,
|
||||
selector: #selector(scrollUpdateTimerDidFire),
|
||||
userInfo: nil,
|
||||
repeats: false)
|
||||
target: self,
|
||||
selector: #selector(scrollUpdateTimerDidFire),
|
||||
userInfo: nil,
|
||||
repeats: false)
|
||||
self.scrollUpdateTimer = scrollUpdateTimer
|
||||
RunLoop.main.add(scrollUpdateTimer, forMode: .common)
|
||||
}
|
||||
@ -191,53 +191,53 @@ extension ConversationViewController: UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
// @available(iOS 2.0, *)
|
||||
// optional func scrollViewDidScroll(_ scrollView: UIScrollView) // any offset changes
|
||||
//
|
||||
// @available(iOS 3.2, *)
|
||||
// optional func scrollViewDidZoom(_ scrollView: UIScrollView) // any zoom scale changes
|
||||
//
|
||||
//
|
||||
// // called on start of dragging (may require some time and or distance to move)
|
||||
// @available(iOS 2.0, *)
|
||||
//
|
||||
// // called on finger up if the user dragged. velocity is in points/millisecond. targetContentOffset may be changed to adjust where the scroll view comes to rest
|
||||
// @available(iOS 5.0, *)
|
||||
// optional func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
|
||||
//
|
||||
// // called on finger up if the user dragged. decelerate is true if it will continue moving afterwards
|
||||
// @available(iOS 2.0, *)
|
||||
//
|
||||
//
|
||||
// @available(iOS 2.0, *)
|
||||
// optional func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) // called on finger up as we are moving
|
||||
//
|
||||
// @available(iOS 2.0, *)
|
||||
//
|
||||
//
|
||||
// @available(iOS 2.0, *)
|
||||
//
|
||||
//
|
||||
// @available(iOS 2.0, *)
|
||||
// optional func viewForZooming(in scrollView: UIScrollView) -> UIView? // return a view that will be scaled. if delegate returns nil, nothing happens
|
||||
//
|
||||
// @available(iOS 3.2, *)
|
||||
// optional func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) // called before the scroll view begins zooming its content
|
||||
//
|
||||
// @available(iOS 2.0, *)
|
||||
// optional func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) // scale between minimum and maximum. called after any 'bounce' animations
|
||||
//
|
||||
//
|
||||
// @available(iOS 2.0, *)
|
||||
//
|
||||
// @available(iOS 2.0, *)
|
||||
//
|
||||
//
|
||||
// /* Also see -[UIScrollView adjustedContentInsetDidChange]
|
||||
// */
|
||||
// @available(iOS 11.0, *)
|
||||
// optional func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView)
|
||||
//
|
||||
// @available(iOS 2.0, *)
|
||||
// optional func scrollViewDidScroll(_ scrollView: UIScrollView) // any offset changes
|
||||
//
|
||||
// @available(iOS 3.2, *)
|
||||
// optional func scrollViewDidZoom(_ scrollView: UIScrollView) // any zoom scale changes
|
||||
//
|
||||
//
|
||||
// // called on start of dragging (may require some time and or distance to move)
|
||||
// @available(iOS 2.0, *)
|
||||
//
|
||||
// // called on finger up if the user dragged. velocity is in points/millisecond. targetContentOffset may be changed to adjust where the scroll view comes to rest
|
||||
// @available(iOS 5.0, *)
|
||||
// optional func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>)
|
||||
//
|
||||
// // called on finger up if the user dragged. decelerate is true if it will continue moving afterwards
|
||||
// @available(iOS 2.0, *)
|
||||
//
|
||||
//
|
||||
// @available(iOS 2.0, *)
|
||||
// optional func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) // called on finger up as we are moving
|
||||
//
|
||||
// @available(iOS 2.0, *)
|
||||
//
|
||||
//
|
||||
// @available(iOS 2.0, *)
|
||||
//
|
||||
//
|
||||
// @available(iOS 2.0, *)
|
||||
// optional func viewForZooming(in scrollView: UIScrollView) -> UIView? // return a view that will be scaled. if delegate returns nil, nothing happens
|
||||
//
|
||||
// @available(iOS 3.2, *)
|
||||
// optional func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) // called before the scroll view begins zooming its content
|
||||
//
|
||||
// @available(iOS 2.0, *)
|
||||
// optional func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) // scale between minimum and maximum. called after any 'bounce' animations
|
||||
//
|
||||
//
|
||||
// @available(iOS 2.0, *)
|
||||
//
|
||||
// @available(iOS 2.0, *)
|
||||
//
|
||||
//
|
||||
// /* Also see -[UIScrollView adjustedContentInsetDidChange]
|
||||
// */
|
||||
// @available(iOS 11.0, *)
|
||||
// optional func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView)
|
||||
//
|
||||
@objc
|
||||
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
@ -2,12 +2,11 @@
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ConversationInputToolbar.h"
|
||||
#import <SignalMessaging/OWSViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSUInteger, ConversationViewAction) {
|
||||
@class ConversationViewController;
|
||||
|
||||
typedef NS_CLOSED_ENUM(NSUInteger, ConversationViewAction) {
|
||||
ConversationViewActionNone,
|
||||
ConversationViewActionCompose,
|
||||
ConversationViewActionAudioCall,
|
||||
@ -17,80 +16,17 @@ typedef NS_ENUM(NSUInteger, ConversationViewAction) {
|
||||
ConversationViewActionUpdateDraft
|
||||
};
|
||||
|
||||
@class CVLoadCoordinator;
|
||||
@class CVViewState;
|
||||
@class ConversationCollectionView;
|
||||
@class ConversationHeaderView;
|
||||
@class ConversationSearchController;
|
||||
@class ConversationStyle;
|
||||
@class ConversationViewLayout;
|
||||
@class MessageActionsToolbar;
|
||||
@class MessageBody;
|
||||
@class SDSAnyReadTransaction;
|
||||
@class SDSDatabaseStorage;
|
||||
@class SignalAttachment;
|
||||
@class TSMessage;
|
||||
@class TSThread;
|
||||
@class ThreadViewModel;
|
||||
void CVCReloadCollectionViewForReset(ConversationViewController *cvc);
|
||||
|
||||
@interface ConversationViewController : OWSViewController
|
||||
typedef void (^CVCPerformBatchUpdatesBlock)(void);
|
||||
typedef void (^CVCPerformBatchUpdatesCompletion)(BOOL);
|
||||
typedef void (^CVCPerformBatchUpdatesFailure)(void);
|
||||
|
||||
@property (nonatomic, readonly) CGFloat safeContentHeight;
|
||||
|
||||
@property (nonatomic, readonly) CVLoadCoordinator *loadCoordinator;
|
||||
|
||||
+ (instancetype)new NS_UNAVAILABLE;
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
- (instancetype)initWithThreadViewModel:(ThreadViewModel *)threadViewModel
|
||||
action:(ConversationViewAction)action
|
||||
focusMessageId:(nullable NSString *)focusMessageId NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
#pragma mark - Keyboard Shortcuts
|
||||
|
||||
- (void)focusInputToolbar;
|
||||
- (void)openAllMedia;
|
||||
- (void)openStickerKeyboard;
|
||||
- (void)openAttachmentKeyboard;
|
||||
- (void)openGifSearch;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - Internal Methods. Used in extensions
|
||||
|
||||
typedef NS_CLOSED_ENUM(NSUInteger, ConversationUIMode) {
|
||||
ConversationUIMode_Normal,
|
||||
ConversationUIMode_Search,
|
||||
ConversationUIMode_Selection
|
||||
};
|
||||
|
||||
@interface ConversationViewController (Internal)
|
||||
|
||||
@property (nonatomic, readonly) ConversationCollectionView *collectionView;
|
||||
@property (nonatomic, readonly) BOOL isViewVisible;
|
||||
@property (nonatomic, readonly) ConversationHeaderView *headerView;
|
||||
|
||||
@property (nonatomic, readonly) ConversationViewLayout *layout;
|
||||
@property (nonatomic, readonly) CVViewState *viewState;
|
||||
|
||||
// TODO: Remove or rework method.
|
||||
- (void)reloadCollectionViewForReset;
|
||||
|
||||
- (void)reloadReactionsDetailSheetWithTransaction:(SDSAnyReadTransaction *)transaction;
|
||||
- (void)performBatchUpdates:(void (^_Nonnull)(void))batchUpdates
|
||||
completion:(void (^_Nonnull)(BOOL))completion
|
||||
logFailureBlock:(void (^_Nonnull)(void))logFailureBlock
|
||||
shouldAnimateUpdates:(BOOL)shouldAnimateUpdates
|
||||
isLoadAdjacent:(BOOL)isLoadAdjacent;
|
||||
|
||||
#pragma mark - Search
|
||||
|
||||
@property (nonatomic, readonly) ConversationSearchController *searchController;
|
||||
|
||||
#pragma mark - Selection
|
||||
|
||||
@property (nonatomic, readonly) MessageActionsToolbar *selectionToolbar;
|
||||
|
||||
@end
|
||||
void CVCPerformBatchUpdates(ConversationViewController *cvc,
|
||||
CVCPerformBatchUpdatesBlock batchUpdates,
|
||||
CVCPerformBatchUpdatesCompletion completion,
|
||||
CVCPerformBatchUpdatesFailure logFailureBlock,
|
||||
BOOL shouldAnimateUpdates,
|
||||
BOOL isLoadAdjacent);
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,616 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc
|
||||
public enum ConversationUIMode: UInt {
|
||||
case normal
|
||||
case search
|
||||
case selection
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
@objc
|
||||
public class ConversationViewController: OWSViewController {
|
||||
|
||||
public let viewState: CVViewState
|
||||
public let loadCoordinator: CVLoadCoordinator
|
||||
@objc
|
||||
public let layout: ConversationViewLayout
|
||||
@objc
|
||||
public let collectionView: ConversationCollectionView
|
||||
public let searchController: ConversationSearchController
|
||||
|
||||
var selectionToolbar: MessageActionsToolbar?
|
||||
|
||||
var otherUsersProfileDidChangeEvent: DebouncedEvent?
|
||||
|
||||
// MARK: -
|
||||
|
||||
@objc
|
||||
public required init(threadViewModel: ThreadViewModel,
|
||||
action: ConversationViewAction = .none,
|
||||
focusMessageId: String? = nil) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
Logger.verbose("")
|
||||
|
||||
let conversationStyle = ConversationViewController.buildInitialConversationStyle(threadViewModel: threadViewModel)
|
||||
self.viewState = CVViewState(threadViewModel: threadViewModel,
|
||||
conversationStyle: conversationStyle)
|
||||
self.loadCoordinator = CVLoadCoordinator(viewState: viewState)
|
||||
self.layout = ConversationViewLayout(conversationStyle: conversationStyle)
|
||||
self.collectionView = ConversationCollectionView(frame: .zero,
|
||||
collectionViewLayout: self.layout)
|
||||
|
||||
self.searchController = ConversationSearchController(thread: threadViewModel.threadRecord)
|
||||
|
||||
super.init()
|
||||
|
||||
self.viewState.delegate = self
|
||||
|
||||
#if TESTABLE_BUILD
|
||||
self.initialLoadBenchSteps.step("Init CVC")
|
||||
#endif
|
||||
|
||||
self.inputAccessoryPlaceholder.delegate = self
|
||||
|
||||
// If we're not scrolling to a specific message AND we don't have
|
||||
// any unread messages, try to focus on the last visible interaction.
|
||||
var focusMessageId = focusMessageId
|
||||
if focusMessageId == nil, !threadViewModel.hasUnreadMessages {
|
||||
focusMessageId = self.lastVisibleInteractionIdWithSneakyTransaction(threadViewModel)
|
||||
}
|
||||
|
||||
contactsViewHelper.addObserver(self)
|
||||
contactShareViewHelper.delegate = self
|
||||
|
||||
self.actionOnOpen = action
|
||||
|
||||
self.recordInitialScrollState(focusMessageId)
|
||||
|
||||
loadCoordinator.configure(delegate: self,
|
||||
componentDelegate: self,
|
||||
focusMessageIdOnOpen: focusMessageId)
|
||||
|
||||
searchController.delegate = self
|
||||
|
||||
// because the search bar view is hosted in the navigation bar, it's not in the CVC's responder
|
||||
// chain, and thus won't inherit our inputAccessoryView, so we manually set it here.
|
||||
searchController.uiSearchController.searchBar.inputAccessoryView = self.inputAccessoryPlaceholder
|
||||
|
||||
self.otherUsersProfileDidChangeEvent = DebouncedEvent(maxFrequencySeconds: 1.0,
|
||||
onQueue: DispatchQueue.main) { [weak self] in
|
||||
// Reload all cells if this is a group conversation,
|
||||
// since we may need to update the sender names on the messages.
|
||||
self?.loadCoordinator.enqueueReload(canReuseInteractionModels: true,
|
||||
canReuseComponentStates: false)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
reloadTimer?.invalidate()
|
||||
scrollUpdateTimer?.invalidate()
|
||||
}
|
||||
|
||||
// MARK: - View Lifecycle
|
||||
|
||||
public override func viewDidLoad() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
// We won't have a navigation controller if we're presented in a preview
|
||||
owsAssertDebug(self.navigationController != nil || self.isInPreviewPlatter)
|
||||
|
||||
#if TESTABLE_BUILD
|
||||
initialLoadBenchSteps.step("viewDidLoad.1")
|
||||
#endif
|
||||
|
||||
super.viewDidLoad()
|
||||
|
||||
createContents()
|
||||
createConversationScrollButtons()
|
||||
createHeaderViews()
|
||||
addNotificationListeners()
|
||||
loadCoordinator.viewDidLoad()
|
||||
|
||||
self.startReloadTimer()
|
||||
|
||||
#if TESTABLE_BUILD
|
||||
initialLoadBenchSteps.step("viewDidLoad.2")
|
||||
#endif
|
||||
}
|
||||
|
||||
private func createContents() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
self.layout.delegate = self.loadCoordinator
|
||||
|
||||
// We use the root view bounds as the initial frame for the collection
|
||||
// view so that its contents can be laid out immediately.
|
||||
//
|
||||
// TODO: To avoid relayout, it'd be better to take into account safeAreaInsets,
|
||||
// but they're not yet set when this method is called.
|
||||
self.collectionView.frame = view.bounds
|
||||
self.collectionView.layoutDelegate = self
|
||||
self.collectionView.delegate = self.loadCoordinator
|
||||
self.collectionView.dataSource = self.loadCoordinator
|
||||
self.collectionView.showsVerticalScrollIndicator = true
|
||||
self.collectionView.showsHorizontalScrollIndicator = false
|
||||
self.collectionView.keyboardDismissMode = .interactive
|
||||
self.collectionView.allowsMultipleSelection = true
|
||||
self.collectionView.backgroundColor = .clear
|
||||
|
||||
// To minimize time to initial apearance, we initially disable prefetching, but then
|
||||
// re-enable it once the view has appeared.
|
||||
self.collectionView.isPrefetchingEnabled = false
|
||||
|
||||
self.view.addSubview(self.collectionView)
|
||||
self.collectionView.autoPinEdge(toSuperviewEdge: .top)
|
||||
self.collectionView.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
self.collectionView.autoPinEdge(toSuperviewSafeArea: .leading)
|
||||
self.collectionView.autoPinEdge(toSuperviewSafeArea: .trailing)
|
||||
|
||||
// TODO: Do we still need to do this?
|
||||
self.collectionView.applyInsetsFix()
|
||||
self.collectionView.accessibilityIdentifier = "collectionView"
|
||||
|
||||
self.registerReuseIdentifiers()
|
||||
|
||||
// The view controller will only automatically adjust content insets for a
|
||||
// scrollView at index 0, so we need the collection view to remain subview index 0.
|
||||
// But the background views should appear visually behind the collection view.
|
||||
let backgroundContainer = self.backgroundContainer
|
||||
backgroundContainer.delegate = self
|
||||
self.view.addSubview(backgroundContainer)
|
||||
backgroundContainer.autoPinEdgesToSuperviewEdges()
|
||||
setupWallpaper()
|
||||
|
||||
self.view.addSubview(bottomBar)
|
||||
self.bottomBarBottomConstraint = bottomBar.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
bottomBar.autoPinWidthToSuperview()
|
||||
|
||||
self.selectionToolbar = self.buildSelectionToolbar()
|
||||
|
||||
// This should kick off the first load.
|
||||
owsAssertDebug(!self.hasRenderState)
|
||||
owsAssertDebug(!self.loadCoordinator.hasLoadInFlight)
|
||||
self.updateConversationStyle()
|
||||
}
|
||||
|
||||
public override var canBecomeFirstResponder: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public override func becomeFirstResponder() -> Bool {
|
||||
let result = super.becomeFirstResponder()
|
||||
|
||||
guard hasViewWillAppearEverBegun else {
|
||||
return result
|
||||
}
|
||||
guard let inputToolbar = inputToolbar else {
|
||||
owsFailDebug("Missing inputToolbar.")
|
||||
return result
|
||||
}
|
||||
|
||||
// If we become the first responder, it means that the
|
||||
// input toolbar is not the first responder. As such,
|
||||
// we should clear out the desired keyboard since an
|
||||
// interactive dismissal may have just occured and we
|
||||
// need to update the UI to reflect that fact. We don't
|
||||
// actually ever want to be the first responder, so resign
|
||||
// immediately. We just want to know when the responder
|
||||
// state of our children changed and that information is
|
||||
// conveniently bubbled up the responder chain.
|
||||
if result {
|
||||
self.resignFirstResponder()
|
||||
inputToolbar.clearDesiredKeyboard()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public override var inputAccessoryView: UIView? {
|
||||
inputAccessoryPlaceholder
|
||||
}
|
||||
|
||||
public override var textInputContextIdentifier: String? {
|
||||
thread.uniqueId
|
||||
}
|
||||
|
||||
public func dismissPresentedViewControllerIfNecessary() {
|
||||
guard let presentedViewController = self.presentedViewController else {
|
||||
Logger.verbose("presentedViewController was nil")
|
||||
return
|
||||
}
|
||||
|
||||
if presentedViewController is ActionSheetController ||
|
||||
presentedViewController is UIAlertController {
|
||||
Logger.verbose("Dismissing presentedViewController: \(type(of: presentedViewController))")
|
||||
dismiss(animated: false, completion: nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
public override func viewWillAppear(_ animated: Bool) {
|
||||
self.viewWillAppearDidBegin()
|
||||
|
||||
#if TESTABLE_BUILD
|
||||
initialLoadBenchSteps.step("viewWillAppear.1")
|
||||
#endif
|
||||
|
||||
Logger.verbose("viewWillAppear")
|
||||
|
||||
self.ensureBannerState()
|
||||
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
if self.inputToolbar == nil {
|
||||
// This will create the input toolbar for the first time.
|
||||
// It's important that we do this at the "last moment" to
|
||||
// avoid expensive work that delays CVC presentation.
|
||||
self.applyTheme()
|
||||
owsAssertDebug(self.inputToolbar != nil)
|
||||
|
||||
self.createGestureRecognizers()
|
||||
}
|
||||
|
||||
self.isViewVisible = true
|
||||
self.viewWillAppearForLoad()
|
||||
|
||||
// We should have already requested contact access at this point, so this should be a no-op
|
||||
// unless it ever becomes possible to load this VC without going via the ConversationListViewController.
|
||||
self.contactsManagerImpl.requestSystemContactsOnce()
|
||||
|
||||
self.updateBarButtonItems()
|
||||
self.updateNavigationTitle()
|
||||
|
||||
// One-time work performed the first time we enter the view.
|
||||
if !self.viewHasEverAppeared {
|
||||
BenchManager.completeEvent(eventId: String(format: "presenting-conversation-\(thread.uniqueId)"))
|
||||
}
|
||||
self.ensureBottomViewType()
|
||||
self.updateInputToolbarLayout()
|
||||
self.refreshCallState()
|
||||
|
||||
self.showMessageRequestDialogIfRequired()
|
||||
self.viewWillAppearDidComplete()
|
||||
#if TESTABLE_BUILD
|
||||
initialLoadBenchSteps.step("viewWillAppear.2")
|
||||
#endif
|
||||
}
|
||||
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
self.viewDidAppearDidBegin()
|
||||
|
||||
#if TESTABLE_BUILD
|
||||
initialLoadBenchSteps.step("viewDidAppear.1")
|
||||
#endif
|
||||
Logger.verbose("viewDidAppear")
|
||||
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
// We don't present incoming message notifications for the presented
|
||||
// conversation. But there's a narrow window *while* the conversationVC
|
||||
// is being presented where a message notification for the not-quite-yet
|
||||
// presented conversation can be shown. If that happens, dismiss it as soon
|
||||
// as we enter the conversation.
|
||||
self.notificationPresenter.cancelNotifications(threadId: thread.uniqueId)
|
||||
|
||||
// recover status bar when returning from PhotoPicker, which is dark (uses light status bar)
|
||||
self.setNeedsStatusBarAppearanceUpdate()
|
||||
|
||||
self.markVisibleMessagesAsRead()
|
||||
self.startReadTimer()
|
||||
self.updateNavigationBarSubtitleLabel()
|
||||
_ = self.autoLoadMoreIfNecessary()
|
||||
if !DebugFlags.reduceLogChatter {
|
||||
self.bulkProfileFetch.fetchProfiles(thread: thread)
|
||||
self.updateV2GroupIfNecessary()
|
||||
}
|
||||
|
||||
if !self.viewHasEverAppeared {
|
||||
// To minimize time to initial apearance, we initially disable prefetching, but then
|
||||
// re-enable it once the view has appeared.
|
||||
self.collectionView.isPrefetchingEnabled = true
|
||||
}
|
||||
|
||||
self.isViewCompletelyAppeared = true
|
||||
self.shouldAnimateKeyboardChanges = true
|
||||
|
||||
switch self.actionOnOpen {
|
||||
case .none:
|
||||
break
|
||||
case .compose:
|
||||
// Don't pop the keyboard if we have a pending message request, since
|
||||
// the user can't currently send a message until acting on this
|
||||
if nil == requestView {
|
||||
self.popKeyBoard()
|
||||
}
|
||||
case .audioCall:
|
||||
self.startIndividualAudioCall()
|
||||
case .videoCall:
|
||||
self.startIndividualVideoCall()
|
||||
case .groupCallLobby:
|
||||
self.showGroupLobbyOrActiveCall()
|
||||
case .newGroupActionSheet:
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.showGroupLinkPromotionActionSheet()
|
||||
}
|
||||
case .updateDraft:
|
||||
// Do nothing input toolbar was just created with the latest draft.
|
||||
break
|
||||
}
|
||||
|
||||
scrollToInitialPosition(animated: false)
|
||||
if viewState.hasAppliedFirstLoad {
|
||||
self.clearInitialScrollState()
|
||||
}
|
||||
|
||||
// Clear the "on open" state after the view has been presented.
|
||||
self.actionOnOpen = .none
|
||||
|
||||
self.updateInputToolbarLayout()
|
||||
self.configureScrollDownButtons()
|
||||
inputToolbar?.viewDidAppear()
|
||||
|
||||
if !self.viewState.hasTriedToMigrateGroup {
|
||||
self.viewState.hasTriedToMigrateGroup = true
|
||||
|
||||
if !DebugFlags.reduceLogChatter {
|
||||
GroupsV2Migration.autoMigrateThreadIfNecessary(thread: thread)
|
||||
}
|
||||
}
|
||||
|
||||
self.viewDidAppearDidComplete()
|
||||
#if TESTABLE_BUILD
|
||||
initialLoadBenchSteps.step("viewDidAppear.2")
|
||||
#endif
|
||||
}
|
||||
|
||||
// `viewWillDisappear` is called whenever the view *starts* to disappear,
|
||||
// but, as is the case with the "pan left for message details view" gesture,
|
||||
// this can be canceled. As such, we shouldn't tear down anything expensive
|
||||
// until `viewDidDisappear`.
|
||||
public override func viewWillDisappear(_ animated: Bool) {
|
||||
Logger.verbose("")
|
||||
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
self.isViewCompletelyAppeared = false
|
||||
|
||||
dismissMessageActions(animated: false)
|
||||
self.dismissReactionsDetailSheet(animated: false)
|
||||
self.saveLastVisibleSortIdAndOnScreenPercentage()
|
||||
}
|
||||
|
||||
public override func viewDidDisappear(_ animated: Bool) {
|
||||
Logger.verbose("")
|
||||
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
self.userHasScrolled = false
|
||||
self.isViewVisible = false
|
||||
self.shouldAnimateKeyboardChanges = false
|
||||
|
||||
self.cvAudioPlayer.stopAll()
|
||||
|
||||
self.cancelReadTimer()
|
||||
self.saveDraft()
|
||||
self.markVisibleMessagesAsRead()
|
||||
self.finishRecordingVoiceMessage(sendImmediately: false)
|
||||
self.mediaCache.removeAllObjects()
|
||||
inputToolbar?.clearDesiredKeyboard()
|
||||
|
||||
self.isUserScrolling = false
|
||||
self.isWaitingForDeceleration = false
|
||||
|
||||
self.scrollingAnimationCompletionTimer?.invalidate()
|
||||
self.scrollingAnimationCompletionTimer = nil
|
||||
}
|
||||
|
||||
public override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
guard hasViewWillAppearEverBegun else {
|
||||
return
|
||||
}
|
||||
guard nil != inputToolbar else {
|
||||
owsFailDebug("Missing inputToolbar.")
|
||||
return
|
||||
}
|
||||
|
||||
// We resize the inputToolbar whenever it's text is modified, including when setting saved draft-text.
|
||||
// However it's possible this draft-text is set before the inputToolbar (an inputAccessoryView) is mounted
|
||||
// in the view hierarchy. Since it's not in the view hierarchy, it hasn't been laid out and has no width,
|
||||
// which is used to determine height.
|
||||
// So here we unsure the proper height once we know everything's been layed out.
|
||||
self.inputToolbar?.ensureTextViewHeight()
|
||||
|
||||
self.positionGroupCallTooltip()
|
||||
}
|
||||
|
||||
public override var shouldAutorotate: Bool {
|
||||
// Don't allow orientation changes while recording voice messages.
|
||||
if let currentVoiceMessageModel = viewState.currentVoiceMessageModel,
|
||||
currentVoiceMessageModel.isRecording {
|
||||
return false
|
||||
}
|
||||
|
||||
return super.shouldAutorotate
|
||||
}
|
||||
|
||||
public override func themeDidChange() {
|
||||
super.themeDidChange()
|
||||
|
||||
self.updateThemeIfNecessary()
|
||||
}
|
||||
|
||||
private func updateThemeIfNecessary() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
if self.isDarkThemeEnabled == Theme.isDarkThemeEnabled {
|
||||
return
|
||||
}
|
||||
self.isDarkThemeEnabled = Theme.isDarkThemeEnabled
|
||||
|
||||
self.applyTheme()
|
||||
|
||||
self.updateConversationStyle()
|
||||
}
|
||||
|
||||
public override func applyTheme() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
super.applyTheme()
|
||||
|
||||
guard hasViewWillAppearEverBegun else {
|
||||
owsFailDebug("Not yet ready.")
|
||||
return
|
||||
}
|
||||
|
||||
// make sure toolbar extends below iPhoneX home button.
|
||||
self.view.backgroundColor = Theme.toolbarBackgroundColor
|
||||
|
||||
self.updateWallpaperView()
|
||||
|
||||
self.updateNavigationTitle()
|
||||
self.updateNavigationBarSubtitleLabel()
|
||||
|
||||
self.updateInputToolbar()
|
||||
self.updateInputToolbarLayout()
|
||||
self.updateBarButtonItems()
|
||||
self.ensureBannerState()
|
||||
|
||||
// Re-styling the message actions is tricky,
|
||||
// since this happens rarely just dismiss
|
||||
dismissMessageActions(animated: false)
|
||||
dismissReactionsDetailSheet(animated: false)
|
||||
}
|
||||
|
||||
func reloadCollectionViewForReset() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard hasAppearedAndHasAppliedFirstLoad else {
|
||||
return
|
||||
}
|
||||
// We use an obj-c free function so that we can handle NSException.
|
||||
CVCReloadCollectionViewForReset(self)
|
||||
}
|
||||
|
||||
var isViewVisible: Bool {
|
||||
get { viewState.isViewVisible }
|
||||
set {
|
||||
viewState.isViewVisible = newValue
|
||||
|
||||
updateCellsVisible()
|
||||
}
|
||||
}
|
||||
|
||||
func updateCellsVisible() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let isAppInBackground = CurrentAppContext().isInBackground()
|
||||
let isCellVisible = self.isViewVisible && !isAppInBackground
|
||||
for cell in self.collectionView.visibleCells {
|
||||
guard let cell = cell as? CVCell else {
|
||||
owsFailDebug("Invalid cell.")
|
||||
continue
|
||||
}
|
||||
cell.isCellVisible = isCellVisible
|
||||
}
|
||||
self.updateScrollingContent()
|
||||
}
|
||||
|
||||
// MARK: - CollectionView updates
|
||||
|
||||
func performBatchUpdates(_ batchUpdates: @escaping () -> Void,
|
||||
completion: @escaping (Bool) -> Void,
|
||||
logFailureBlock: @escaping () -> Void,
|
||||
shouldAnimateUpdates: Bool,
|
||||
isLoadAdjacent: Bool) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
// We use an obj-c free function so that we can handle NSException.
|
||||
CVCPerformBatchUpdates(self,
|
||||
batchUpdates,
|
||||
completion,
|
||||
logFailureBlock,
|
||||
shouldAnimateUpdates,
|
||||
isLoadAdjacent)
|
||||
}
|
||||
|
||||
// MARK: - Orientation
|
||||
|
||||
public override func viewWillTransition(to size: CGSize,
|
||||
with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
|
||||
dismissMessageActions(animated: false)
|
||||
dismissReactionsDetailSheet(animated: false)
|
||||
|
||||
self.scrollContinuity = .bottom
|
||||
|
||||
guard hasAppearedAndHasAppliedFirstLoad else {
|
||||
return
|
||||
}
|
||||
|
||||
self.setScrollActionForSizeTransition()
|
||||
|
||||
_ = coordinator.animate(
|
||||
alongsideTransition: { _ in
|
||||
},
|
||||
completion: { [weak self] _ in
|
||||
self?.clearScrollActionForSizeTransition()
|
||||
})
|
||||
}
|
||||
|
||||
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
self.updateBarButtonItems()
|
||||
self.updateNavigationBarSubtitleLabel()
|
||||
|
||||
// Invoking -ensureBannerState synchronously can lead to reenterant updates to the
|
||||
// trait collection while building the banners. This can lead us to blow out the stack
|
||||
// on unrelated trait collection changes (e.g. rotating to landscape).
|
||||
// We workaround this by just asyncing any banner updates to break the synchronous
|
||||
// dependency chain.
|
||||
DispatchQueue.main.async {
|
||||
self.ensureBannerState()
|
||||
}
|
||||
}
|
||||
|
||||
public override func viewSafeAreaInsetsDidChange() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
super.viewSafeAreaInsetsDidChange()
|
||||
|
||||
updateContentInsets(animated: false)
|
||||
self.updateInputToolbarLayout()
|
||||
self.viewSafeAreaInsetsDidChangeForLoad()
|
||||
self.updateConversationStyle()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
// TODO: Is this necessary?
|
||||
extension ConversationViewController: UINavigationControllerDelegate {
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
extension ConversationViewController: ContactsViewHelperObserver {
|
||||
public func contactsViewHelperDidUpdateContacts() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
self.updateNavigationTitle()
|
||||
loadCoordinator.enqueueReload(canReuseInteractionModels: true,
|
||||
canReuseComponentStates: false)
|
||||
}
|
||||
}
|
||||
@ -20,7 +20,8 @@ public protocol InputAccessoryViewPlaceholderDelegate: AnyObject {
|
||||
/// presenting view.
|
||||
@objc
|
||||
public class InputAccessoryViewPlaceholder: UIView {
|
||||
@objc weak var delegate: InputAccessoryViewPlaceholderDelegate?
|
||||
@objc
|
||||
public weak var delegate: InputAccessoryViewPlaceholderDelegate?
|
||||
|
||||
/// The amount of the application frame that is overlapped
|
||||
/// by the keyboard.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user