Finished off UI clean up

Fixed a couple more vertical alignment issues with certain attachment types.
Finished cleaning up the MediaMessageView UI code (removed old code).
This commit is contained in:
Morgan Pretty 2022-01-13 14:49:14 +11:00
parent 67ad965859
commit e6c90c5e18
3 changed files with 379 additions and 423 deletions

View file

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

View file

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

View file

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