From 2af858c5292381e187448b533d82191807016c89 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 17 Jan 2018 14:11:55 -0500 Subject: [PATCH] Add message approval view. --- Signal.xcodeproj/project.pbxproj | 28 +- .../MessageApprovalViewController.swift | 572 ++++++++++++++++++ .../SharingThreadPickerViewController.m | 122 ++-- 3 files changed, 661 insertions(+), 61 deletions(-) create mode 100644 SignalMessaging/attachments/MessageApprovalViewController.swift diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index 966fe1434..b13eb6afe 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -44,6 +44,10 @@ 344F248B20069F0600CFB4F4 /* ViewControllerUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 344F248920069F0600CFB4F4 /* ViewControllerUtils.m */; }; 344F248D2007CCD600CFB4F4 /* DisplayableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344F248C2007CCD600CFB4F4 /* DisplayableText.swift */; }; 344F248F2007D7F200CFB4F4 /* OWSMessagesBubbleImageFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344F248E2007D7F200CFB4F4 /* OWSMessagesBubbleImageFactory.swift */; }; + 344F2499200FD03300CFB4F4 /* SharingThreadPickerViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 344F2495200FD03200CFB4F4 /* SharingThreadPickerViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 344F249A200FD03300CFB4F4 /* MessageApprovalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344F2496200FD03200CFB4F4 /* MessageApprovalViewController.swift */; }; + 344F249B200FD03300CFB4F4 /* SharingThreadPickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 344F2497200FD03200CFB4F4 /* SharingThreadPickerViewController.m */; }; + 344F249C200FD03300CFB4F4 /* AttachmentApprovalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 344F2498200FD03200CFB4F4 /* AttachmentApprovalViewController.swift */; }; 3461284B1FD0B94000532771 /* SAELoadViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3461284A1FD0B93F00532771 /* SAELoadViewController.swift */; }; 346129341FD1A88700532771 /* OWSSwiftUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346129331FD1A88700532771 /* OWSSwiftUtils.swift */; }; 346129391FD1B47300532771 /* OWSPreferences.h in Headers */ = {isa = PBXBuildFile; fileRef = 346129371FD1B47200532771 /* OWSPreferences.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -214,8 +218,6 @@ 45194F951FD7216600333B2C /* TSUnreadIndicatorInteraction.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C42D651F4734ED0072EC04 /* TSUnreadIndicatorInteraction.m */; }; 45194F961FD7226300333B2C /* SelectThreadViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 3400C7941EAF99F4008A8584 /* SelectThreadViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 451A13B11E13DED2000A50FD /* CallNotificationsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451A13B01E13DED2000A50FD /* CallNotificationsAdapter.swift */; }; - 451F8A311FD70DE9005CB9DA /* SharingThreadPickerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3400C7911EAF89CD008A8584 /* SharingThreadPickerViewController.m */; }; - 451F8A321FD70DFA005CB9DA /* SharingThreadPickerViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 3400C7901EAF89CD008A8584 /* SharingThreadPickerViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 451F8A331FD71083005CB9DA /* SelectThreadViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3400C7951EAF99F4008A8584 /* SelectThreadViewController.m */; }; 451F8A341FD710C3005CB9DA /* ConversationSearcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451777C71FD61554001225FF /* ConversationSearcher.swift */; }; 451F8A351FD710DE005CB9DA /* Searcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45360B8C1F9521F800FA666C /* Searcher.swift */; }; @@ -302,7 +304,6 @@ 45CD81EF1DC030E7004C9430 /* SyncPushTokensJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45CD81EE1DC030E7004C9430 /* SyncPushTokensJob.swift */; }; 45D231771DC7E8F10034FA89 /* SessionResetJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D231761DC7E8F10034FA89 /* SessionResetJob.swift */; }; 45DF5DF21DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45DF5DF11DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift */; }; - 45E547201FD755E700DFC09E /* AttachmentApprovalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E5471F1FD755E700DFC09E /* AttachmentApprovalViewController.swift */; }; 45E5A6991F61E6DE001E4A8A /* MarqueeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */; }; 45E7A6A81E71CA7E00D44FB5 /* DisplayableTextFilterTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45E7A6A61E71CA7E00D44FB5 /* DisplayableTextFilterTest.swift */; }; 45F170AC1E2F0351003FC1F2 /* CallAudioSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45F170AB1E2F0351003FC1F2 /* CallAudioSession.swift */; }; @@ -470,8 +471,6 @@ 1C93CF3971B64E8B6C1F9AC1 /* Pods-SignalShareExtension.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.test.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.test.xcconfig"; sourceTree = ""; }; 1CE3CD5C23334683BDD3D78C /* Pods-Signal.test.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Signal.test.xcconfig"; path = "Pods/Target Support Files/Pods-Signal/Pods-Signal.test.xcconfig"; sourceTree = ""; }; 264242150E87D10A357DB07B /* Pods_SignalMessaging.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_SignalMessaging.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 3400C7901EAF89CD008A8584 /* SharingThreadPickerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SharingThreadPickerViewController.h; sourceTree = ""; }; - 3400C7911EAF89CD008A8584 /* SharingThreadPickerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SharingThreadPickerViewController.m; sourceTree = ""; }; 3400C7941EAF99F4008A8584 /* SelectThreadViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SelectThreadViewController.h; sourceTree = ""; }; 3400C7951EAF99F4008A8584 /* SelectThreadViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SelectThreadViewController.m; sourceTree = ""; }; 3400C7971EAFB772008A8584 /* ThreadViewHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ThreadViewHelper.h; sourceTree = ""; }; @@ -527,6 +526,10 @@ 344F248920069F0600CFB4F4 /* ViewControllerUtils.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ViewControllerUtils.m; path = SignalMessaging/contacts/ViewControllerUtils.m; sourceTree = SOURCE_ROOT; }; 344F248C2007CCD600CFB4F4 /* DisplayableText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayableText.swift; sourceTree = ""; }; 344F248E2007D7F200CFB4F4 /* OWSMessagesBubbleImageFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSMessagesBubbleImageFactory.swift; sourceTree = ""; }; + 344F2495200FD03200CFB4F4 /* SharingThreadPickerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SharingThreadPickerViewController.h; path = SignalMessaging/attachments/SharingThreadPickerViewController.h; sourceTree = SOURCE_ROOT; }; + 344F2496200FD03200CFB4F4 /* MessageApprovalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = MessageApprovalViewController.swift; path = SignalMessaging/attachments/MessageApprovalViewController.swift; sourceTree = SOURCE_ROOT; }; + 344F2497200FD03200CFB4F4 /* SharingThreadPickerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SharingThreadPickerViewController.m; path = SignalMessaging/attachments/SharingThreadPickerViewController.m; sourceTree = SOURCE_ROOT; }; + 344F2498200FD03200CFB4F4 /* AttachmentApprovalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AttachmentApprovalViewController.swift; path = SignalMessaging/attachments/AttachmentApprovalViewController.swift; sourceTree = SOURCE_ROOT; }; 34533F161EA8D2070006114F /* OWSAudioAttachmentPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSAudioAttachmentPlayer.h; sourceTree = ""; }; 34533F171EA8D2070006114F /* OWSAudioAttachmentPlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAudioAttachmentPlayer.m; sourceTree = ""; }; 3461284A1FD0B93F00532771 /* SAELoadViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SAELoadViewController.swift; sourceTree = ""; }; @@ -847,7 +850,6 @@ 45E282DE1D08E67800ADD4C8 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = translations/gl.lproj/Localizable.strings; sourceTree = ""; }; 45E282DF1D08E6CC00ADD4C8 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = translations/id.lproj/Localizable.strings; sourceTree = ""; }; 45E2E91E1E13EE3500457AA0 /* OWSCallNotificationsAdaptee.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; name = OWSCallNotificationsAdaptee.h; path = UserInterface/OWSCallNotificationsAdaptee.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; - 45E5471F1FD755E700DFC09E /* AttachmentApprovalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentApprovalViewController.swift; sourceTree = ""; }; 45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarqueeLabel.swift; sourceTree = ""; }; 45E7A6A61E71CA7E00D44FB5 /* DisplayableTextFilterTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayableTextFilterTest.swift; sourceTree = ""; }; 45F170AB1E2F0351003FC1F2 /* CallAudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallAudioSession.swift; sourceTree = ""; }; @@ -1274,15 +1276,19 @@ 346129DB1FD5C02900532771 /* viewControllers */ = { isa = PBXGroup; children = ( + 344F2498200FD03200CFB4F4 /* AttachmentApprovalViewController.swift */, 344F248220069E9B00CFB4F4 /* CountryCodeViewController.h */, 344F248320069E9B00CFB4F4 /* CountryCodeViewController.m */, 346129DC1FD5C02900532771 /* LockInteractionController.h */, 346129DD1FD5C02900532771 /* LockInteractionController.m */, + 344F2496200FD03200CFB4F4 /* MessageApprovalViewController.swift */, 344F248620069ECB00CFB4F4 /* ModalActivityIndicatorViewController.swift */, 344D6CE920069E070042AF96 /* NewNonContactConversationViewController.h */, 344D6CE820069E070042AF96 /* NewNonContactConversationViewController.m */, 344D6CE620069E060042AF96 /* SelectRecipientViewController.h */, 344D6CE720069E060042AF96 /* SelectRecipientViewController.m */, + 344F2495200FD03200CFB4F4 /* SharingThreadPickerViewController.h */, + 344F2497200FD03200CFB4F4 /* SharingThreadPickerViewController.m */, 344F248820069F0600CFB4F4 /* ViewControllerUtils.h */, 344F248920069F0600CFB4F4 /* ViewControllerUtils.m */, ); @@ -1570,9 +1576,6 @@ children = ( 34B3F8391E8DF1700035BE1A /* AttachmentSharing.h */, 34B3F83A1E8DF1700035BE1A /* AttachmentSharing.m */, - 45E5471F1FD755E700DFC09E /* AttachmentApprovalViewController.swift */, - 3400C7901EAF89CD008A8584 /* SharingThreadPickerViewController.h */, - 3400C7911EAF89CD008A8584 /* SharingThreadPickerViewController.m */, 34533F161EA8D2070006114F /* OWSAudioAttachmentPlayer.h */, 34533F171EA8D2070006114F /* OWSAudioAttachmentPlayer.m */, 34D913491F62D4A500722898 /* SignalAttachment.swift */, @@ -2057,7 +2060,6 @@ 451F8A3C1FD71392005CB9DA /* UIUtil.h in Headers */, 346129D61FD20ADC00532771 /* UIViewController+OWS.h in Headers */, 451F8A401FD7145D005CB9DA /* OWSTableViewController.h in Headers */, - 451F8A321FD70DFA005CB9DA /* SharingThreadPickerViewController.h in Headers */, 3461296F1FD1D74C00532771 /* Release.h in Headers */, 34612A061FD7238600532771 /* OWSContactsSyncing.h in Headers */, 34480B571FD0A7A400BC14EF /* OWSScrubbingLogFormatter.h in Headers */, @@ -2092,6 +2094,7 @@ 45194F901FD7200000333B2C /* ThreadUtil.h in Headers */, 346129CC1FD2072E00532771 /* NSAttributedString+OWS.h in Headers */, 346129FD1FD5F31400532771 /* OWS102MoveLoggingPreferenceToUserDefaults.h in Headers */, + 344F2499200FD03300CFB4F4 /* SharingThreadPickerViewController.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2759,15 +2762,16 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 344F249B200FD03300CFB4F4 /* SharingThreadPickerViewController.m in Sources */, 45194F951FD7216600333B2C /* TSUnreadIndicatorInteraction.m in Sources */, 45BE4EA22012AD2000935E59 /* DisappearingTimerConfigurationView.swift in Sources */, 346129F71FD5F31400532771 /* OWS105AttachmentFilePaths.m in Sources */, 45194F931FD7215C00333B2C /* OWSContactOffersInteraction.m in Sources */, + 344F249A200FD03300CFB4F4 /* MessageApprovalViewController.swift in Sources */, 450998681FD8C0FF00D89EB3 /* AttachmentSharing.m in Sources */, 347850711FDAEB17007B8332 /* OWSUserProfile.m in Sources */, 346129761FD1E0B500532771 /* WeakTimer.swift in Sources */, 346129F81FD5F31400532771 /* OWS100RemoveTSRecipientsMigration.m in Sources */, - 45E547201FD755E700DFC09E /* AttachmentApprovalViewController.swift in Sources */, 346129B51FD1F7E800532771 /* OWSProfileManager.m in Sources */, 346129701FD1D74C00532771 /* Release.m in Sources */, 3478506C1FD9B78A007B8332 /* NoopNotificationsManager.swift in Sources */, @@ -2779,6 +2783,7 @@ 34480B671FD0AA9400BC14EF /* UIFont+OWS.m in Sources */, 346129E61FD5C0C600532771 /* OWSDatabaseMigrationRunner.m in Sources */, 346129AB1FD1F0EE00532771 /* OWSFormat.m in Sources */, + 344F249C200FD03300CFB4F4 /* AttachmentApprovalViewController.swift in Sources */, 451F8A461FD715BA005CB9DA /* OWSGroupAvatarBuilder.m in Sources */, 347850591FD9972E007B8332 /* SwiftSingletons.swift in Sources */, 344F248720069ECB00CFB4F4 /* ModalActivityIndicatorViewController.swift in Sources */, @@ -2833,7 +2838,6 @@ 3461293C1FD1D46A00532771 /* OWSMath.m in Sources */, 451F8A391FD711D6005CB9DA /* ContactsViewHelper.m in Sources */, 346129AF1FD1F5D900532771 /* SystemContactsFetcher.swift in Sources */, - 451F8A311FD70DE9005CB9DA /* SharingThreadPickerViewController.m in Sources */, 344F248B20069F0600CFB4F4 /* ViewControllerUtils.m in Sources */, 451F8A411FD714B8005CB9DA /* ContactTableViewCell.m in Sources */, 346129C81FD2072E00532771 /* NSAttributedString+OWS.m in Sources */, diff --git a/SignalMessaging/attachments/MessageApprovalViewController.swift b/SignalMessaging/attachments/MessageApprovalViewController.swift new file mode 100644 index 000000000..b8ee36659 --- /dev/null +++ b/SignalMessaging/attachments/MessageApprovalViewController.swift @@ -0,0 +1,572 @@ +// +// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// + +import Foundation +//import MediaPlayer + +@objc +public protocol MessageApprovalViewControllerDelegate: class { + func messageApproval(_ messageApproval: MessageApprovalViewController, didApproveMessage messageText: String) + func messageApprovalDidCancel(_ messageApproval: MessageApprovalViewController) +} + +@objc +public class MessageApprovalViewController: OWSViewController { + + let TAG = "[MessageApprovalViewController]" + weak var delegate: MessageApprovalViewControllerDelegate? + +// // We sometimes shrink the attachment view so that it remains somewhat visible +// // when the keyboard is presented. +// enum AttachmentViewScale { +// case fullsize, compact +// } + + // MARK: Properties + +// let attachment: SignalAttachment + let initialMessageText: String + +// private(set) var bottomToolbar: UIView! +// private(set) var mediaMessageView: MediaMessageView! +// private(set) var scrollView: UIScrollView! + private(set) var textView: UITextView! + + // MARK: Initializers + + @available(*, unavailable, message:"use attachment: constructor instead.") + required public init?(coder aDecoder: NSCoder) { + fatalError("unimplemented") + } + + @objc + required public init(messageText: String, delegate: MessageApprovalViewControllerDelegate) { + self.initialMessageText = messageText + self.delegate = delegate + + super.init(nibName: nil, bundle: nil) + } + + // MARK: View Lifecycle + + override public func viewDidLoad() { + super.viewDidLoad() + self.navigationItem.title = NSLocalizedString("MESSAGE_APPROVAL_DIALOG_TITLE", + comment: "Title for the 'message approval' dialog.") + } + +// override public func viewWillLayoutSubviews() { +// Logger.debug("\(logTag) in \(#function)") +// super.viewWillLayoutSubviews() +// +// // e.g. if flipping to/from landscape +// updateMinZoomScaleForSize(view.bounds.size) +// } + +// private func dialogTitle() -> String { +// guard let filename = mediaMessageView.formattedFileName() else { +// return NSLocalizedString("ATTACHMENT_APPROVAL_DIALOG_TITLE", +// comment: "Title for the 'attachment approval' dialog.") +// } +// return filename +// } + +// override public func viewWillAppear(_ animated: Bool) { +// Logger.debug("\(logTag) in \(#function)") +// super.viewWillAppear(animated) +// +// mediaMessageView.viewWillAppear(animated) +// } +// +// override public func viewDidAppear(_ animated: Bool) { +// Logger.debug("\(logTag) in \(#function)") +// super.viewDidAppear(animated) +// } +// +// override public func viewWillDisappear(_ animated: Bool) { +// Logger.debug("\(logTag) in \(#function)") +// super.viewWillDisappear(animated) +// +// mediaMessageView.viewWillDisappear(animated) +// } + + // MARK: - Create Views + + public override func loadView() { + + self.view = UIView() + +// self.mediaMessageView = MediaMessageView(attachment: attachment, mode: .messageApproval) + + textView = UITextView() + view.addSubview(textView) + textView.autoPinEdgesToSuperviewEdges() + +// // Scroll View - used to zoom/pan on images and video +// scrollView = UIScrollView() +// view.addSubview(scrollView) +// +// scrollView.delegate = self +// scrollView.showsHorizontalScrollIndicator = false +// scrollView.showsVerticalScrollIndicator = false +// +// // Panning should stop pretty soon after the user stops scrolling +// scrollView.decelerationRate = UIScrollViewDecelerationRateFast +// +// // We want scroll view content up and behind the system status bar content +// // but we want other content (e.g. bar buttons) to respect the top layout guide. +// self.automaticallyAdjustsScrollViewInsets = false +// +// scrollView.autoPinEdgesToSuperviewEdges() +// +// let defaultCaption = self.defaultCaption() +// let isTextualShare = defaultCaption != nil +// let backgroundColor = isTextualShare ? UIColor.ows_signalBrandBlue : UIColor.black +// self.view.backgroundColor = backgroundColor +// +// // Create full screen container view so the scrollView +// // can compute an appropriate content size in which to center +// // our media view. +// let containerView = UIView.container() +// scrollView.addSubview(containerView) +// containerView.autoPinEdgesToSuperviewEdges() +// containerView.autoMatch(.height, to: .height, of: self.view) +// containerView.autoMatch(.width, to: .width, of: self.view) +// +// containerView.addSubview(mediaMessageView) +// mediaMessageView.autoPinEdgesToSuperviewEdges() +// +// if isZoomable { +// // Add top and bottom gradients to ensure toolbar controls are legible +// // when placed over image/video preview which may be a clashing color. +// let topGradient = GradientView(from: backgroundColor, to: UIColor.clear) +// self.view.addSubview(topGradient) +// topGradient.autoPinWidthToSuperview() +// topGradient.autoPinEdge(toSuperviewEdge: .top) +// topGradient.autoSetDimension(.height, toSize: ScaleFromIPhone5(60)) +// } +// +// // Hide the play button embedded in the MediaView and replace it with our own. +// // This allows us to zoom in on the media view without zooming in on the button +// if attachment.isVideo { +// self.mediaMessageView.videoPlayButton?.isHidden = true +// let playButton = UIButton() +// playButton.accessibilityLabel = NSLocalizedString("PLAY_BUTTON_ACCESSABILITY_LABEL", comment: "accessability label for button to start media playback") +// playButton.setBackgroundImage(#imageLiteral(resourceName: "play_button"), for: .normal) +// playButton.contentMode = .scaleAspectFit +// +// let playButtonWidth = ScaleFromIPhone5(70) +// playButton.autoSetDimensions(to: CGSize(width: playButtonWidth, height: playButtonWidth)) +// self.view.addSubview(playButton) +// +// playButton.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside) +// playButton.autoCenterInSuperview() +// } +// +// // Top Toolbar +// let topToolbar = makeClearToolbar() +// +// self.view.addSubview(topToolbar) +// topToolbar.autoPinWidthToSuperview() +// topToolbar.autoPin(toTopLayoutGuideOf: self, withInset: 0) +// topToolbar.setContentHuggingVerticalHigh() +// topToolbar.setCompressionResistanceVerticalHigh() +// +// let cancelButton = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(cancelPressed)) +// cancelButton.tintColor = UIColor.white +// topToolbar.items = [cancelButton] +// +// // Bottom Toolbar +// let captioningToolbar = CaptioningToolbar(defaultCaption:defaultCaption) +// captioningToolbar.captioningToolbarDelegate = self +// self.bottomToolbar = captioningToolbar + } + +// private func defaultCaption() -> String? { +// guard self.attachment.isUrl || self.attachment.isText else { +// return nil +// } +// let data = self.attachment.data +// guard let messageText = String(data: data, encoding: String.Encoding.utf8) else { +// Logger.error("\(self.logTag) Couldn't load url or text string") +// return nil +// } +// return messageText +// } + +// override public var inputAccessoryView: UIView? { +// self.bottomToolbar.layoutIfNeeded() +// return self.bottomToolbar +// } + +// override public var canBecomeFirstResponder: Bool { +// return true +// } + +// private func makeClearToolbar() -> UIToolbar { +// let toolbar = UIToolbar() +// +// toolbar.backgroundColor = UIColor.clear +// +// // Making a toolbar transparent requires setting an empty uiimage +// toolbar.setBackgroundImage(UIImage(), forToolbarPosition: .any, barMetrics: .default) +// +// // hide 1px top-border +// toolbar.clipsToBounds = true +// +// return toolbar +// } + + // MARK: - Event Handlers + +// @objc +// public func playButtonTapped() { +// mediaMessageView.playVideo() +// } + + func cancelPressed(sender: UIButton) { + self.delegate?.messageApprovalDidCancel(self) + } + +// // MARK: CaptioningToolbarDelegate +// +// func captioningToolbarDidBeginEditing(_ captioningToolbar: CaptioningToolbar) { +// self.scaleAttachmentView(.compact) +// } +// +// func captioningToolbarDidEndEditing(_ captioningToolbar: CaptioningToolbar) { +// self.scaleAttachmentView(.fullsize) +// } +// +// func captioningToolbarDidTapSend(_ captioningToolbar: CaptioningToolbar, captionText: String?) { +// self.approveAttachment(captionText: captionText) +// } +// +// func captioningToolbar(_ captioningToolbar: CaptioningToolbar, didChangeTextViewHeight newHeight: CGFloat) { +// Logger.info("Changed height: \(newHeight)") +// } + + // MARK: Helpers + +// var isZoomable: Bool { +// return attachment.isImage || attachment.isVideo +// } +// +// private func approveAttachment(captionText: String?) { +// // Toolbar flickers in and out if there are errors +// // and remains visible momentarily after share extension is dismissed. +// // It's easiest to just hide it at this point since we're done with it. +// shouldAllowAttachmentViewResizing = false +// bottomToolbar.isUserInteractionEnabled = false +// bottomToolbar.isHidden = true +// +// attachment.captionText = captionText +// delegate?.messageApproval(self, didApproveAttachment: attachment) +// } +// +// // When the keyboard is popped, it can obscure the attachment view. +// // so we sometimes allow resizing the attachment. +// private var shouldAllowAttachmentViewResizing: Bool = true +// +// private func scaleAttachmentView(_ fit: AttachmentViewScale) { +// guard shouldAllowAttachmentViewResizing else { +// if self.scrollView.transform != CGAffineTransform.identity { +// UIView.animate(withDuration: 0.2) { +// self.scrollView.transform = CGAffineTransform.identity +// } +// } +// return +// } +// +// switch fit { +// case .fullsize: +// UIView.animate(withDuration: 0.2) { +// self.scrollView.transform = CGAffineTransform.identity +// } +// case .compact: +// UIView.animate(withDuration: 0.2) { +// let kScaleFactor: CGFloat = 0.7 +// let scale = CGAffineTransform(scaleX: kScaleFactor, y: kScaleFactor) +// +// let originalHeight = self.scrollView.bounds.size.height +// +// // Position the new scaled item to be centered with respect +// // to it's new size. +// let heightDelta = originalHeight * (1 - kScaleFactor) +// let translate = CGAffineTransform(translationX: 0, y: -heightDelta / 2) +// +// self.scrollView.transform = scale.concatenating(translate) +// } +// } +// } +} + +//extension MessageApprovalViewController: UIScrollViewDelegate { +// +// public func viewForZooming(in scrollView: UIScrollView) -> UIView? { +// if isZoomable { +// return mediaMessageView +// } else { +// // don't zoom for audio or generic attachments. +// return nil +// } +// } +// +// fileprivate func updateMinZoomScaleForSize(_ size: CGSize) { +// Logger.debug("\(logTag) in \(#function)") +// +// // Ensure bounds have been computed +// mediaMessageView.layoutIfNeeded() +// guard mediaMessageView.bounds.width > 0, mediaMessageView.bounds.height > 0 else { +// Logger.warn("\(logTag) bad bounds in \(#function)") +// return +// } +// +// let widthScale = size.width / mediaMessageView.bounds.width +// let heightScale = size.height / mediaMessageView.bounds.height +// let minScale = min(widthScale, heightScale) +// scrollView.maximumZoomScale = minScale * 5.0 +// scrollView.minimumZoomScale = minScale +// scrollView.zoomScale = minScale +// } +// +// // Keep the media view centered within the scroll view as you zoom +// public func scrollViewDidZoom(_ scrollView: UIScrollView) { +// // The scroll view has zoomed, so you need to re-center the contents +// let scrollViewSize = self.scrollViewVisibleSize +// +// // First assume that mediaMessageView center coincides with the contents center +// // This is correct when the mediaMessageView is bigger than scrollView due to zoom +// var contentCenter = CGPoint(x: (scrollView.contentSize.width / 2), y: (scrollView.contentSize.height / 2)) +// +// let scrollViewCenter = self.scrollViewCenter +// +// // if mediaMessageView is smaller than the scrollView visible size - fix the content center accordingly +// if self.scrollView.contentSize.width < scrollViewSize.width { +// contentCenter.x = scrollViewCenter.x +// } +// +// if self.scrollView.contentSize.height < scrollViewSize.height { +// contentCenter.y = scrollViewCenter.y +// } +// +// self.mediaMessageView.center = contentCenter +// } +// +// // return the scroll view center +// private var scrollViewCenter: CGPoint { +// let size = scrollViewVisibleSize +// return CGPoint(x: (size.width / 2), y: (size.height / 2)) +// } +// +// // Return scrollview size without the area overlapping with tab and nav bar. +// private var scrollViewVisibleSize: CGSize { +// let contentInset = scrollView.contentInset +// let scrollViewSize = scrollView.bounds.standardized.size +// let width = scrollViewSize.width - (contentInset.left + contentInset.right) +// let height = scrollViewSize.height - (contentInset.top + contentInset.bottom) +// return CGSize(width: width, height: height) +// } +//} +// +//private class GradientView: UIView { +// +// let gradientLayer = CAGradientLayer() +// +// required init(from fromColor: UIColor, to toColor: UIColor) { +// gradientLayer.colors = [fromColor.cgColor, toColor.cgColor] +// super.init(frame: CGRect.zero) +// +// self.layer.addSublayer(gradientLayer) +// } +// +// required init?(coder aDecoder: NSCoder) { +// fatalError("init(coder:) has not been implemented") +// } +// +// override func layoutSubviews() { +// super.layoutSubviews() +// gradientLayer.frame = self.bounds +// } +//} + +//protocol CaptioningToolbarDelegate: class { +// func captioningToolbarDidTapSend(_ captioningToolbar: CaptioningToolbar, captionText: String?) +// func captioningToolbar(_ captioningToolbar: CaptioningToolbar, didChangeTextViewHeight newHeight: CGFloat) +// func captioningToolbarDidBeginEditing(_ captioningToolbar: CaptioningToolbar) +// func captioningToolbarDidEndEditing(_ captioningToolbar: CaptioningToolbar) +//} + +//class CaptioningToolbar: UIView, UITextViewDelegate { +// +// weak var captioningToolbarDelegate: CaptioningToolbarDelegate? +// private let sendButton: UIButton +// private let textView: UITextView +// private let bottomGradient: GradientView +// +// // Layout Constants +// var maxTextViewHeight: CGFloat { +// // About ~4 lines in portrait and ~3 lines in landscape. +// // Otherwise we risk obscuring too much of the content. +// return UIDevice.current.orientation.isPortrait ? 160 : 100 +// } +// +// let kMinTextViewHeight: CGFloat = 38 +// var textViewHeight: CGFloat { +// didSet { +// self.captioningToolbarDelegate?.captioningToolbar(self, didChangeTextViewHeight: textViewHeight) +// } +// } +// +// required init?(coder aDecoder: NSCoder) { +// fatalError("init(coder:) has not been implemented") +// } +// +// class MessageTextView: UITextView { +// // When creating new lines, contentOffset is animated, but because because +// // we are simultaneously resizing the text view, this can cause the +// // text in the textview to be "too high" in the text view. +// // Solution is to disable animation for setting content offset. +// override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) { +// super.setContentOffset(contentOffset, animated: false) +// } +// } +// +// let kSendButtonShadowOffset: CGFloat = 1 +// init(defaultCaption: String?) { +// self.sendButton = UIButton(type: .system) +// self.bottomGradient = GradientView(from: UIColor.clear, to: UIColor.black) +// self.textView = MessageTextView() +// self.textViewHeight = kMinTextViewHeight +// +// super.init(frame: CGRect.zero) +// +// self.backgroundColor = UIColor.clear +// +// textView.delegate = self +// textView.backgroundColor = UIColor.white +// textView.layer.cornerRadius = 4.0 +// textView.addBorder(with: UIColor.lightGray) +// textView.font = UIFont.ows_dynamicTypeBody() +// textView.returnKeyType = .done +// if let defaultCaption = defaultCaption { +// textView.text = defaultCaption +// } +// +// let sendTitle = NSLocalizedString("ATTACHMENT_APPROVAL_SEND_BUTTON", comment: "Label for 'send' button in the 'attachment approval' dialog.") +// sendButton.setTitle(sendTitle, for: .normal) +// sendButton.addTarget(self, action: #selector(didTapSend), for: .touchUpInside) +// +// sendButton.titleLabel?.font = UIFont.ows_mediumFont(withSize: 16) +// sendButton.titleLabel?.textAlignment = .center +// sendButton.tintColor = UIColor.white +// sendButton.backgroundColor = UIColor.ows_systemPrimaryButton +// sendButton.layer.cornerRadius = 4 +// +// // Send Button Shadow - without this the send button bottom doesn't align with the toolbar. +// sendButton.layer.shadowColor = UIColor.darkGray.cgColor +// sendButton.layer.shadowOffset = CGSize(width: 0, height: kSendButtonShadowOffset) +// sendButton.layer.shadowOpacity = 0.8 +// sendButton.layer.shadowRadius = 0.0 +// sendButton.layer.masksToBounds = false +// +// // Increase hit area of send button +// sendButton.contentEdgeInsets = UIEdgeInsets(top: 6, left: 8, bottom: 6, right: 8) +// +// addSubview(bottomGradient) +// addSubview(sendButton) +// addSubview(textView) +// +// sendButton.sizeToFit() +// } +// +// func didTapSend() { +// self.captioningToolbarDelegate?.captioningToolbarDidTapSend(self, captionText: self.textView.text) +// } +// +// // MARK: - UIView Overrides +// +// // We do progammatic layout, explicitly computing and setting frames since autoLayout does +// // not seem to work with inputAccessory views, even when forcing a layout. +// override func layoutSubviews() { +// super.layoutSubviews() +// +// let kToolbarHMargin: CGFloat = 8 +// let kToolbarVMargin: CGFloat = 8 +// +// let sendButtonWidth = sendButton.frame.size.width +// +// let kOriginalToolbarHeight = kMinTextViewHeight + 2 * kToolbarVMargin +// // Assume send button has proper size. +// let textViewWidth = frame.size.width - 3 * kToolbarHMargin - sendButtonWidth +// +// // determine height given a fixed width +// let textViewHeight = clampedTextViewHeight(fixedWidth: textViewWidth) +// let newToolbarHeight = textViewHeight + 2 * kToolbarVMargin +// self.frame.size.height = newToolbarHeight +// let toolbarHeightOffset = newToolbarHeight - kOriginalToolbarHeight +// +// let textViewY = kToolbarVMargin - toolbarHeightOffset +// textView.frame = CGRect(x: kToolbarHMargin, y: textViewY, width: textViewWidth, height: textViewHeight) +// if (self.textViewHeight != textViewHeight) { +// // textViewHeight changed without textView's content changing, this can happen +// // when the user flips their device orientation after writing a caption. +// self.textViewHeight = textViewHeight +// } +// +// // Send Button +// +// // position in bottom right corner +// let sendButtonX = frame.size.width - kToolbarHMargin - sendButton.frame.size.width +// let sendButtonY = kOriginalToolbarHeight - kToolbarVMargin - sendButton.frame.size.height - kSendButtonShadowOffset +// sendButton.frame = CGRect(origin: CGPoint(x: sendButtonX, y: sendButtonY), size: sendButton.frame.size) +// sendButton.frame.size.height = kMinTextViewHeight - kSendButtonShadowOffset - textView.layer.borderWidth +// +// let bottomGradientHeight = ScaleFromIPhone5(100) +// let bottomGradientY = kOriginalToolbarHeight - bottomGradientHeight +// bottomGradient.frame = CGRect(x: 0, y: bottomGradientY, width: frame.size.width, height: bottomGradientHeight) +// } +// +// // MARK: - UITextViewDelegate +// +// public func textViewDidChange(_ textView: UITextView) { +// // compute new height assuming width is unchanged +// let currentSize = textView.frame.size +// let newHeight = clampedTextViewHeight(fixedWidth: currentSize.width) +// +// if newHeight != self.textViewHeight { +// Logger.debug("\(self.logTag) TextView height changed: \(self.textViewHeight) -> \(newHeight)") +// self.textViewHeight = newHeight +// self.setNeedsLayout() +// self.layoutIfNeeded() +// } +// } +// +// public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { +// // Though we can wrap the text, we don't want to encourage multline captions, plus a "done" button +// // allows the user to get the keyboard out of the way while in the attachment approval view. +// if text == "\n" { +// textView.resignFirstResponder() +// return false +// } else { +// return true +// } +// } +// +// public func textViewDidBeginEditing(_ textView: UITextView) { +// self.captioningToolbarDelegate?.captioningToolbarDidBeginEditing(self) +// } +// +// public func textViewDidEndEditing(_ textView: UITextView) { +// self.captioningToolbarDelegate?.captioningToolbarDidEndEditing(self) +// } +// +// // MARK: - Helpers +// +// private func clampedTextViewHeight(fixedWidth: CGFloat) -> CGFloat { +// let contentSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude)) +// return Clamp(contentSize.height, kMinTextViewHeight, maxTextViewHeight) +// } +// +//} diff --git a/SignalMessaging/attachments/SharingThreadPickerViewController.m b/SignalMessaging/attachments/SharingThreadPickerViewController.m index f1bf3d4e3..95dfeb38b 100644 --- a/SignalMessaging/attachments/SharingThreadPickerViewController.m +++ b/SignalMessaging/attachments/SharingThreadPickerViewController.m @@ -19,9 +19,11 @@ NS_ASSUME_NONNULL_BEGIN typedef void (^SendCompletionBlock)(NSError *_Nullable, TSOutgoingMessage *); +typedef void (^SendMessageBlock)(SendCompletionBlock completion); @interface SharingThreadPickerViewController () + AttachmentApprovalViewControllerDelegate, + MessageApprovalViewControllerDelegate> @property (nonatomic, readonly) OWSContactsManager *contactsManager; @property (nonatomic, readonly) OWSMessageSender *messageSender; @@ -122,16 +124,37 @@ typedef void (^SendCompletionBlock)(NSError *_Nullable, TSOutgoingMessage *); #pragma mark - SelectThreadViewControllerDelegate +// If the attachment is textual (e.g. text or URL), returns the message text +// for the attachment. Returns nil otherwise. +- (nullable NSString *)messageTextForAttachment +{ + if (!(self.attachment.isUrl || self.attachment.isText)) { + return nil; + } + NSData *data = self.attachment.data; + NSString *_Nullable messageText = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + return messageText; +} + - (void)threadWasSelected:(TSThread *)thread { OWSAssert(self.attachment); OWSAssert(thread); self.thread = thread; - AttachmentApprovalViewController *approvalVC = - [[AttachmentApprovalViewController alloc] initWithAttachment:self.attachment delegate:self]; + NSString *_Nullable messageText = [self messageTextForAttachment]; - [self.navigationController pushViewController:approvalVC animated:YES]; + if (messageText) { + MessageApprovalViewController *approvalVC = + [[MessageApprovalViewController alloc] initWithMessageText:messageText delegate:self]; + + [self.navigationController pushViewController:approvalVC animated:YES]; + } else { + AttachmentApprovalViewController *approvalVC = + [[AttachmentApprovalViewController alloc] initWithAttachment:self.attachment delegate:self]; + + [self.navigationController pushViewController:approvalVC animated:YES]; + } } - (void)didTapCancelShareButton @@ -147,11 +170,20 @@ typedef void (^SendCompletionBlock)(NSError *_Nullable, TSOutgoingMessage *); #pragma mark - AttachmentApprovalViewControllerDelegate -- (void)attachmentApproval:(AttachmentApprovalViewController *)attachmentApproval +- (void)attachmentApproval:(AttachmentApprovalViewController *)approvalViewController didApproveAttachment:(SignalAttachment *)attachment { [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread]; - [self tryToSendAttachment:attachment fromViewController:attachmentApproval]; + [self tryToSendMessageWithBlock:^(SendCompletionBlock sendCompletion) { + __block TSOutgoingMessage *outgoingMessage = nil; + outgoingMessage = [ThreadUtil sendMessageWithAttachment:attachment + inThread:self.thread + messageSender:self.messageSender + completion:^(NSError *_Nullable error) { + sendCompletion(error, outgoingMessage); + }]; + } + fromViewController:approvalViewController]; } - (void)attachmentApproval:(AttachmentApprovalViewController *)attachmentApproval @@ -160,15 +192,43 @@ typedef void (^SendCompletionBlock)(NSError *_Nullable, TSOutgoingMessage *); [self cancelShareExperience]; } -#pragma mark - Helpers +#pragma mark - MessageApprovalViewControllerDelegate -- (void)tryToSendAttachment:(SignalAttachment *)attachment fromViewController:(UIViewController *)fromViewController +- (void)messageApproval:(MessageApprovalViewController *)approvalViewController + didApproveMessage:(NSString *)messageText +{ + OWSAssert(messageText.length > 0); + + [ThreadUtil addThreadToProfileWhitelistIfEmptyContactThread:self.thread]; + [self tryToSendMessageWithBlock:^(SendCompletionBlock sendCompletion) { + __block TSOutgoingMessage *outgoingMessage = nil; + outgoingMessage = [ThreadUtil sendMessageWithText:messageText + inThread:self.thread + messageSender:self.messageSender + success:^{ + sendCompletion(nil, outgoingMessage); + } + failure:^(NSError *_Nonnull error) { + sendCompletion(error, outgoingMessage); + }]; + } + fromViewController:approvalViewController]; +} + +- (void)messageApprovalDidCancel:(MessageApprovalViewController *)approvalViewController +{ + [self cancelShareExperience]; +} + +#pragma mark - Helpers +typedef void (^SendMessageBlock)(SendCompletionBlock completion); + +- (void)tryToSendMessageWithBlock:(SendMessageBlock)sendMessageBlock + fromViewController:(UIViewController *)fromViewController { // Reset progress in case we're retrying self.progressView.progress = 0; - self.attachment = attachment; - NSString *progressTitle = NSLocalizedString(@"SHARE_EXTENSION_SENDING_IN_PROGRESS_TITLE", @"Alert title"); UIAlertController *progressAlert = [UIAlertController alertControllerWithTitle:progressTitle message:nil @@ -202,8 +262,7 @@ typedef void (^SendCompletionBlock)(NSError *_Nullable, TSOutgoingMessage *); [fromViewController dismissViewControllerAnimated:YES completion:^(void) { - DDLogInfo( - @"%@ Sending attachment failed with error: %@", self.logTag, error); + DDLogInfo(@"%@ Sending message failed with error: %@", self.logTag, error); [self showSendFailureAlertWithError:error message:message fromViewController:fromViewController]; @@ -211,7 +270,7 @@ typedef void (^SendCompletionBlock)(NSError *_Nullable, TSOutgoingMessage *); return; } - DDLogInfo(@"%@ Sending attachment succeeded.", self.logTag); + DDLogInfo(@"%@ Sending message succeeded.", self.logTag); [self.shareViewDelegate shareViewWasCompleted]; }); }; @@ -219,45 +278,10 @@ typedef void (^SendCompletionBlock)(NSError *_Nullable, TSOutgoingMessage *); [fromViewController presentViewController:progressAlert animated:YES completion:^(void) { - if ((self.attachment.isUrl || self.attachment.isText) - && self.attachment.captionText.length > 0) { - // Urls and text shares are added to the caption text, so discard - // the attachment and send the caption as a regular text message. - NSString *messageText = self.attachment.captionText; - [self sendAsTextMessage:messageText sendCompletion:sendCompletion]; - } else { - [self sendAsAttachmentMessage:sendCompletion]; - } + sendMessageBlock(sendCompletion); }]; } -- (void)sendAsTextMessage:(NSString *)messageText sendCompletion:(SendCompletionBlock)sendCompletion -{ - OWSAssert(messageText.length > 0); - - __block TSOutgoingMessage *outgoingMessage = nil; - outgoingMessage = [ThreadUtil sendMessageWithText:messageText - inThread:self.thread - messageSender:self.messageSender - success:^{ - sendCompletion(nil, outgoingMessage); - } - failure:^(NSError *_Nonnull error) { - sendCompletion(error, outgoingMessage); - }]; -} - -- (void)sendAsAttachmentMessage:(SendCompletionBlock)sendCompletion -{ - __block TSOutgoingMessage *outgoingMessage = nil; - outgoingMessage = [ThreadUtil sendMessageWithAttachment:self.attachment - inThread:self.thread - messageSender:self.messageSender - completion:^(NSError *_Nullable error) { - sendCompletion(error, outgoingMessage); - }]; -} - - (void)showSendFailureAlertWithError:(NSError *)error message:(TSOutgoingMessage *)message fromViewController:(UIViewController *)fromViewController