Port CVC to Swift.

This commit is contained in:
Matthew Chen 2021-06-16 13:21:32 -03:00
parent be48bb75ca
commit c093739463
21 changed files with 1085 additions and 1150 deletions

View File

@ -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 */,

View File

@ -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)

View File

@ -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()

View File

@ -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
}

View File

@ -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 {

View File

@ -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

View File

@ -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)

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation

View File

@ -1,5 +1,5 @@
//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//
import Foundation

View File

@ -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
}
}
}

View File

@ -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)

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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()
}
}

View File

@ -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()

View File

@ -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

View File

@ -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)
}
}

View File

@ -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.