diff --git a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift index f65d8640c..e6222d59e 100644 --- a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift +++ b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift @@ -20,6 +20,18 @@ public enum SignalAttachmentError: Error { case couldNotResizeImage } +@objc +public enum SignalAttachmentType: Int { + case text + case oversizeText + case image + case animatedImage + case video + case audio + case url + case unknown +} + extension String { public var filenameWithoutExtension: String { return (self as NSString).deletingPathExtension @@ -434,6 +446,19 @@ public class SignalAttachment: NSObject { private class var mediaUTISet: Set { return audioUTISet.union(videoUTISet).union(animatedImageUTISet).union(inputImageUTISet) } + + @objc + public var fileType: SignalAttachmentType { + if isAnimatedImage { return .animatedImage } + if isImage { return .image } + if isVideo { return .video } + if isAudio { return .audio } + if isUrl { return .url } + if isOversizeText { return .oversizeText } + if isText { return .text } + + return .unknown + } @objc public var isImage: Bool { diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift index d249a63ef..998d7c783 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentPrepViewController.swift @@ -45,6 +45,10 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD // MARK: - UI + fileprivate static let verticalCenterOffset: CGFloat = ( + AttachmentTextToolbar.kMinTextViewHeight + (AttachmentTextToolbar.kToolbarMargin * 2) + ) + private lazy var scrollView: UIScrollView = { // Scroll View - used to zoom/pan on images and video let scrollView: UIScrollView = UIScrollView() @@ -56,17 +60,6 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD // 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 }() @@ -112,6 +105,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD private lazy var progressBar: PlayerProgressBar = { let progressBar: PlayerProgressBar = PlayerProgressBar() + progressBar.translatesAutoresizingMaskIntoConstraints = false progressBar.player = videoPlayer?.avPlayer progressBar.delegate = self @@ -161,7 +155,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD contentContainerView.addSubview(scrollView) scrollView.addSubview(mediaMessageView) - if let editorView: ImageEditorView = imageEditorView { + if attachment.isImage, let editorView: ImageEditorView = imageEditorView { view.addSubview(editorView) imageEditorUpdateNavigationBar() @@ -235,12 +229,20 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD mediaMessageView.heightAnchor.constraint(equalTo: view.heightAnchor) ]) - if let editorView: ImageEditorView = imageEditorView { + if attachment.isImage, let editorView: ImageEditorView = imageEditorView { + let size: CGSize = (attachment.image()?.size ?? CGSize.zero) + let isPortrait: Bool = (size.height > size.width) + 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) + editorView.bottomAnchor.constraint( + equalTo: view.bottomAnchor, + // Don't offset portrait images as they look fine vertically aligned, horizontal + // ones need to be pushed up a bit though + constant: (isPortrait ? 0 : -AttachmentPrepViewController.verticalCenterOffset) + ) ]) } @@ -258,7 +260,10 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD progressBar.heightAnchor.constraint(equalToConstant: 44), playVideoButton.centerXAnchor.constraint(equalTo: contentContainerView.centerXAnchor), - playVideoButton.centerYAnchor.constraint(equalTo: contentContainerView.centerYAnchor), + playVideoButton.centerYAnchor.constraint( + equalTo: contentContainerView.centerYAnchor, + constant: -AttachmentPrepViewController.verticalCenterOffset + ), playVideoButton.widthAnchor.constraint(equalToConstant: playButtonSize), playVideoButton.heightAnchor.constraint(equalToConstant: playButtonSize), ]) @@ -455,8 +460,14 @@ extension AttachmentPrepViewController: UIScrollViewDelegate { // Allow the user to zoom out to 100% of the attachment size if it's smaller // than the screen fileprivate func resetContentInset() { + // If the content isn't zoomable then inset the content so it appears centered guard isZoomable else { - scrollView.contentOffset = CGPoint(x: 0, y: scrollView.contentInset.bottom) + scrollView.contentInset = UIEdgeInsets( + top: -AttachmentPrepViewController.verticalCenterOffset, + leading: 0, + bottom: 0, + trailing: 0 + ) return } @@ -464,7 +475,7 @@ extension AttachmentPrepViewController: UIScrollViewDelegate { let offsetY: CGFloat = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0) scrollView.contentInset = UIEdgeInsets( - top: offsetY, + top: offsetY - AttachmentPrepViewController.verticalCenterOffset, left: offsetX, bottom: 0, right: 0 diff --git a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift index f0d260c08..d5c9c2d64 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/MediaMessageView.swift @@ -20,11 +20,15 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { public let mode: Mode public let attachment: SignalAttachment - public var audioPlayer: OWSAudioPlayer? + public lazy var audioPlayer: OWSAudioPlayer? = { + guard let dataUrl = attachment.dataUrl else { return nil } + + return OWSAudioPlayer(mediaUrl: dataUrl, audioBehavior: .playback, delegate: self) + }() - private var linkPreviewInfo: (url: String, draft: OWSLinkPreviewDraft?)? + public var audioProgressSeconds: CGFloat = 0 + public var audioDurationSeconds: CGFloat = 0 - public var playbackState = AudioPlaybackState.stopped { didSet { AssertIsOnMainThread() @@ -32,13 +36,51 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { ensureButtonState() } } - - public var audioProgressSeconds: CGFloat = 0 - public var audioDurationSeconds: CGFloat = 0 - - public var contentView: UIView? - + private lazy var validImage: UIImage? = { + switch attachment.fileType { + case .image: + guard + attachment.isValidImage, + let image: UIImage = attachment.image(), + image.size.width > 0, + image.size.height > 0 + else { + return nil + } + + return image + + case .video: + guard + attachment.isValidVideo, + let image: UIImage = attachment.videoPreview(), + image.size.width > 0, + image.size.height > 0 + else { + return nil + } + + return image + + default: return nil + } + }() + private lazy var validAnimatedImage: YYImage? = { + guard + attachment.fileType == .animatedImage, + attachment.isValidImage, + let dataUrl: URL = attachment.dataUrl, + let image: YYImage = YYImage(contentsOfFile: dataUrl.path), + image.size.width > 0, + image.size.height > 0 + else { + return nil + } + + return image + }() + private var linkPreviewInfo: (url: String, draft: OWSLinkPreviewDraft?)? // MARK: Initializers @@ -55,11 +97,14 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { self.attachment = attachment self.mode = mode + // Set the linkPreviewUrl if it's a url + if attachment.isUrl, let linkPreviewURL: String = OWSLinkPreview.previewURL(forRawBodyText: attachment.text()) { + self.linkPreviewInfo = (url: linkPreviewURL, draft: nil) + } + super.init(frame: CGRect.zero) - createViews() - - + setupViews() setupLayout() } @@ -74,7 +119,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical stackView.alignment = .center - stackView.distribution = .equalSpacing + stackView.distribution = .fill switch mode { case .attachmentApproval: stackView.spacing = 2 @@ -97,8 +142,28 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { let view: UIImageView = UIImageView() view.translatesAutoresizingMaskIntoConstraints = false view.contentMode = .scaleAspectFit - view.layer.minificationFilter = .trilinear - view.layer.magnificationFilter = .trilinear + view.image = UIImage(named: "FileLarge")?.withRenderingMode(.alwaysTemplate) + view.tintColor = Colors.text + view.isHidden = true + + // Override the image to the correct one + switch attachment.fileType { + case .image, .video: + if let validImage: UIImage = validImage { + view.layer.minificationFilter = .trilinear + view.layer.magnificationFilter = .trilinear + view.image = validImage + } + + case .url: + view.clipsToBounds = true + view.image = UIImage(named: "Link")?.withTint(Colors.text) + view.contentMode = .center + view.backgroundColor = (isDarkMode ? .black : UIColor.black.withAlphaComponent(0.06)) + view.layer.cornerRadius = 8 + + default: break + } return view }() @@ -106,6 +171,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { private lazy var fileTypeImageView: UIImageView = { let view: UIImageView = UIImageView() view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true return view }() @@ -113,16 +179,27 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { private lazy var animatedImageView: YYAnimatedImageView = { let view: YYAnimatedImageView = YYAnimatedImageView() view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + + if let image: YYImage = validAnimatedImage { + view.image = image + } + else { + view.contentMode = .scaleAspectFit + view.image = UIImage(named: "FileLarge")?.withRenderingMode(.alwaysTemplate) + view.tintColor = Colors.text + } return view }() lazy var videoPlayButton: UIImageView = { - let imageView: UIImageView = UIImageView(image: UIImage(named: "CirclePlay")) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.contentMode = .scaleAspectFit + let view: UIImageView = UIImageView(image: UIImage(named: "CirclePlay")) + view.translatesAutoresizingMaskIntoConstraints = false + view.contentMode = .scaleAspectFit + view.isHidden = true - return imageView + return view }() /// Note: This uses different assets from the `videoPlayButton` and has a 'Pause' state @@ -133,6 +210,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { button.setBackgroundImage(UIColor.white.toImage(), for: .normal) button.setBackgroundImage(UIColor.white.darken(by: 0.2).toImage(), for: .highlighted) button.addTarget(self, action: #selector(audioPlayPauseButtonPressed), for: .touchUpInside) + button.isHidden = true return button }() @@ -140,21 +218,8 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { private lazy var titleLabel: UILabel = { let label: UILabel = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.textAlignment = .center - label.lineBreakMode = .byTruncatingMiddle - - if let fileName: String = attachment.sourceFilename?.trimmingCharacters(in: .whitespacesAndNewlines), fileName.count > 0 { - label.text = fileName - } - else if let fileExtension: String = attachment.fileExtension { - label.text = String( - format: "ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT".localized(), - fileExtension.uppercased() - ) - } - - label.isHidden = ((label.text?.count ?? 0) == 0) + // Styling switch mode { case .attachmentApproval: label.font = UIFont.ows_boldFont(withSize: ScaleFromIPhone5To7Plus(16, 22)) @@ -169,19 +234,49 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { label.textColor = Colors.accent } + // Content + switch attachment.fileType { + case .image, .animatedImage, .video: break // No title for these + + case .url: + // If we have no link preview info at this point then assume link previews are disabled + guard let linkPreviewURL: String = linkPreviewInfo?.url else { + label.text = "vc_share_link_previews_disabled_title".localized() + break + } + + label.font = .boldSystemFont(ofSize: Values.smallFontSize) + label.text = linkPreviewURL + label.textAlignment = .left + label.lineBreakMode = .byTruncatingTail + label.numberOfLines = 2 + + default: + if let fileName: String = attachment.sourceFilename?.trimmingCharacters(in: .whitespacesAndNewlines), fileName.count > 0 { + label.text = fileName + } + else if let fileExtension: String = attachment.fileExtension { + label.text = String( + format: "ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT".localized(), + fileExtension.uppercased() + ) + } + + label.textAlignment = .center + label.lineBreakMode = .byTruncatingMiddle + } + + // Hide the label if it has no content + 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 - // 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.textAlignment = .center + // Styling switch mode { case .attachmentApproval: label.font = UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(12, 18)) @@ -196,137 +291,193 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { label.textColor = Colors.accent } + // Content + switch attachment.fileType { + case .image, .animatedImage, .video: break // No size for these + + case .url: + // If we have no link preview info at this point then assume link previews are disabled + if linkPreviewInfo == nil { + label.text = "vc_share_link_previews_disabled_explanation".localized() + label.textColor = Colors.text + label.textAlignment = .center + label.numberOfLines = 0 + break + } + + default: + // Format string for file size label in call interstitial view. + // Embeds: {{file size as 'N mb' or 'N kb'}}. + let fileSize: UInt = attachment.dataLength + label.text = String(format: "ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT".localized(), OWSFormat.formatFileSize(UInt(fileSize))) + label.textAlignment = .center + } + + // Hide the label if it has no content + label.isHidden = ((label.text?.count ?? 0) == 0) + return label }() // MARK: - Layout - private func createViews() { - if attachment.isAnimatedImage { - createAnimatedPreview() - } else if attachment.isImage { - createImagePreview() - } else if attachment.isVideo { - createVideoPreview() - } else if attachment.isAudio { - createAudioPreview() - } else if attachment.isUrl { - createUrlPreview() - } else if attachment.isText { - // Do nothing as we will just put the text in the 'message' input - } else { - createGenericPreview() - } - } - - private func setupLayout() { - // Bottom inset - } - - // TODO: Any reason for not just using UIStackView - private func wrapViewsInVerticalStack(subviews: [UIView]) -> UIView { - assert(subviews.count > 0) - - let stackView = UIView() - - var lastView: UIView? - for subview in subviews { - - stackView.addSubview(subview) - subview.autoHCenterInSuperview() - - if lastView == nil { - subview.autoPinEdge(toSuperviewEdge: .top) - } else { - subview.autoPinEdge(.top, to: .bottom, of: lastView!, withOffset: 10) - } - - lastView = subview - } - - lastView?.autoPinEdge(toSuperviewEdge: .bottom) - - return stackView - } - - private func wrapViewsInHorizontalStack(subviews: [UIView]) -> UIView { - assert(subviews.count > 0) - - let stackView = UIView() - - var lastView: UIView? - for subview in subviews { - - stackView.addSubview(subview) - subview.autoVCenterInSuperview() - - if lastView == nil { - subview.autoPinEdge(toSuperviewEdge: .left) - } else { - subview.autoPinEdge(.left, to: .right, of: lastView!, withOffset: 10) - } - - lastView = subview - } - - lastView?.autoPinEdge(toSuperviewEdge: .right) - - return stackView - } - -// 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 { - createGenericPreview() - return - } - - audioPlayer = OWSAudioPlayer(mediaUrl: dataUrl, audioBehavior: .playback, delegate: self) + private func setupViews() { + // Plain text will just be put in the 'message' input so do nothing + guard attachment.fileType != .text && attachment.fileType != .oversizeText else { return } - imageView.image = UIImage(named: "FileLarge")?.withRenderingMode(.alwaysTemplate) - imageView.tintColor = Colors.text - fileTypeImageView.image = UIImage(named: "table_ic_notification_sound")? - .withRenderingMode(.alwaysTemplate) - fileTypeImageView.tintColor = Colors.text - setAudioIconToPlay() - - self.addSubview(stackView) - self.addSubview(audioPlayPauseButton) + // Setup the view hierarchy + addSubview(stackView) + addSubview(loadingView) + addSubview(videoPlayButton) stackView.addArrangedSubview(imageView) - stackView.addArrangedSubview(UIView.vSpacer(0)) + stackView.addArrangedSubview(animatedImageView) + if !titleLabel.isHidden { stackView.addArrangedSubview(UIView.vhSpacer(10, 10)) } stackView.addArrangedSubview(titleLabel) stackView.addArrangedSubview(fileSizeLabel) imageView.addSubview(fileTypeImageView) - let imageSize: CGFloat = { + // Type-specific configurations + switch attachment.fileType { + case .animatedImage: animatedImageView.isHidden = false + case .image: imageView.isHidden = false + + case .video: + // Note: The 'attachmentApproval' mode provides it's own play button to keep + // it at the proper scale when zooming + imageView.isHidden = false + videoPlayButton.isHidden = (mode == .attachmentApproval) + + case .audio: + // Hide the 'audioPlayPauseButton' if the 'audioPlayer' failed to get created + imageView.isHidden = false + audioPlayPauseButton.isHidden = (audioPlayer == nil) + setAudioIconToPlay() + + fileTypeImageView.image = UIImage(named: "table_ic_notification_sound")? + .withRenderingMode(.alwaysTemplate) + fileTypeImageView.tintColor = Colors.text + fileTypeImageView.isHidden = false + + // Note: There is an annoying bug where the MediaMessageView will fill the screen if the + // 'audioPlayPauseButton' is added anywhere within the view hierarchy causing issues with + // the min scale on 'image' and 'animatedImage' file types (assume it's actually any UIButton) + addSubview(audioPlayPauseButton) + + case .url: + imageView.isHidden = false + imageView.alpha = 0 // Not 'isHidden' because we want it to take up space in the UIStackView + loadingView.isHidden = false + + if let linkPreviewUrl: String = linkPreviewInfo?.url { + // Don't want to change the axis until we have a URL to start loading, otherwise the + // error message will be broken + stackView.axis = .horizontal + + loadLinkPreview(linkPreviewURL: linkPreviewUrl) + } + + default: imageView.isHidden = false + } + } + + private func setupLayout() { + // Sizing calculations + let clampedRatio: CGFloat = { + switch attachment.fileType { + case .url: return 1 + + case .image, .video, .audio, .unknown: + let imageSize: CGSize = (imageView.image?.size ?? CGSize(width: 1, height: 1)) + let aspectRatio: CGFloat = (imageSize.width / imageSize.height) + + return CGFloatClamp(aspectRatio, 0.05, 95.0) + + case .animatedImage: + let imageSize: CGSize = (animatedImageView.image?.size ?? CGSize(width: 1, height: 1)) + let aspectRatio: CGFloat = (imageSize.width / imageSize.height) + + return CGFloatClamp(aspectRatio, 0.05, 95.0) + + default: return 0 + } + }() + + let maybeImageSize: CGFloat? = { + switch attachment.fileType { + case .image, .video: + if validImage != nil { return nil } + + // If we don't have a valid image then use the 'generic' case + break + + case .animatedImage: + if validAnimatedImage != nil { return nil } + + // If we don't have a valid image then use the 'generic' case + break + + case .url: + switch mode { + case .large: return 120 + case .attachmentApproval, .small: return 80 + } + + // Use the 'generic' case for these + case .audio, .unknown: break + + default: return nil + } + + // Generic file size switch mode { case .large: return 200 - case .attachmentApproval: return 150 + case .attachmentApproval: return 120 case .small: return 80 } }() + + let imageSize: CGFloat = (maybeImageSize ?? 0) let audioButtonSize: CGFloat = (imageSize / 2.5) audioPlayPauseButton.layer.cornerRadius = (audioButtonSize / 2) + // Actual layout NSLayoutConstraint.activate([ + stackView.centerXAnchor.constraint(equalTo: centerXAnchor), stackView.centerYAnchor.constraint(equalTo: centerYAnchor), - stackView.widthAnchor.constraint(equalTo: widthAnchor), stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor), - imageView.widthAnchor.constraint(equalToConstant: imageSize), - imageView.heightAnchor.constraint(equalToConstant: imageSize), - titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), - fileSizeLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), + (maybeImageSize != nil ? + stackView.widthAnchor.constraint( + equalTo: widthAnchor, + constant: (attachment.isUrl ? -(32 * 2) : 0) // Inset stackView for urls + ) : + stackView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor) + ), + + imageView.widthAnchor.constraint( + equalTo: imageView.heightAnchor, + multiplier: clampedRatio + ), + animatedImageView.widthAnchor.constraint( + equalTo: animatedImageView.heightAnchor, + multiplier: clampedRatio + ), + + // Note: AnimatedImage, Image and Video types should allow zooming so be lessThanOrEqualTo + // the view size but some other types should have specific sizes + animatedImageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor), + animatedImageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor), + + (maybeImageSize != nil ? + imageView.widthAnchor.constraint(equalToConstant: imageSize) : + imageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor) + ), + (maybeImageSize != nil ? + imageView.heightAnchor.constraint(equalToConstant: imageSize) : + imageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor) + ), fileTypeImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), fileTypeImageView.centerYAnchor.constraint( @@ -339,228 +490,41 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { ), fileTypeImageView.widthAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 0.5), - audioPlayPauseButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), - audioPlayPauseButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), - audioPlayPauseButton.widthAnchor.constraint(equalToConstant: audioButtonSize), - audioPlayPauseButton.heightAnchor.constraint(equalToConstant: audioButtonSize) - ]) - } + videoPlayButton.centerXAnchor.constraint(equalTo: centerXAnchor), + videoPlayButton.centerYAnchor.constraint(equalTo: centerYAnchor), - private func createAnimatedPreview() { - guard attachment.isValidImage else { - createGenericPreview() - return - } - guard let dataUrl = attachment.dataUrl else { - createGenericPreview() - return - } - guard let image = YYImage(contentsOfFile: dataUrl.path) else { - createGenericPreview() - return - } - guard image.size.width > 0 && image.size.height > 0 else { - createGenericPreview() - return - } - animatedImageView.image = image - 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 createImagePreview() { - guard attachment.isValidImage else { - createGenericPreview() - return - } - guard let image = attachment.image() else { - createGenericPreview() - return - } - guard image.size.width > 0 && image.size.height > 0 else { - createGenericPreview() - return - } - - imageView.image = image -// imageView.layer.minificationFilter = .trilinear -// imageView.layer.magnificationFilter = .trilinear - - let aspectRatio = image.size.width / image.size.height - 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() { - guard attachment.isValidVideo else { - createGenericPreview() - return - } - guard let image = attachment.videoPreview() else { - createGenericPreview() - return - } - guard image.size.width > 0 && image.size.height > 0 else { - createGenericPreview() - return - } - - imageView.image = image - self.addSubview(imageView) - - let aspectRatio = image.size.width / image.size.height - let clampedRatio: CGFloat = CGFloatClamp(aspectRatio, 0.05, 95.0) - - 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 - // at the proper zoom scale. - if mode != .attachmentApproval { - self.addSubview(videoPlayButton) - - 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 { - titleLabel.text = "vc_share_link_previews_disabled_title".localized() - titleLabel.isHidden = false - - fileSizeLabel.text = "vc_share_link_previews_disabled_explanation".localized() - fileSizeLabel.textColor = Colors.text - fileSizeLabel.numberOfLines = 0 - - self.addSubview(stackView) - - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(UIView.vSpacer(10)) - stackView.addArrangedSubview(fileSizeLabel) - - NSLayoutConstraint.activate([ - stackView.centerXAnchor.constraint(equalTo: centerXAnchor), - stackView.centerYAnchor.constraint(equalTo: centerYAnchor), - stackView.widthAnchor.constraint(equalTo: widthAnchor, constant: -(32 * 2)), - stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor) - ]) - return - } - - linkPreviewInfo = (url: linkPreviewURL, draft: nil) - - stackView.axis = .horizontal - stackView.distribution = .fill - - imageView.clipsToBounds = true - imageView.image = UIImage(named: "Link")?.withTint(Colors.text) - imageView.alpha = 0 // Not 'isHidden' because we want it to take up space in the UIStackView - imageView.contentMode = .center - imageView.backgroundColor = (isDarkMode ? .black : UIColor.black.withAlphaComponent(0.06)) - imageView.layer.cornerRadius = 8 - - loadingView.isHidden = false - loadingView.startAnimating() - - titleLabel.font = .boldSystemFont(ofSize: Values.smallFontSize) - titleLabel.text = linkPreviewURL - titleLabel.textAlignment = .left - titleLabel.numberOfLines = 2 - titleLabel.isHidden = false - - self.addSubview(stackView) - self.addSubview(loadingView) - - stackView.addArrangedSubview(imageView) - stackView.addArrangedSubview(UIView.vhSpacer(10, 0)) - stackView.addArrangedSubview(titleLabel) - - let imageSize: CGFloat = { - switch mode { - case .large: return 120 - case .attachmentApproval, .small: return 80 - } - }() - - NSLayoutConstraint.activate([ - stackView.centerXAnchor.constraint(equalTo: centerXAnchor), - stackView.centerYAnchor.constraint(equalTo: centerYAnchor), - stackView.widthAnchor.constraint(equalTo: widthAnchor, constant: -(32 * 2)), - stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor), - - imageView.widthAnchor.constraint(equalToConstant: imageSize), - imageView.heightAnchor.constraint(equalToConstant: imageSize), - loadingView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), loadingView.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), loadingView.widthAnchor.constraint(equalToConstant: ceil(imageSize / 3)), loadingView.heightAnchor.constraint(equalToConstant: ceil(imageSize / 3)) ]) - // Build the link preview + // No inset for the text for URLs but there is for all other layouts + if (attachment.fileType != .url) { + NSLayoutConstraint.activate([ + titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)), + fileSizeLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)) + ]) + } + + // Note: There is an annoying bug where the MediaMessageView will fill the screen if the + // 'audioPlayPauseButton' is added anywhere within the view hierarchy causing issues with + // the min scale on 'image' and 'animatedImage' file types (assume it's actually any UIButton) + if attachment.fileType == .audio { + NSLayoutConstraint.activate([ + audioPlayPauseButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), + audioPlayPauseButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), + audioPlayPauseButton.widthAnchor.constraint(equalToConstant: audioButtonSize), + audioPlayPauseButton.heightAnchor.constraint(equalToConstant: audioButtonSize), + ]) + } + } + + // MARK: - Link Loading + + private func loadLinkPreview(linkPreviewURL: String) { + loadingView.startAnimating() + OWSLinkPreview.tryToBuildPreviewInfo(previewUrl: linkPreviewURL) .done { [weak self] draft in // TODO: Look at refactoring this behaviour to consolidate attachment mutations @@ -580,8 +544,9 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { } .catch { [weak self] _ in self?.titleLabel.attributedText = NSMutableAttributedString(string: linkPreviewURL) + .rtlSafeAppend("\n") .rtlSafeAppend( - "\n\("vc_share_link_previews_error".localized())", + "vc_share_link_previews_error".localized(), attributes: [ NSAttributedString.Key.font: UIFont.ows_regularFont( withSize: Values.verySmallFontSize @@ -597,51 +562,6 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate { .retainUntilComplete() } - private func createGenericPreview() { - imageView.image = UIImage(named: "FileLarge") - - self.addSubview(stackView) - - stackView.addArrangedSubview(imageView) - stackView.addArrangedSubview(UIView.vSpacer(5)) - stackView.addArrangedSubview(titleLabel) - stackView.addArrangedSubview(fileSizeLabel) - - imageView.addSubview(fileTypeImageView) - - let imageSize: CGFloat = { - switch mode { - case .large: return 200 - case .attachmentApproval: return 150 - case .small: return 80 - } - }() - - NSLayoutConstraint.activate([ - stackView.centerYAnchor.constraint(equalTo: centerYAnchor), - stackView.widthAnchor.constraint(equalTo: widthAnchor), - stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor), - - imageView.widthAnchor.constraint(equalToConstant: imageSize), - imageView.heightAnchor.constraint(equalToConstant: imageSize), - 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 - ) - ]) - } - // MARK: - Event Handlers @objc func audioPlayPauseButtonPressed(sender: UIButton) {