mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
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:
parent
67ad965859
commit
e6c90c5e18
3 changed files with 379 additions and 423 deletions
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue