diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index a224cb1f3..221a6a919 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -778,6 +778,7 @@ FD705A90278CEBBC00F16121 /* ShareAppExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */; }; FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; }; FD705A94278D052B00F16121 /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */; }; + FD705A98278E9F4D00F16121 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1784,6 +1785,7 @@ FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppExtensionContext.swift; sourceTree = ""; }; FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; }; FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = ""; }; + FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = ""; }; FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -3321,6 +3323,7 @@ C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */, C38EF307255B6DBE007E1867 /* UIGestureRecognizer+OWS.swift */, C38EF2F3255B6DBC007E1867 /* UIImage+OWS.swift */, + FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */, C38EF30A255B6DBE007E1867 /* UIUtil.h */, C38EF300255B6DBD007E1867 /* UIUtil.m */, C38EF239255B6D66007E1867 /* UIFont+OWS.h */, @@ -4445,6 +4448,7 @@ C38EF248255B6D67007E1867 /* UIViewController+OWS.m in Sources */, C38EF272255B6D7A007E1867 /* OWSResaveCollectionDBMigration.m in Sources */, C33FDD3A255A582000E217F9 /* Notification+Loki.swift in Sources */, + FD705A98278E9F4D00F16121 /* UIColor+Extensions.swift in Sources */, C38EF276255B6D7A007E1867 /* OWSDatabaseMigration.m in Sources */, C38EF370255B6DCC007E1867 /* OWSNavigationController.m in Sources */, C38EF24E255B6D67007E1867 /* Collection+OWS.swift in Sources */, diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift index dea19759d..d249a63ef 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift @@ -7,7 +7,7 @@ import UIKit import AVFoundation import SessionUIKit -protocol AttachmentPrepViewControllerDelegate: class { +protocol AttachmentPrepViewControllerDelegate: AnyObject { func prepViewControllerUpdateNavigationBar() func prepViewControllerUpdateControls() @@ -31,13 +31,102 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD return attachmentItem.attachment } - private var videoPlayer: OWSVideoPlayer? + private lazy var videoPlayer: OWSVideoPlayer? = { + guard let videoURL = attachment.dataUrl else { + owsFailDebug("Missing videoURL") + return nil + } - private(set) var mediaMessageView: MediaMessageView! - private(set) var scrollView: UIScrollView! - private(set) var contentContainer: UIView! - private(set) var playVideoButton: UIView? - private var imageEditorView: ImageEditorView? + let player: OWSVideoPlayer = OWSVideoPlayer(url: videoURL) + player.delegate = self + + return player + }() + + // MARK: - UI + + private lazy var scrollView: UIScrollView = { + // Scroll View - used to zoom/pan on images and video + let scrollView: UIScrollView = UIScrollView() + scrollView.translatesAutoresizingMaskIntoConstraints = false + scrollView.delegate = self + scrollView.showsHorizontalScrollIndicator = false + scrollView.showsVerticalScrollIndicator = false + + // Panning should stop pretty soon after the user stops scrolling + scrollView.decelerationRate = UIScrollView.DecelerationRate.fast + + // If the content isn't zoomable then inset the content so it appears centered + if !isZoomable { + scrollView.isScrollEnabled = false + scrollView.contentInset = UIEdgeInsets( + top: 0, + leading: 0, + bottom: (AttachmentTextToolbar.kMinTextViewHeight + (AttachmentTextToolbar.kToolbarMargin * 2)), + trailing: 0 + ) + } + + return scrollView + }() + + private lazy var contentContainerView: UIView = { + // Anything that should be shrunk when user pops keyboard lives in the contentContainer. + let view: UIView = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() + + private lazy var mediaMessageView: MediaMessageView = { + let view: MediaMessageView = MediaMessageView(attachment: attachment, mode: .attachmentApproval) + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = (imageEditorView != nil) + + return view + }() + + private lazy var imageEditorView: ImageEditorView? = { + guard let imageEditorModel = attachmentItem.imageEditorModel else { return nil } + + let view: ImageEditorView = ImageEditorView(model: imageEditorModel, delegate: self) + view.translatesAutoresizingMaskIntoConstraints = false + + guard view.configureSubviews() else { return nil } + + return view + }() + + private lazy var videoPlayerView: VideoPlayerView? = { + guard let videoPlayer: OWSVideoPlayer = videoPlayer else { return nil } + + let view: VideoPlayerView = VideoPlayerView() + view.translatesAutoresizingMaskIntoConstraints = false + view.player = videoPlayer.avPlayer + + let pauseGesture = UITapGestureRecognizer(target: self, action: #selector(didTapPlayerView(_:))) + view.addGestureRecognizer(pauseGesture) + + return view + }() + + private lazy var progressBar: PlayerProgressBar = { + let progressBar: PlayerProgressBar = PlayerProgressBar() + progressBar.player = videoPlayer?.avPlayer + progressBar.delegate = self + + return progressBar + }() + + private lazy var playVideoButton: UIButton = { + let button: UIButton = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.contentMode = .scaleAspectFit + button.setBackgroundImage(#imageLiteral(resourceName: "CirclePlay"), for: .normal) + button.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside) + + return button + }() public var shouldHideControls: Bool { guard let imageEditorView = imageEditorView else { @@ -61,143 +150,120 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD } // MARK: - View Lifecycle - - override public func loadView() { - self.view = UIView() - - self.mediaMessageView = MediaMessageView(attachment: attachment, mode: .attachmentApproval) - - // Anything that should be shrunk when user pops keyboard lives in the contentContainer. - let contentContainer = UIView() - self.contentContainer = contentContainer - view.addSubview(contentContainer) - contentContainer.autoPinEdgesToSuperviewEdges() - - // Scroll View - used to zoom/pan on images and video - scrollView = UIScrollView() - contentContainer.addSubview(scrollView) - scrollView.delegate = self - scrollView.showsHorizontalScrollIndicator = false - scrollView.showsVerticalScrollIndicator = false - - // Panning should stop pretty soon after the user stops scrolling - scrollView.decelerationRate = UIScrollView.DecelerationRate.fast - - // 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 backgroundColor = Colors.navigationBarBackground - 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 let imageEditorModel = attachmentItem.imageEditorModel { - - let imageEditorView = ImageEditorView(model: imageEditorModel, delegate: self) - if imageEditorView.configureSubviews() { - self.imageEditorView = imageEditorView - - mediaMessageView.isHidden = true - - view.addSubview(imageEditorView) - imageEditorView.autoPinEdgesToSuperviewEdges() - - imageEditorUpdateNavigationBar() - } + + public override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = Colors.navigationBarBackground + + view.addSubview(contentContainerView) + + contentContainerView.addSubview(scrollView) + scrollView.addSubview(mediaMessageView) + + if let editorView: ImageEditorView = imageEditorView { + view.addSubview(editorView) + + imageEditorUpdateNavigationBar() } // 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 { - - guard let videoURL = attachment.dataUrl else { - owsFailDebug("Missing videoURL") - return - } - - let player = OWSVideoPlayer(url: videoURL) - self.videoPlayer = player - player.delegate = self - - let playerView = VideoPlayerView() - playerView.player = player.avPlayer - self.mediaMessageView.addSubview(playerView) - playerView.autoPinEdgesToSuperviewEdges() - - let pauseGesture = UITapGestureRecognizer(target: self, action: #selector(didTapPlayerView(_:))) - playerView.addGestureRecognizer(pauseGesture) - - let progressBar = PlayerProgressBar() - progressBar.player = player.avPlayer - progressBar.delegate = self - + // TODO: This for both Audio and Video? + if attachment.isVideo, let playerView: VideoPlayerView = videoPlayerView { + mediaMessageView.videoPlayButton.isHidden = true + mediaMessageView.addSubview(playerView) + // we don't want the progress bar to zoom during "pinch-to-zoom" // but we do want it to shrink with the media content when the user // pops the keyboard. - contentContainer.addSubview(progressBar) - - progressBar.autoPinEdge(.top, to: .top, of: view) - progressBar.autoPinWidthToSuperview() - progressBar.autoSetDimension(.height, toSize: 44) - - self.mediaMessageView.videoPlayButton?.isHidden = true - let playButton = UIButton() - self.playVideoButton = playButton - playButton.accessibilityLabel = NSLocalizedString("PLAY_BUTTON_ACCESSABILITY_LABEL", comment: "Accessibility label for button to start media playback") - playButton.setBackgroundImage(#imageLiteral(resourceName: "CirclePlay"), for: .normal) - playButton.contentMode = .scaleAspectFit - playButton.autoSetDimension(.width, toSize: 72) - playButton.autoSetDimension(.height, toSize: 72) - - let playButtonWidth = ScaleFromIPhone5(70) - playButton.autoSetDimensions(to: CGSize(width: playButtonWidth, height: playButtonWidth)) - self.contentContainer.addSubview(playButton) - - playButton.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside) - playButton.autoCenterInSuperview() + contentContainerView.addSubview(progressBar) + contentContainerView.addSubview(playVideoButton) } + + setupLayout() } override public func viewWillAppear(_ animated: Bool) { - Logger.debug("") - super.viewWillAppear(animated) - + prepDelegate?.prepViewControllerUpdateNavigationBar() prepDelegate?.prepViewControllerUpdateControls() } override public func viewDidAppear(_ animated: Bool) { - Logger.debug("") - super.viewDidAppear(animated) prepDelegate?.prepViewControllerUpdateNavigationBar() prepDelegate?.prepViewControllerUpdateControls() } - + override public func viewWillLayoutSubviews() { - Logger.debug("") super.viewWillLayoutSubviews() - - // e.g. if flipping to/from landscape - updateMinZoomScaleForSize(view.bounds.size) - + + setupZoomScale() ensureAttachmentViewScale(animated: false) } + + public override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + // Note: Need to do this here to ensure it's based on the final sizing + // otherwise the offsets will be slightly off + resetContentInset() + } + + // MARK: - Layout + + private func setupLayout() { + NSLayoutConstraint.activate([ + contentContainerView.topAnchor.constraint(equalTo: view.topAnchor), + contentContainerView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentContainerView.rightAnchor.constraint(equalTo: view.rightAnchor), + contentContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + scrollView.topAnchor.constraint(equalTo: contentContainerView.topAnchor), + scrollView.leftAnchor.constraint(equalTo: contentContainerView.leftAnchor), + scrollView.rightAnchor.constraint(equalTo: contentContainerView.rightAnchor), + scrollView.bottomAnchor.constraint(equalTo: contentContainerView.bottomAnchor), + + mediaMessageView.topAnchor.constraint(equalTo: scrollView.topAnchor), + mediaMessageView.leftAnchor.constraint(equalTo: scrollView.leftAnchor), + mediaMessageView.rightAnchor.constraint(equalTo: scrollView.rightAnchor), + mediaMessageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + mediaMessageView.widthAnchor.constraint(equalTo: view.widthAnchor), + mediaMessageView.heightAnchor.constraint(equalTo: view.heightAnchor) + ]) + + if let editorView: ImageEditorView = imageEditorView { + NSLayoutConstraint.activate([ + editorView.topAnchor.constraint(equalTo: view.topAnchor), + editorView.leftAnchor.constraint(equalTo: view.leftAnchor), + editorView.rightAnchor.constraint(equalTo: view.rightAnchor), + editorView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } + + if attachment.isVideo, let playerView: VideoPlayerView = videoPlayerView { + let playButtonSize: CGFloat = ScaleFromIPhone5(70) + + NSLayoutConstraint.activate([ + playerView.topAnchor.constraint(equalTo: mediaMessageView.topAnchor), + playerView.leftAnchor.constraint(equalTo: mediaMessageView.leftAnchor), + playerView.rightAnchor.constraint(equalTo: mediaMessageView.rightAnchor), + playerView.bottomAnchor.constraint(equalTo: mediaMessageView.bottomAnchor), + + progressBar.topAnchor.constraint(equalTo: view.topAnchor), + progressBar.widthAnchor.constraint(equalTo: contentContainerView.widthAnchor), + progressBar.heightAnchor.constraint(equalToConstant: 44), + + playVideoButton.centerXAnchor.constraint(equalTo: contentContainerView.centerXAnchor), + playVideoButton.centerYAnchor.constraint(equalTo: contentContainerView.centerYAnchor), + playVideoButton.widthAnchor.constraint(equalToConstant: playButtonSize), + playVideoButton.heightAnchor.constraint(equalToConstant: playButtonSize), + ]) + } + } // MARK: - Navigation Bar @@ -205,39 +271,33 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD guard let imageEditorView = imageEditorView else { return [] } + return imageEditorView.navigationBarItems() } // MARK: - Event Handlers - @objc - public func didTapPlayerView(_ gestureRecognizer: UIGestureRecognizer) { + @objc public func didTapPlayerView(_ gestureRecognizer: UIGestureRecognizer) { assert(self.videoPlayer != nil) self.pauseVideo() } - @objc - public func playButtonTapped() { + @objc public func playButtonTapped() { self.playVideo() } // MARK: - Video private func playVideo() { - Logger.info("") - guard let videoPlayer = self.videoPlayer else { owsFailDebug("video player was unexpectedly nil") return } - guard let playVideoButton = self.playVideoButton else { - owsFailDebug("playVideoButton was unexpectedly nil") - return - } - UIView.animate(withDuration: 0.1) { - playVideoButton.alpha = 0.0 + UIView.animate(withDuration: 0.1) { [weak self] in + self?.playVideoButton.alpha = 0.0 } + videoPlayer.play() } @@ -248,24 +308,15 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD } videoPlayer.pause() - guard let playVideoButton = self.playVideoButton else { - owsFailDebug("playVideoButton was unexpectedly nil") - return - } - UIView.animate(withDuration: 0.1) { - playVideoButton.alpha = 1.0 + + UIView.animate(withDuration: 0.1) { [weak self] in + self?.playVideoButton.alpha = 1.0 } } - @objc - public func videoPlayerDidPlayToCompletion(_ videoPlayer: OWSVideoPlayer) { - guard let playVideoButton = self.playVideoButton else { - owsFailDebug("playVideoButton was unexpectedly nil") - return - } - - UIView.animate(withDuration: 0.1) { - playVideoButton.alpha = 1.0 + @objc public func videoPlayerDidPlayToCompletion(_ videoPlayer: OWSVideoPlayer) { + UIView.animate(withDuration: 0.1) { [weak self] in + self?.playVideoButton.alpha = 1.0 } } @@ -274,6 +325,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD owsFailDebug("video player was unexpectedly nil") return } + videoPlayer.pause() } @@ -315,6 +367,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD var shouldAllowAttachmentViewResizing: Bool = true var attachmentViewScale: AttachmentViewScale = .fullsize + public func setAttachmentViewScale(_ attachmentViewScale: AttachmentViewScale, animated: Bool) { self.attachmentViewScale = attachmentViewScale ensureAttachmentViewScale(animated: animated) @@ -323,9 +376,9 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD func ensureAttachmentViewScale(animated: Bool) { let animationDuration = animated ? 0.2 : 0 guard shouldAllowAttachmentViewResizing else { - if self.contentContainer.transform != CGAffineTransform.identity { + if self.contentContainerView.transform != CGAffineTransform.identity { UIView.animate(withDuration: animationDuration) { - self.contentContainer.transform = CGAffineTransform.identity + self.contentContainerView.transform = CGAffineTransform.identity } } return @@ -333,14 +386,14 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD switch attachmentViewScale { case .fullsize: - guard self.contentContainer.transform != .identity else { + guard self.contentContainerView.transform != .identity else { return } UIView.animate(withDuration: animationDuration) { - self.contentContainer.transform = CGAffineTransform.identity + self.contentContainerView.transform = CGAffineTransform.identity } case .compact: - guard self.contentContainer.transform == .identity else { + guard self.contentContainerView.transform == .identity else { return } UIView.animate(withDuration: animationDuration) { @@ -354,7 +407,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD let heightDelta = originalHeight * (1 - kScaleFactor) let translate = CGAffineTransform(translationX: 0, y: -heightDelta / 2) - self.contentContainer.transform = scale.concatenating(translate) + self.contentContainerView.transform = scale.concatenating(translate) } } } @@ -367,66 +420,55 @@ extension AttachmentPrepViewController: UIScrollViewDelegate { public func viewForZooming(in scrollView: UIScrollView) -> UIView? { if isZoomable { return mediaMessageView - } else { - // don't zoom for audio or generic attachments. - return nil } + + // Don't zoom for audio or generic attachments. + return nil + } + + public func scrollViewDidZoom(_ scrollView: UIScrollView) { + resetContentInset() } - fileprivate func updateMinZoomScaleForSize(_ size: CGSize) { - Logger.debug("") - + fileprivate func setupZoomScale() { + // We only want to setup the zoom scale once (otherwise we get glitchy behaviour + // when anything forces a re-layout) + guard abs(scrollView.maximumZoomScale - 1.0) <= CGFloat.leastNormalMagnitude else { + return + } + // Ensure bounds have been computed - mediaMessageView.layoutIfNeeded() guard mediaMessageView.bounds.width > 0, mediaMessageView.bounds.height > 0 else { Logger.warn("bad bounds") 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 + let widthScale: CGFloat = (view.bounds.size.width / mediaMessageView.bounds.width) + let heightScale: CGFloat = (view.bounds.size.height / mediaMessageView.bounds.height) + let minScale: CGFloat = min(widthScale, heightScale) + scrollView.minimumZoomScale = minScale + scrollView.maximumZoomScale = (minScale * 5) 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 + + // Allow the user to zoom out to 100% of the attachment size if it's smaller + // than the screen + fileprivate func resetContentInset() { + guard isZoomable else { + scrollView.contentOffset = CGPoint(x: 0, y: scrollView.contentInset.bottom) + return } - - 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) + + let offsetX: CGFloat = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0) + let offsetY: CGFloat = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0) + + scrollView.contentInset = UIEdgeInsets( + top: offsetY, + left: offsetX, + bottom: 0, + right: 0 + ) } } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift index 8cdc30eb2..2907d0c64 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentTextToolbar.swift @@ -32,8 +32,9 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate { } // Layout Constants - - let kMinTextViewHeight: CGFloat = 40 + + static let kToolbarMargin: CGFloat = 8 + static let kMinTextViewHeight: CGFloat = 40 var maxTextViewHeight: CGFloat { // About ~4 lines in portrait and ~3 lines in landscape. // Otherwise we risk obscuring too much of the content. @@ -46,7 +47,7 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate { init() { self.sendButton = UIButton(type: .system) - self.textViewHeight = kMinTextViewHeight + self.textViewHeight = AttachmentTextToolbar.kMinTextViewHeight super.init(frame: CGRect.zero) @@ -77,15 +78,19 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate { contentView.autoPinEdgesToSuperviewEdges() // Layout - let kToolbarMargin: CGFloat = 8 // We have to wrap the toolbar items in a content view because iOS (at least on iOS10.3) assigns the inputAccessoryView.layoutMargins // when resigning first responder (verified by auditing with `layoutMarginsDidChange`). // The effect of this is that if we were to assign these margins to self.layoutMargins, they'd be blown away if the // user dismisses the keyboard, giving the input accessory view a wonky layout. - contentView.layoutMargins = UIEdgeInsets(top: kToolbarMargin, left: kToolbarMargin, bottom: kToolbarMargin, right: kToolbarMargin) + contentView.layoutMargins = UIEdgeInsets( + top: AttachmentTextToolbar.kToolbarMargin, + left: AttachmentTextToolbar.kToolbarMargin, + bottom: AttachmentTextToolbar.kToolbarMargin, + right: AttachmentTextToolbar.kToolbarMargin + ) - self.textViewHeightConstraint = textView.autoSetDimension(.height, toSize: kMinTextViewHeight) + self.textViewHeightConstraint = textView.autoSetDimension(.height, toSize: AttachmentTextToolbar.kMinTextViewHeight) // We pin all three edges explicitly rather than doing something like: // textView.autoPinEdges(toSuperviewMarginsExcludingEdge: .right) @@ -97,7 +102,7 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate { textContainer.autoPinEdge(toSuperviewMargin: .bottom) textContainer.autoPinEdge(toSuperviewMargin: .left) - sendButton.autoPinEdge(.left, to: .right, of: textContainer, withOffset: kToolbarMargin) + sendButton.autoPinEdge(.left, to: .right, of: textContainer, withOffset: AttachmentTextToolbar.kToolbarMargin) sendButton.autoPinEdge(.bottom, to: .bottom, of: textContainer, withOffset: -3) sendButton.autoPinEdge(toSuperviewMargin: .right) @@ -170,7 +175,7 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate { textContainer.layer.borderColor = UIColor.white.cgColor textContainer.layer.borderWidth = Values.separatorThickness - textContainer.layer.cornerRadius = kMinTextViewHeight / 2 + textContainer.layer.cornerRadius = (AttachmentTextToolbar.kMinTextViewHeight / 2) textContainer.clipsToBounds = true textContainer.addSubview(placeholderTextView) @@ -314,6 +319,6 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate { private func clampedTextViewHeight(fixedWidth: CGFloat) -> CGFloat { let contentSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude)) - return CGFloatClamp(contentSize.height, kMinTextViewHeight, maxTextViewHeight) + return CGFloatClamp(contentSize.height, AttachmentTextToolbar.kMinTextViewHeight, maxTextViewHeight) } } diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index 9c294582c..28b5cbac4 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -6,37 +6,25 @@ import Foundation import MediaPlayer import YYImage import NVActivityIndicatorView - import SessionUIKit -@objc -public enum MediaMessageViewMode: UInt { - case large - case small - case attachmentApproval -} - -@objc public class MediaMessageView: UIView, OWSAudioPlayerDelegate { + public enum Mode: UInt { + case large + case small + case attachmentApproval + } // MARK: Properties - @objc - public let mode: MediaMessageViewMode - - @objc + public let mode: Mode public let attachment: SignalAttachment - @objc public var audioPlayer: OWSAudioPlayer? + + private var linkPreviewInfo: (url: String, draft: OWSLinkPreviewDraft?)? + - @objc - public var audioPlayButton: UIButton? - - @objc - public var videoPlayButton: UIImageView? - - @objc public var playbackState = AudioPlaybackState.stopped { didSet { AssertIsOnMainThread() @@ -45,16 +33,12 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } } - @objc public var audioProgressSeconds: CGFloat = 0 - - @objc public var audioDurationSeconds: CGFloat = 0 - @objc public var contentView: UIView? - private var linkPreviewInfo: (url: String, draft: OWSLinkPreviewDraft?)? + // MARK: Initializers @@ -65,23 +49,119 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { // Currently we only use one mode (AttachmentApproval), so we could simplify this class, but it's kind // of nice that it's written in a flexible way in case we'd want to use it elsewhere again in the future. - @objc - public required init(attachment: SignalAttachment, mode: MediaMessageViewMode) { - if attachment.hasError { - owsFailDebug(attachment.error.debugDescription) - } + public required init(attachment: SignalAttachment, mode: MediaMessageView.Mode) { + if attachment.hasError { owsFailDebug(attachment.error.debugDescription) } + self.attachment = attachment self.mode = mode + super.init(frame: CGRect.zero) createViews() + + backgroundColor = .red + + setupLayout() } deinit { NotificationCenter.default.removeObserver(self) } - - // MARK: - Create Views + + // MARK: - UI + + private lazy var stackView: UIStackView = { + let stackView: UIStackView = UIStackView() + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.alignment = .center + stackView.distribution = .equalSpacing + + switch mode { + case .large, .attachmentApproval: stackView.spacing = 10 + case .small: stackView.spacing = 5 + } + + return stackView + }() + + private lazy var imageView: UIImageView = { + let view: UIImageView = UIImageView() + view.translatesAutoresizingMaskIntoConstraints = false + view.contentMode = .scaleAspectFit + view.layer.minificationFilter = .trilinear + view.layer.magnificationFilter = .trilinear + + return view + }() + + private lazy var fileTypeImageView: UIImageView = { + let view: UIImageView = UIImageView() + view.translatesAutoresizingMaskIntoConstraints = false + view.layer.minificationFilter = .trilinear + view.layer.magnificationFilter = .trilinear + + return view + }() + + private lazy var animatedImageView: YYAnimatedImageView = { + let view: YYAnimatedImageView = YYAnimatedImageView() + view.translatesAutoresizingMaskIntoConstraints = false + + return view + }() + + lazy var videoPlayButton: UIImageView = { + let imageView: UIImageView = UIImageView(image: UIImage(named: "CirclePlay")) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFit + + return imageView + }() + + /// Note: This uses different assets from the `videoPlayButton` and has a 'Pause' state + private lazy var audioPlayPauseButton: UIButton = { + let button: UIButton = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.clipsToBounds = true + button.setBackgroundImage(UIColor.white.toImage(), for: .normal) + button.setBackgroundImage(UIColor.white.darken(by: 0.2).toImage(), for: .highlighted) + button.layer.cornerRadius = 30 + + button.addTarget(self, action: #selector(audioPlayPauseButtonPressed), for: .touchUpInside) + + return button + }() + + private lazy var titleLabel: UILabel = { + let label: UILabel = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = labelFont() + label.text = (formattedFileName() ?? formattedFileExtension()) + label.textColor = controlTintColor + label.textAlignment = .center + label.lineBreakMode = .byTruncatingMiddle + label.isHidden = ((label.text?.count ?? 0) == 0) + + return label + }() + + private lazy var fileSizeLabel: UILabel = { + let fileSize: UInt = attachment.dataLength + + let label: UILabel = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = labelFont() + // Format string for file size label in call interstitial view. + // Embeds: {{file size as 'N mb' or 'N kb'}}. + label.text = String(format: "ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT".localized(), OWSFormat.formatFileSize(UInt(fileSize))) + label.textColor = controlTintColor + label.textAlignment = .center + + return label + }() + + // MARK: - Layout private func createViews() { if attachment.isAnimatedImage { @@ -100,7 +180,12 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { createGenericPreview() } } + + private func setupLayout() { + // Bottom inset + } + // TODO: Any reason for not just using UIStackView private func wrapViewsInVerticalStack(subviews: [UIView]) -> UIView { assert(subviews.count > 0) @@ -115,7 +200,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { if lastView == nil { subview.autoPinEdge(toSuperviewEdge: .top) } else { - subview.autoPinEdge(.top, to: .bottom, of: lastView!, withOffset: stackSpacing()) + subview.autoPinEdge(.top, to: .bottom, of: lastView!, withOffset: 10) } lastView = subview @@ -140,7 +225,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { if lastView == nil { subview.autoPinEdge(toSuperviewEdge: .left) } else { - subview.autoPinEdge(.left, to: .right, of: lastView!, withOffset: stackSpacing()) + subview.autoPinEdge(.left, to: .right, of: lastView!, withOffset: 10) } lastView = subview @@ -151,14 +236,14 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { return stackView } - private func stackSpacing() -> CGFloat { - switch mode { - case .large, .attachmentApproval: - return CGFloat(10) - case .small: - return CGFloat(5) - } - } +// private func stackSpacing() -> CGFloat { +// switch mode { +// case .large, .attachmentApproval: +// return CGFloat(10) +// case .small: +// return CGFloat(5) +// } +// } private func createAudioPreview() { guard let dataUrl = attachment.dataUrl else { @@ -167,41 +252,53 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } audioPlayer = OWSAudioPlayer(mediaUrl: dataUrl, audioBehavior: .playback, delegate: self) - - var subviews = [UIView]() - - let audioPlayButton = UIButton() - self.audioPlayButton = audioPlayButton + + imageView.image = UIImage(named: "FileLarge") + fileTypeImageView.image = UIImage(named: "table_ic_notification_sound") setAudioIconToPlay() - audioPlayButton.imageView?.layer.minificationFilter = .trilinear - audioPlayButton.imageView?.layer.magnificationFilter = .trilinear - audioPlayButton.addTarget(self, action: #selector(audioPlayButtonPressed), for: .touchUpInside) - let buttonSize = createHeroViewSize() - audioPlayButton.autoSetDimension(.width, toSize: buttonSize) - audioPlayButton.autoSetDimension(.height, toSize: buttonSize) - subviews.append(audioPlayButton) - - let fileNameLabel = createFileNameLabel() - if let fileNameLabel = fileNameLabel { - subviews.append(fileNameLabel) - } - - let fileSizeLabel = createFileSizeLabel() - subviews.append(fileSizeLabel) - - let stackView = wrapViewsInVerticalStack(subviews: subviews) + self.addSubview(stackView) - fileNameLabel?.autoPinWidthToSuperview(withMargin: 32) + self.addSubview(audioPlayPauseButton) + + stackView.addArrangedSubview(imageView) + stackView.addArrangedSubview(UIView.vSpacer(0)) + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(fileSizeLabel) + + imageView.addSubview(fileTypeImageView) - // We want to center the stackView in it's superview while also ensuring - // it's superview is big enough to contain it. - stackView.autoPinWidthToSuperview() - stackView.autoVCenterInSuperview() - NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultLow) { - stackView.autoPinHeightToSuperview() - } - stackView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual) - stackView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual) + NSLayoutConstraint.activate([ + stackView.centerYAnchor.constraint(equalTo: centerYAnchor), + stackView.widthAnchor.constraint(equalTo: widthAnchor), + stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor), + + imageView.widthAnchor.constraint(equalToConstant: 150), + imageView.heightAnchor.constraint(equalToConstant: 150), + titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), + fileSizeLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), + + fileTypeImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), + fileTypeImageView.centerYAnchor.constraint( + equalTo: imageView.centerYAnchor, + constant: 25 + ), + fileTypeImageView.widthAnchor.constraint( + equalTo: fileTypeImageView.heightAnchor, + multiplier: ((fileTypeImageView.image?.size.width ?? 1) / (fileTypeImageView.image?.size.height ?? 1)) + ), + fileTypeImageView.widthAnchor.constraint( + equalTo: imageView.widthAnchor, constant: -75 + ), + + audioPlayPauseButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), + audioPlayPauseButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), + audioPlayPauseButton.widthAnchor.constraint( + equalToConstant: (audioPlayPauseButton.layer.cornerRadius * 2) + ), + audioPlayPauseButton.heightAnchor.constraint( + equalToConstant: (audioPlayPauseButton.layer.cornerRadius * 2) + ) + ]) } private func createAnimatedPreview() { @@ -221,25 +318,38 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { createGenericPreview() return } - let animatedImageView = YYAnimatedImageView() animatedImageView.image = image - let aspectRatio = image.size.width / image.size.height - addSubviewWithScaleAspectFitLayout(view: animatedImageView, aspectRatio: aspectRatio) + let aspectRatio: CGFloat = (image.size.width / image.size.height) + let clampedRatio: CGFloat = CGFloatClamp(aspectRatio, 0.05, 95.0) + + addSubview(animatedImageView) +// addSubviewWithScaleAspectFitLayout(view: animatedImageView, aspectRatio: aspectRatio) contentView = animatedImageView + + NSLayoutConstraint.activate([ + animatedImageView.centerXAnchor.constraint(equalTo: centerXAnchor), + animatedImageView.centerYAnchor.constraint(equalTo: centerYAnchor), + animatedImageView.widthAnchor.constraint( + equalTo: animatedImageView.heightAnchor, + multiplier: clampedRatio + ), + animatedImageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor), + animatedImageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor) + ]) } - private func addSubviewWithScaleAspectFitLayout(view: UIView, aspectRatio: CGFloat) { - self.addSubview(view) - // This emulates the behavior of contentMode = .scaleAspectFit using - // iOS auto layout constraints. - // - // This allows ConversationInputToolbar to place the "cancel" button - // in the upper-right hand corner of the preview content. - view.autoCenterInSuperview() - view.autoPin(toAspectRatio: aspectRatio) - view.autoMatch(.width, to: .width, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual) - view.autoMatch(.height, to: .height, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual) - } +// private func addSubviewWithScaleAspectFitLayout(view: UIView, aspectRatio: CGFloat) { +// self.addSubview(view) +// // This emulates the behavior of contentMode = .scaleAspectFit using +// // iOS auto layout constraints. +// // +// // This allows ConversationInputToolbar to place the "cancel" button +// // in the upper-right hand corner of the preview content. +// view.autoCenterInSuperview() +// view.autoPin(toAspectRatio: aspectRatio) +// view.autoMatch(.width, to: .width, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual) +// view.autoMatch(.height, to: .height, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual) +// } private func createImagePreview() { guard attachment.isValidImage else { @@ -255,12 +365,26 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { return } - let imageView = UIImageView(image: image) - imageView.layer.minificationFilter = .trilinear - imageView.layer.magnificationFilter = .trilinear + imageView.image = image +// imageView.layer.minificationFilter = .trilinear +// imageView.layer.magnificationFilter = .trilinear + let aspectRatio = image.size.width / image.size.height - addSubviewWithScaleAspectFitLayout(view: imageView, aspectRatio: aspectRatio) + let clampedRatio: CGFloat = CGFloatClamp(aspectRatio, 0.05, 95.0) + +// addSubviewWithScaleAspectFitLayout(view: imageView, aspectRatio: aspectRatio) contentView = imageView + + NSLayoutConstraint.activate([ + imageView.centerXAnchor.constraint(equalTo: centerXAnchor), + imageView.centerYAnchor.constraint(equalTo: centerYAnchor), + imageView.widthAnchor.constraint( + equalTo: imageView.heightAnchor, + multiplier: clampedRatio + ), + imageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor), + imageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor) + ]) } private func createVideoPreview() { @@ -277,30 +401,58 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { return } - let imageView = UIImageView(image: image) - imageView.layer.minificationFilter = .trilinear - imageView.layer.magnificationFilter = .trilinear +// let imageView = UIImageView(image: image) + imageView.image = image +// imageView.layer.minificationFilter = .trilinear +// imageView.layer.magnificationFilter = .trilinear + self.addSubview(imageView) + let aspectRatio = image.size.width / image.size.height - addSubviewWithScaleAspectFitLayout(view: imageView, aspectRatio: aspectRatio) + let clampedRatio: CGFloat = CGFloatClamp(aspectRatio, 0.05, 95.0) + +// addSubviewWithScaleAspectFitLayout(view: imageView, aspectRatio: aspectRatio) contentView = imageView + + // Attachment approval provides it's own play button to keep it + // at the proper zoom scale. + if mode != .attachmentApproval { + self.addSubview(videoPlayButton) + } + + NSLayoutConstraint.activate([ + imageView.centerXAnchor.constraint(equalTo: centerXAnchor), + imageView.centerYAnchor.constraint(equalTo: centerYAnchor), + imageView.widthAnchor.constraint( + equalTo: imageView.heightAnchor, + multiplier: clampedRatio + ), + imageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor), + imageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor) + ]) - // attachment approval provides it's own play button to keep it + // Attachment approval provides it's own play button to keep it // at the proper zoom scale. if mode != .attachmentApproval { - let videoPlayIcon = UIImage(named: "CirclePlay")! - let videoPlayButton = UIImageView(image: videoPlayIcon) - self.videoPlayButton = videoPlayButton - videoPlayButton.contentMode = .scaleAspectFit self.addSubview(videoPlayButton) - videoPlayButton.autoCenterInSuperview() - videoPlayButton.autoSetDimension(.width, toSize: 72) - videoPlayButton.autoSetDimension(.height, toSize: 72) +// videoPlayButton.autoCenterInSuperview() +// videoPlayButton.autoSetDimension(.width, toSize: 72) +// videoPlayButton.autoSetDimension(.height, toSize: 72) + + NSLayoutConstraint.activate([ + videoPlayButton.centerXAnchor.constraint(equalTo: centerXAnchor), + videoPlayButton.centerYAnchor.constraint(equalTo: centerYAnchor), + imageView.widthAnchor.constraint(equalToConstant: 72), + imageView.heightAnchor.constraint(equalToConstant: 72) + ]) } } private func createUrlPreview() { // If link previews aren't enabled then use a fallback state guard let linkPreviewURL: String = OWSLinkPreview.previewURL(forRawBodyText: attachment.text()) else { +// "vc_share_link_previews_disabled_title" = "Link Previews Disabled"; +// "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you sshare. This can be useful, but Session will need to contact linked websites to generate previews. You can enable link previews in Session's settings."; +// TODO: Show "warning" about disabled link previews instead createGenericPreview() return } @@ -413,33 +565,40 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } private func createGenericPreview() { - var subviews = [UIView]() - - let imageView = createHeroImageView(imageName: "FileLarge") - imageView.contentMode = .center - subviews.append(imageView) - - let fileNameLabel = createFileNameLabel() - if let fileNameLabel = fileNameLabel { - subviews.append(fileNameLabel) - } - - let fileSizeLabel = createFileSizeLabel() - subviews.append(fileSizeLabel) - - let stackView = wrapViewsInVerticalStack(subviews: subviews) + imageView.image = UIImage(named: "FileLarge") + stackView.backgroundColor = .green self.addSubview(stackView) - fileNameLabel?.autoPinWidthToSuperview(withMargin: 32) + + stackView.addArrangedSubview(imageView) + stackView.addArrangedSubview(UIView.vSpacer(0)) + stackView.addArrangedSubview(titleLabel) + stackView.addArrangedSubview(fileSizeLabel) + + imageView.addSubview(fileTypeImageView) - // We want to center the stackView in it's superview while also ensuring - // it's superview is big enough to contain it. - stackView.autoPinWidthToSuperview() - stackView.autoVCenterInSuperview() - NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultLow) { - stackView.autoPinHeightToSuperview() - } - stackView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual) - stackView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual) + NSLayoutConstraint.activate([ + stackView.centerYAnchor.constraint(equalTo: centerYAnchor), + stackView.widthAnchor.constraint(equalTo: widthAnchor), + stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor), + + imageView.widthAnchor.constraint(equalToConstant: 150), + imageView.heightAnchor.constraint(equalToConstant: 150), + titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), + fileSizeLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), + + fileTypeImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), + fileTypeImageView.centerYAnchor.constraint( + equalTo: imageView.centerYAnchor, + constant: 25 + ), + fileTypeImageView.widthAnchor.constraint( + equalTo: fileTypeImageView.heightAnchor, + multiplier: ((fileTypeImageView.image?.size.width ?? 1) / (fileTypeImageView.image?.size.height ?? 1)) + ), + fileTypeImageView.widthAnchor.constraint( + equalTo: imageView.widthAnchor, constant: -75 + ) + ]) } private func createHeroViewSize() -> CGFloat { @@ -474,10 +633,10 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { private func labelFont() -> UIFont { switch mode { - case .large, .attachmentApproval: - return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(18, 24)) - case .small: - return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(14, 14)) + case .large, .attachmentApproval: + return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(18, 24)) + case .small: + return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(14, 14)) } } @@ -495,19 +654,17 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { return nil } - return String(format: NSLocalizedString("ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT", - comment: "Format string for file extension label in call interstitial view"), - fileExtension.uppercased()) + //"Format string for file extension label in call interstitial view" + return String(format: "ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT".localized(), fileExtension.uppercased()) } public func formattedFileName() -> String? { - guard let sourceFilename = attachment.sourceFilename else { - return nil - } + guard let sourceFilename = attachment.sourceFilename else { return nil } + let filename = sourceFilename.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) - guard filename.count > 0 else { - return nil - } + + guard filename.count > 0 else { return nil } + return filename } @@ -543,8 +700,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { // MARK: - Event Handlers - @objc - func audioPlayButtonPressed(sender: UIButton) { + @objc func audioPlayPauseButtonPressed(sender: UIButton) { audioPlayer?.togglePlayState() } @@ -580,16 +736,22 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } private func setAudioIconToPlay() { - let image = UIImage(named: "audio_play_black_large")?.withRenderingMode(.alwaysTemplate) - assert(image != nil) - audioPlayButton?.setImage(image, for: .normal) - audioPlayButton?.imageView?.tintColor = controlTintColor + //attachment_audio +// let image = UIImage(named: "audio_play_black_large")?.withRenderingMode(.alwaysTemplate) +// assert(image != nil) +// audioPlayButton?.setImage(image, for: .normal) +// audioPlayButton?.imageView?.tintColor = controlTintColor + //let image = UIImage(named: "CirclePlay") + let image = UIImage(named: "Play") + audioPlayPauseButton.setImage(image, for: .normal) } private func setAudioIconToPause() { - let image = UIImage(named: "audio_pause_black_large")?.withRenderingMode(.alwaysTemplate) - assert(image != nil) - audioPlayButton?.setImage(image, for: .normal) - audioPlayButton?.imageView?.tintColor = controlTintColor +// let image = UIImage(named: "audio_pause_black_large")?.withRenderingMode(.alwaysTemplate) +// assert(image != nil) +// audioPlayButton?.setImage(image, for: .normal) +// audioPlayButton?.imageView?.tintColor = controlTintColor + let image = UIImage(named: "Pause") + audioPlayPauseButton.setImage(image, for: .normal) } } diff --git a/SignalUtilitiesKit/Utilities/UIColor+Extensions.swift b/SignalUtilitiesKit/Utilities/UIColor+Extensions.swift new file mode 100644 index 000000000..19aa376ce --- /dev/null +++ b/SignalUtilitiesKit/Utilities/UIColor+Extensions.swift @@ -0,0 +1,43 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import UIKit.UIColor + +public extension UIColor { + struct HSBA { + public var hue: CGFloat = 0 + public var saturation: CGFloat = 0 + public var brightness: CGFloat = 0 + public var alpha: CGFloat = 0 + + public init?(color: UIColor) { + // Note: Looks like as of iOS 10 devices use the kCGColorSpaceExtendedGray color + // space for grayscale colors which seems to be compatible with the RGB color space + // meaning we don'e need to check 'getWhite:alpha:' if the below method fails, for + // more info see: https://developer.apple.com/documentation/uikit/uicolor#overview + guard color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) else { + return nil + } + } + } + + var hsba: HSBA? { return HSBA(color: self) } + + // MARK: - Functions + + func toImage() -> UIImage { + let bounds: CGRect = CGRect(x: 0, y: 0, width: 1, height: 1) + let renderer: UIGraphicsImageRenderer = UIGraphicsImageRenderer(bounds: bounds) + + return renderer.image { rendererContext in + rendererContext.cgContext.setFillColor(self.cgColor) + rendererContext.cgContext.fill(bounds) + } + } + + func darken(by percentage: CGFloat) -> UIColor { + guard percentage != 0 else { return self } + guard let hsba: HSBA = self.hsba else { return self } + + return UIColor(hue: hsba.hue, saturation: hsba.saturation, brightness: (hsba.brightness - percentage), alpha: hsba.alpha) + } +}