Fixed a couple of bugs and started UI refactoring

Refactored the UI creation and layout code in the attachments UI.
Started refactoring the UI in the MediaMessageView (converting the existing stuff and will then consolidate when done).
Fixed a bug where playing a video attachment would result in the zoom continually getting reset.
Fixed a bug where the attachment zoom scale would randomly change causing odd behaviours.
pull/548/head
Morgan Pretty 3 years ago
parent dd9eeb5d61
commit 61f809caee

@ -778,6 +778,7 @@
FD705A90278CEBBC00F16121 /* ShareAppExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */; }; FD705A90278CEBBC00F16121 /* ShareAppExtensionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */; };
FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; }; FD705A92278D051200F16121 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A91278D051200F16121 /* ReusableView.swift */; };
FD705A94278D052B00F16121 /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */; }; FD705A94278D052B00F16121 /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */; };
FD705A98278E9F4D00F16121 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@ -1784,6 +1785,7 @@
FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppExtensionContext.swift; sourceTree = "<group>"; }; FD705A8F278CEBBC00F16121 /* ShareAppExtensionContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAppExtensionContext.swift; sourceTree = "<group>"; };
FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = "<group>"; }; FD705A91278D051200F16121 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = "<group>"; };
FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = "<group>"; }; FD705A93278D052B00F16121 /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = "<group>"; };
FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+Extensions.swift"; sourceTree = "<group>"; };
FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = "<group>"; }; FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -3321,6 +3323,7 @@
C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */, C38EF2B1255B6D9C007E1867 /* UIViewController+Utilities.swift */,
C38EF307255B6DBE007E1867 /* UIGestureRecognizer+OWS.swift */, C38EF307255B6DBE007E1867 /* UIGestureRecognizer+OWS.swift */,
C38EF2F3255B6DBC007E1867 /* UIImage+OWS.swift */, C38EF2F3255B6DBC007E1867 /* UIImage+OWS.swift */,
FD705A97278E9F4D00F16121 /* UIColor+Extensions.swift */,
C38EF30A255B6DBE007E1867 /* UIUtil.h */, C38EF30A255B6DBE007E1867 /* UIUtil.h */,
C38EF300255B6DBD007E1867 /* UIUtil.m */, C38EF300255B6DBD007E1867 /* UIUtil.m */,
C38EF239255B6D66007E1867 /* UIFont+OWS.h */, C38EF239255B6D66007E1867 /* UIFont+OWS.h */,
@ -4445,6 +4448,7 @@
C38EF248255B6D67007E1867 /* UIViewController+OWS.m in Sources */, C38EF248255B6D67007E1867 /* UIViewController+OWS.m in Sources */,
C38EF272255B6D7A007E1867 /* OWSResaveCollectionDBMigration.m in Sources */, C38EF272255B6D7A007E1867 /* OWSResaveCollectionDBMigration.m in Sources */,
C33FDD3A255A582000E217F9 /* Notification+Loki.swift in Sources */, C33FDD3A255A582000E217F9 /* Notification+Loki.swift in Sources */,
FD705A98278E9F4D00F16121 /* UIColor+Extensions.swift in Sources */,
C38EF276255B6D7A007E1867 /* OWSDatabaseMigration.m in Sources */, C38EF276255B6D7A007E1867 /* OWSDatabaseMigration.m in Sources */,
C38EF370255B6DCC007E1867 /* OWSNavigationController.m in Sources */, C38EF370255B6DCC007E1867 /* OWSNavigationController.m in Sources */,
C38EF24E255B6D67007E1867 /* Collection+OWS.swift in Sources */, C38EF24E255B6D67007E1867 /* Collection+OWS.swift in Sources */,

@ -7,7 +7,7 @@ import UIKit
import AVFoundation import AVFoundation
import SessionUIKit import SessionUIKit
protocol AttachmentPrepViewControllerDelegate: class { protocol AttachmentPrepViewControllerDelegate: AnyObject {
func prepViewControllerUpdateNavigationBar() func prepViewControllerUpdateNavigationBar()
func prepViewControllerUpdateControls() func prepViewControllerUpdateControls()
@ -31,13 +31,102 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
return attachmentItem.attachment return attachmentItem.attachment
} }
private var videoPlayer: OWSVideoPlayer? private lazy var videoPlayer: OWSVideoPlayer? = {
guard let videoURL = attachment.dataUrl else {
owsFailDebug("Missing videoURL")
return nil
}
private(set) var mediaMessageView: MediaMessageView! let player: OWSVideoPlayer = OWSVideoPlayer(url: videoURL)
private(set) var scrollView: UIScrollView! player.delegate = self
private(set) var contentContainer: UIView!
private(set) var playVideoButton: UIView? return player
private var imageEditorView: ImageEditorView? }()
// MARK: - UI
private lazy var scrollView: UIScrollView = {
// Scroll View - used to zoom/pan on images and video
let scrollView: UIScrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.delegate = self
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
// Panning should stop pretty soon after the user stops scrolling
scrollView.decelerationRate = UIScrollView.DecelerationRate.fast
// If the content isn't zoomable then inset the content so it appears centered
if !isZoomable {
scrollView.isScrollEnabled = false
scrollView.contentInset = UIEdgeInsets(
top: 0,
leading: 0,
bottom: (AttachmentTextToolbar.kMinTextViewHeight + (AttachmentTextToolbar.kToolbarMargin * 2)),
trailing: 0
)
}
return scrollView
}()
private lazy var contentContainerView: UIView = {
// Anything that should be shrunk when user pops keyboard lives in the contentContainer.
let view: UIView = UIView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var mediaMessageView: MediaMessageView = {
let view: MediaMessageView = MediaMessageView(attachment: attachment, mode: .attachmentApproval)
view.translatesAutoresizingMaskIntoConstraints = false
view.isHidden = (imageEditorView != nil)
return view
}()
private lazy var imageEditorView: ImageEditorView? = {
guard let imageEditorModel = attachmentItem.imageEditorModel else { return nil }
let view: ImageEditorView = ImageEditorView(model: imageEditorModel, delegate: self)
view.translatesAutoresizingMaskIntoConstraints = false
guard view.configureSubviews() else { return nil }
return view
}()
private lazy var videoPlayerView: VideoPlayerView? = {
guard let videoPlayer: OWSVideoPlayer = videoPlayer else { return nil }
let view: VideoPlayerView = VideoPlayerView()
view.translatesAutoresizingMaskIntoConstraints = false
view.player = videoPlayer.avPlayer
let pauseGesture = UITapGestureRecognizer(target: self, action: #selector(didTapPlayerView(_:)))
view.addGestureRecognizer(pauseGesture)
return view
}()
private lazy var progressBar: PlayerProgressBar = {
let progressBar: PlayerProgressBar = PlayerProgressBar()
progressBar.player = videoPlayer?.avPlayer
progressBar.delegate = self
return progressBar
}()
private lazy var playVideoButton: UIButton = {
let button: UIButton = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.contentMode = .scaleAspectFit
button.setBackgroundImage(#imageLiteral(resourceName: "CirclePlay"), for: .normal)
button.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside)
return button
}()
public var shouldHideControls: Bool { public var shouldHideControls: Bool {
guard let imageEditorView = imageEditorView else { guard let imageEditorView = imageEditorView else {
@ -61,143 +150,120 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
} }
// MARK: - View Lifecycle // MARK: - View Lifecycle
override public func loadView() { public override func viewDidLoad() {
self.view = UIView() super.viewDidLoad()
self.mediaMessageView = MediaMessageView(attachment: attachment, mode: .attachmentApproval) view.backgroundColor = Colors.navigationBarBackground
// Anything that should be shrunk when user pops keyboard lives in the contentContainer. view.addSubview(contentContainerView)
let contentContainer = UIView()
self.contentContainer = contentContainer contentContainerView.addSubview(scrollView)
view.addSubview(contentContainer) scrollView.addSubview(mediaMessageView)
contentContainer.autoPinEdgesToSuperviewEdges()
if let editorView: ImageEditorView = imageEditorView {
// Scroll View - used to zoom/pan on images and video view.addSubview(editorView)
scrollView = UIScrollView()
contentContainer.addSubview(scrollView) imageEditorUpdateNavigationBar()
scrollView.delegate = self
scrollView.showsHorizontalScrollIndicator = false
scrollView.showsVerticalScrollIndicator = false
// Panning should stop pretty soon after the user stops scrolling
scrollView.decelerationRate = UIScrollView.DecelerationRate.fast
// We want scroll view content up and behind the system status bar content
// but we want other content (e.g. bar buttons) to respect the top layout guide.
self.automaticallyAdjustsScrollViewInsets = false
scrollView.autoPinEdgesToSuperviewEdges()
let backgroundColor = Colors.navigationBarBackground
self.view.backgroundColor = backgroundColor
// Create full screen container view so the scrollView
// can compute an appropriate content size in which to center
// our media view.
let containerView = UIView.container()
scrollView.addSubview(containerView)
containerView.autoPinEdgesToSuperviewEdges()
containerView.autoMatch(.height, to: .height, of: self.view)
containerView.autoMatch(.width, to: .width, of: self.view)
containerView.addSubview(mediaMessageView)
mediaMessageView.autoPinEdgesToSuperviewEdges()
if let imageEditorModel = attachmentItem.imageEditorModel {
let imageEditorView = ImageEditorView(model: imageEditorModel, delegate: self)
if imageEditorView.configureSubviews() {
self.imageEditorView = imageEditorView
mediaMessageView.isHidden = true
view.addSubview(imageEditorView)
imageEditorView.autoPinEdgesToSuperviewEdges()
imageEditorUpdateNavigationBar()
}
} }
// Hide the play button embedded in the MediaView and replace it with our own. // Hide the play button embedded in the MediaView and replace it with our own.
// This allows us to zoom in on the media view without zooming in on the button // This allows us to zoom in on the media view without zooming in on the button
if attachment.isVideo { // TODO: This for both Audio and Video?
if attachment.isVideo, let playerView: VideoPlayerView = videoPlayerView {
guard let videoURL = attachment.dataUrl else { mediaMessageView.videoPlayButton.isHidden = true
owsFailDebug("Missing videoURL") mediaMessageView.addSubview(playerView)
return
}
let player = OWSVideoPlayer(url: videoURL)
self.videoPlayer = player
player.delegate = self
let playerView = VideoPlayerView()
playerView.player = player.avPlayer
self.mediaMessageView.addSubview(playerView)
playerView.autoPinEdgesToSuperviewEdges()
let pauseGesture = UITapGestureRecognizer(target: self, action: #selector(didTapPlayerView(_:)))
playerView.addGestureRecognizer(pauseGesture)
let progressBar = PlayerProgressBar()
progressBar.player = player.avPlayer
progressBar.delegate = self
// we don't want the progress bar to zoom during "pinch-to-zoom" // we don't want the progress bar to zoom during "pinch-to-zoom"
// but we do want it to shrink with the media content when the user // but we do want it to shrink with the media content when the user
// pops the keyboard. // pops the keyboard.
contentContainer.addSubview(progressBar) contentContainerView.addSubview(progressBar)
contentContainerView.addSubview(playVideoButton)
progressBar.autoPinEdge(.top, to: .top, of: view)
progressBar.autoPinWidthToSuperview()
progressBar.autoSetDimension(.height, toSize: 44)
self.mediaMessageView.videoPlayButton?.isHidden = true
let playButton = UIButton()
self.playVideoButton = playButton
playButton.accessibilityLabel = NSLocalizedString("PLAY_BUTTON_ACCESSABILITY_LABEL", comment: "Accessibility label for button to start media playback")
playButton.setBackgroundImage(#imageLiteral(resourceName: "CirclePlay"), for: .normal)
playButton.contentMode = .scaleAspectFit
playButton.autoSetDimension(.width, toSize: 72)
playButton.autoSetDimension(.height, toSize: 72)
let playButtonWidth = ScaleFromIPhone5(70)
playButton.autoSetDimensions(to: CGSize(width: playButtonWidth, height: playButtonWidth))
self.contentContainer.addSubview(playButton)
playButton.addTarget(self, action: #selector(playButtonTapped), for: .touchUpInside)
playButton.autoCenterInSuperview()
} }
setupLayout()
} }
override public func viewWillAppear(_ animated: Bool) { override public func viewWillAppear(_ animated: Bool) {
Logger.debug("")
super.viewWillAppear(animated) super.viewWillAppear(animated)
prepDelegate?.prepViewControllerUpdateNavigationBar() prepDelegate?.prepViewControllerUpdateNavigationBar()
prepDelegate?.prepViewControllerUpdateControls() prepDelegate?.prepViewControllerUpdateControls()
} }
override public func viewDidAppear(_ animated: Bool) { override public func viewDidAppear(_ animated: Bool) {
Logger.debug("")
super.viewDidAppear(animated) super.viewDidAppear(animated)
prepDelegate?.prepViewControllerUpdateNavigationBar() prepDelegate?.prepViewControllerUpdateNavigationBar()
prepDelegate?.prepViewControllerUpdateControls() prepDelegate?.prepViewControllerUpdateControls()
} }
override public func viewWillLayoutSubviews() { override public func viewWillLayoutSubviews() {
Logger.debug("")
super.viewWillLayoutSubviews() super.viewWillLayoutSubviews()
// e.g. if flipping to/from landscape setupZoomScale()
updateMinZoomScaleForSize(view.bounds.size)
ensureAttachmentViewScale(animated: false) ensureAttachmentViewScale(animated: false)
} }
public override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// Note: Need to do this here to ensure it's based on the final sizing
// otherwise the offsets will be slightly off
resetContentInset()
}
// MARK: - Layout
private func setupLayout() {
NSLayoutConstraint.activate([
contentContainerView.topAnchor.constraint(equalTo: view.topAnchor),
contentContainerView.leftAnchor.constraint(equalTo: view.leftAnchor),
contentContainerView.rightAnchor.constraint(equalTo: view.rightAnchor),
contentContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
scrollView.topAnchor.constraint(equalTo: contentContainerView.topAnchor),
scrollView.leftAnchor.constraint(equalTo: contentContainerView.leftAnchor),
scrollView.rightAnchor.constraint(equalTo: contentContainerView.rightAnchor),
scrollView.bottomAnchor.constraint(equalTo: contentContainerView.bottomAnchor),
mediaMessageView.topAnchor.constraint(equalTo: scrollView.topAnchor),
mediaMessageView.leftAnchor.constraint(equalTo: scrollView.leftAnchor),
mediaMessageView.rightAnchor.constraint(equalTo: scrollView.rightAnchor),
mediaMessageView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor),
mediaMessageView.widthAnchor.constraint(equalTo: view.widthAnchor),
mediaMessageView.heightAnchor.constraint(equalTo: view.heightAnchor)
])
if let editorView: ImageEditorView = imageEditorView {
NSLayoutConstraint.activate([
editorView.topAnchor.constraint(equalTo: view.topAnchor),
editorView.leftAnchor.constraint(equalTo: view.leftAnchor),
editorView.rightAnchor.constraint(equalTo: view.rightAnchor),
editorView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
}
if attachment.isVideo, let playerView: VideoPlayerView = videoPlayerView {
let playButtonSize: CGFloat = ScaleFromIPhone5(70)
NSLayoutConstraint.activate([
playerView.topAnchor.constraint(equalTo: mediaMessageView.topAnchor),
playerView.leftAnchor.constraint(equalTo: mediaMessageView.leftAnchor),
playerView.rightAnchor.constraint(equalTo: mediaMessageView.rightAnchor),
playerView.bottomAnchor.constraint(equalTo: mediaMessageView.bottomAnchor),
progressBar.topAnchor.constraint(equalTo: view.topAnchor),
progressBar.widthAnchor.constraint(equalTo: contentContainerView.widthAnchor),
progressBar.heightAnchor.constraint(equalToConstant: 44),
playVideoButton.centerXAnchor.constraint(equalTo: contentContainerView.centerXAnchor),
playVideoButton.centerYAnchor.constraint(equalTo: contentContainerView.centerYAnchor),
playVideoButton.widthAnchor.constraint(equalToConstant: playButtonSize),
playVideoButton.heightAnchor.constraint(equalToConstant: playButtonSize),
])
}
}
// MARK: - Navigation Bar // MARK: - Navigation Bar
@ -205,39 +271,33 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
guard let imageEditorView = imageEditorView else { guard let imageEditorView = imageEditorView else {
return [] return []
} }
return imageEditorView.navigationBarItems() return imageEditorView.navigationBarItems()
} }
// MARK: - Event Handlers // MARK: - Event Handlers
@objc @objc public func didTapPlayerView(_ gestureRecognizer: UIGestureRecognizer) {
public func didTapPlayerView(_ gestureRecognizer: UIGestureRecognizer) {
assert(self.videoPlayer != nil) assert(self.videoPlayer != nil)
self.pauseVideo() self.pauseVideo()
} }
@objc @objc public func playButtonTapped() {
public func playButtonTapped() {
self.playVideo() self.playVideo()
} }
// MARK: - Video // MARK: - Video
private func playVideo() { private func playVideo() {
Logger.info("")
guard let videoPlayer = self.videoPlayer else { guard let videoPlayer = self.videoPlayer else {
owsFailDebug("video player was unexpectedly nil") owsFailDebug("video player was unexpectedly nil")
return return
} }
guard let playVideoButton = self.playVideoButton else { UIView.animate(withDuration: 0.1) { [weak self] in
owsFailDebug("playVideoButton was unexpectedly nil") self?.playVideoButton.alpha = 0.0
return
}
UIView.animate(withDuration: 0.1) {
playVideoButton.alpha = 0.0
} }
videoPlayer.play() videoPlayer.play()
} }
@ -248,24 +308,15 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
} }
videoPlayer.pause() videoPlayer.pause()
guard let playVideoButton = self.playVideoButton else {
owsFailDebug("playVideoButton was unexpectedly nil") UIView.animate(withDuration: 0.1) { [weak self] in
return self?.playVideoButton.alpha = 1.0
}
UIView.animate(withDuration: 0.1) {
playVideoButton.alpha = 1.0
} }
} }
@objc @objc public func videoPlayerDidPlayToCompletion(_ videoPlayer: OWSVideoPlayer) {
public func videoPlayerDidPlayToCompletion(_ videoPlayer: OWSVideoPlayer) { UIView.animate(withDuration: 0.1) { [weak self] in
guard let playVideoButton = self.playVideoButton else { self?.playVideoButton.alpha = 1.0
owsFailDebug("playVideoButton was unexpectedly nil")
return
}
UIView.animate(withDuration: 0.1) {
playVideoButton.alpha = 1.0
} }
} }
@ -274,6 +325,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
owsFailDebug("video player was unexpectedly nil") owsFailDebug("video player was unexpectedly nil")
return return
} }
videoPlayer.pause() videoPlayer.pause()
} }
@ -315,6 +367,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
var shouldAllowAttachmentViewResizing: Bool = true var shouldAllowAttachmentViewResizing: Bool = true
var attachmentViewScale: AttachmentViewScale = .fullsize var attachmentViewScale: AttachmentViewScale = .fullsize
public func setAttachmentViewScale(_ attachmentViewScale: AttachmentViewScale, animated: Bool) { public func setAttachmentViewScale(_ attachmentViewScale: AttachmentViewScale, animated: Bool) {
self.attachmentViewScale = attachmentViewScale self.attachmentViewScale = attachmentViewScale
ensureAttachmentViewScale(animated: animated) ensureAttachmentViewScale(animated: animated)
@ -323,9 +376,9 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
func ensureAttachmentViewScale(animated: Bool) { func ensureAttachmentViewScale(animated: Bool) {
let animationDuration = animated ? 0.2 : 0 let animationDuration = animated ? 0.2 : 0
guard shouldAllowAttachmentViewResizing else { guard shouldAllowAttachmentViewResizing else {
if self.contentContainer.transform != CGAffineTransform.identity { if self.contentContainerView.transform != CGAffineTransform.identity {
UIView.animate(withDuration: animationDuration) { UIView.animate(withDuration: animationDuration) {
self.contentContainer.transform = CGAffineTransform.identity self.contentContainerView.transform = CGAffineTransform.identity
} }
} }
return return
@ -333,14 +386,14 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
switch attachmentViewScale { switch attachmentViewScale {
case .fullsize: case .fullsize:
guard self.contentContainer.transform != .identity else { guard self.contentContainerView.transform != .identity else {
return return
} }
UIView.animate(withDuration: animationDuration) { UIView.animate(withDuration: animationDuration) {
self.contentContainer.transform = CGAffineTransform.identity self.contentContainerView.transform = CGAffineTransform.identity
} }
case .compact: case .compact:
guard self.contentContainer.transform == .identity else { guard self.contentContainerView.transform == .identity else {
return return
} }
UIView.animate(withDuration: animationDuration) { UIView.animate(withDuration: animationDuration) {
@ -354,7 +407,7 @@ public class AttachmentPrepViewController: OWSViewController, PlayerProgressBarD
let heightDelta = originalHeight * (1 - kScaleFactor) let heightDelta = originalHeight * (1 - kScaleFactor)
let translate = CGAffineTransform(translationX: 0, y: -heightDelta / 2) let translate = CGAffineTransform(translationX: 0, y: -heightDelta / 2)
self.contentContainer.transform = scale.concatenating(translate) self.contentContainerView.transform = scale.concatenating(translate)
} }
} }
} }
@ -367,66 +420,55 @@ extension AttachmentPrepViewController: UIScrollViewDelegate {
public func viewForZooming(in scrollView: UIScrollView) -> UIView? { public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
if isZoomable { if isZoomable {
return mediaMessageView return mediaMessageView
} else {
// don't zoom for audio or generic attachments.
return nil
} }
// Don't zoom for audio or generic attachments.
return nil
}
public func scrollViewDidZoom(_ scrollView: UIScrollView) {
resetContentInset()
} }
fileprivate func updateMinZoomScaleForSize(_ size: CGSize) { fileprivate func setupZoomScale() {
Logger.debug("") // We only want to setup the zoom scale once (otherwise we get glitchy behaviour
// when anything forces a re-layout)
guard abs(scrollView.maximumZoomScale - 1.0) <= CGFloat.leastNormalMagnitude else {
return
}
// Ensure bounds have been computed // Ensure bounds have been computed
mediaMessageView.layoutIfNeeded()
guard mediaMessageView.bounds.width > 0, mediaMessageView.bounds.height > 0 else { guard mediaMessageView.bounds.width > 0, mediaMessageView.bounds.height > 0 else {
Logger.warn("bad bounds") Logger.warn("bad bounds")
return return
} }
let widthScale = size.width / mediaMessageView.bounds.width let widthScale: CGFloat = (view.bounds.size.width / mediaMessageView.bounds.width)
let heightScale = size.height / mediaMessageView.bounds.height let heightScale: CGFloat = (view.bounds.size.height / mediaMessageView.bounds.height)
let minScale = min(widthScale, heightScale) let minScale: CGFloat = min(widthScale, heightScale)
scrollView.maximumZoomScale = minScale * 5.0
scrollView.minimumZoomScale = minScale scrollView.minimumZoomScale = minScale
scrollView.maximumZoomScale = (minScale * 5)
scrollView.zoomScale = minScale scrollView.zoomScale = minScale
} }
// Keep the media view centered within the scroll view as you zoom // Allow the user to zoom out to 100% of the attachment size if it's smaller
public func scrollViewDidZoom(_ scrollView: UIScrollView) { // than the screen
// The scroll view has zoomed, so you need to re-center the contents fileprivate func resetContentInset() {
let scrollViewSize = self.scrollViewVisibleSize guard isZoomable else {
scrollView.contentOffset = CGPoint(x: 0, y: scrollView.contentInset.bottom)
// First assume that mediaMessageView center coincides with the contents center return
// This is correct when the mediaMessageView is bigger than scrollView due to zoom
var contentCenter = CGPoint(x: (scrollView.contentSize.width / 2), y: (scrollView.contentSize.height / 2))
let scrollViewCenter = self.scrollViewCenter
// if mediaMessageView is smaller than the scrollView visible size - fix the content center accordingly
if self.scrollView.contentSize.width < scrollViewSize.width {
contentCenter.x = scrollViewCenter.x
}
if self.scrollView.contentSize.height < scrollViewSize.height {
contentCenter.y = scrollViewCenter.y
} }
self.mediaMessageView.center = contentCenter let offsetX: CGFloat = max((scrollView.bounds.width - scrollView.contentSize.width) * 0.5, 0)
} let offsetY: CGFloat = max((scrollView.bounds.height - scrollView.contentSize.height) * 0.5, 0)
// return the scroll view center scrollView.contentInset = UIEdgeInsets(
private var scrollViewCenter: CGPoint { top: offsetY,
let size = scrollViewVisibleSize left: offsetX,
return CGPoint(x: (size.width / 2), y: (size.height / 2)) bottom: 0,
} right: 0
)
// Return scrollview size without the area overlapping with tab and nav bar.
private var scrollViewVisibleSize: CGSize {
let contentInset = scrollView.contentInset
let scrollViewSize = scrollView.bounds.standardized.size
let width = scrollViewSize.width - (contentInset.left + contentInset.right)
let height = scrollViewSize.height - (contentInset.top + contentInset.bottom)
return CGSize(width: width, height: height)
} }
} }

@ -32,8 +32,9 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate {
} }
// Layout Constants // Layout Constants
let kMinTextViewHeight: CGFloat = 40 static let kToolbarMargin: CGFloat = 8
static let kMinTextViewHeight: CGFloat = 40
var maxTextViewHeight: CGFloat { var maxTextViewHeight: CGFloat {
// About ~4 lines in portrait and ~3 lines in landscape. // About ~4 lines in portrait and ~3 lines in landscape.
// Otherwise we risk obscuring too much of the content. // Otherwise we risk obscuring too much of the content.
@ -46,7 +47,7 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate {
init() { init() {
self.sendButton = UIButton(type: .system) self.sendButton = UIButton(type: .system)
self.textViewHeight = kMinTextViewHeight self.textViewHeight = AttachmentTextToolbar.kMinTextViewHeight
super.init(frame: CGRect.zero) super.init(frame: CGRect.zero)
@ -77,15 +78,19 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate {
contentView.autoPinEdgesToSuperviewEdges() contentView.autoPinEdgesToSuperviewEdges()
// Layout // Layout
let kToolbarMargin: CGFloat = 8
// We have to wrap the toolbar items in a content view because iOS (at least on iOS10.3) assigns the inputAccessoryView.layoutMargins // We have to wrap the toolbar items in a content view because iOS (at least on iOS10.3) assigns the inputAccessoryView.layoutMargins
// when resigning first responder (verified by auditing with `layoutMarginsDidChange`). // when resigning first responder (verified by auditing with `layoutMarginsDidChange`).
// The effect of this is that if we were to assign these margins to self.layoutMargins, they'd be blown away if the // The effect of this is that if we were to assign these margins to self.layoutMargins, they'd be blown away if the
// user dismisses the keyboard, giving the input accessory view a wonky layout. // user dismisses the keyboard, giving the input accessory view a wonky layout.
contentView.layoutMargins = UIEdgeInsets(top: kToolbarMargin, left: kToolbarMargin, bottom: kToolbarMargin, right: kToolbarMargin) contentView.layoutMargins = UIEdgeInsets(
top: AttachmentTextToolbar.kToolbarMargin,
left: AttachmentTextToolbar.kToolbarMargin,
bottom: AttachmentTextToolbar.kToolbarMargin,
right: AttachmentTextToolbar.kToolbarMargin
)
self.textViewHeightConstraint = textView.autoSetDimension(.height, toSize: kMinTextViewHeight) self.textViewHeightConstraint = textView.autoSetDimension(.height, toSize: AttachmentTextToolbar.kMinTextViewHeight)
// We pin all three edges explicitly rather than doing something like: // We pin all three edges explicitly rather than doing something like:
// textView.autoPinEdges(toSuperviewMarginsExcludingEdge: .right) // textView.autoPinEdges(toSuperviewMarginsExcludingEdge: .right)
@ -97,7 +102,7 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate {
textContainer.autoPinEdge(toSuperviewMargin: .bottom) textContainer.autoPinEdge(toSuperviewMargin: .bottom)
textContainer.autoPinEdge(toSuperviewMargin: .left) textContainer.autoPinEdge(toSuperviewMargin: .left)
sendButton.autoPinEdge(.left, to: .right, of: textContainer, withOffset: kToolbarMargin) sendButton.autoPinEdge(.left, to: .right, of: textContainer, withOffset: AttachmentTextToolbar.kToolbarMargin)
sendButton.autoPinEdge(.bottom, to: .bottom, of: textContainer, withOffset: -3) sendButton.autoPinEdge(.bottom, to: .bottom, of: textContainer, withOffset: -3)
sendButton.autoPinEdge(toSuperviewMargin: .right) sendButton.autoPinEdge(toSuperviewMargin: .right)
@ -170,7 +175,7 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate {
textContainer.layer.borderColor = UIColor.white.cgColor textContainer.layer.borderColor = UIColor.white.cgColor
textContainer.layer.borderWidth = Values.separatorThickness textContainer.layer.borderWidth = Values.separatorThickness
textContainer.layer.cornerRadius = kMinTextViewHeight / 2 textContainer.layer.cornerRadius = (AttachmentTextToolbar.kMinTextViewHeight / 2)
textContainer.clipsToBounds = true textContainer.clipsToBounds = true
textContainer.addSubview(placeholderTextView) textContainer.addSubview(placeholderTextView)
@ -314,6 +319,6 @@ class AttachmentTextToolbar: UIView, UITextViewDelegate {
private func clampedTextViewHeight(fixedWidth: CGFloat) -> CGFloat { private func clampedTextViewHeight(fixedWidth: CGFloat) -> CGFloat {
let contentSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude)) let contentSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
return CGFloatClamp(contentSize.height, kMinTextViewHeight, maxTextViewHeight) return CGFloatClamp(contentSize.height, AttachmentTextToolbar.kMinTextViewHeight, maxTextViewHeight)
} }
} }

@ -6,37 +6,25 @@ import Foundation
import MediaPlayer import MediaPlayer
import YYImage import YYImage
import NVActivityIndicatorView import NVActivityIndicatorView
import SessionUIKit import SessionUIKit
@objc
public enum MediaMessageViewMode: UInt {
case large
case small
case attachmentApproval
}
@objc
public class MediaMessageView: UIView, OWSAudioPlayerDelegate { public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
public enum Mode: UInt {
case large
case small
case attachmentApproval
}
// MARK: Properties // MARK: Properties
@objc public let mode: Mode
public let mode: MediaMessageViewMode
@objc
public let attachment: SignalAttachment public let attachment: SignalAttachment
@objc
public var audioPlayer: OWSAudioPlayer? public var audioPlayer: OWSAudioPlayer?
private var linkPreviewInfo: (url: String, draft: OWSLinkPreviewDraft?)?
@objc
public var audioPlayButton: UIButton?
@objc
public var videoPlayButton: UIImageView?
@objc
public var playbackState = AudioPlaybackState.stopped { public var playbackState = AudioPlaybackState.stopped {
didSet { didSet {
AssertIsOnMainThread() AssertIsOnMainThread()
@ -45,16 +33,12 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
} }
} }
@objc
public var audioProgressSeconds: CGFloat = 0 public var audioProgressSeconds: CGFloat = 0
@objc
public var audioDurationSeconds: CGFloat = 0 public var audioDurationSeconds: CGFloat = 0
@objc
public var contentView: UIView? public var contentView: UIView?
private var linkPreviewInfo: (url: String, draft: OWSLinkPreviewDraft?)?
// MARK: Initializers // MARK: Initializers
@ -65,23 +49,119 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
// Currently we only use one mode (AttachmentApproval), so we could simplify this class, but it's kind // Currently we only use one mode (AttachmentApproval), so we could simplify this class, but it's kind
// of nice that it's written in a flexible way in case we'd want to use it elsewhere again in the future. // of nice that it's written in a flexible way in case we'd want to use it elsewhere again in the future.
@objc public required init(attachment: SignalAttachment, mode: MediaMessageView.Mode) {
public required init(attachment: SignalAttachment, mode: MediaMessageViewMode) { if attachment.hasError { owsFailDebug(attachment.error.debugDescription) }
if attachment.hasError {
owsFailDebug(attachment.error.debugDescription)
}
self.attachment = attachment self.attachment = attachment
self.mode = mode self.mode = mode
super.init(frame: CGRect.zero) super.init(frame: CGRect.zero)
createViews() createViews()
backgroundColor = .red
setupLayout()
} }
deinit { deinit {
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)
} }
// MARK: - Create Views // MARK: - UI
private lazy var stackView: UIStackView = {
let stackView: UIStackView = UIStackView()
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.alignment = .center
stackView.distribution = .equalSpacing
switch mode {
case .large, .attachmentApproval: stackView.spacing = 10
case .small: stackView.spacing = 5
}
return stackView
}()
private lazy var imageView: UIImageView = {
let view: UIImageView = UIImageView()
view.translatesAutoresizingMaskIntoConstraints = false
view.contentMode = .scaleAspectFit
view.layer.minificationFilter = .trilinear
view.layer.magnificationFilter = .trilinear
return view
}()
private lazy var fileTypeImageView: UIImageView = {
let view: UIImageView = UIImageView()
view.translatesAutoresizingMaskIntoConstraints = false
view.layer.minificationFilter = .trilinear
view.layer.magnificationFilter = .trilinear
return view
}()
private lazy var animatedImageView: YYAnimatedImageView = {
let view: YYAnimatedImageView = YYAnimatedImageView()
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
lazy var videoPlayButton: UIImageView = {
let imageView: UIImageView = UIImageView(image: UIImage(named: "CirclePlay"))
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
return imageView
}()
/// Note: This uses different assets from the `videoPlayButton` and has a 'Pause' state
private lazy var audioPlayPauseButton: UIButton = {
let button: UIButton = UIButton()
button.translatesAutoresizingMaskIntoConstraints = false
button.clipsToBounds = true
button.setBackgroundImage(UIColor.white.toImage(), for: .normal)
button.setBackgroundImage(UIColor.white.darken(by: 0.2).toImage(), for: .highlighted)
button.layer.cornerRadius = 30
button.addTarget(self, action: #selector(audioPlayPauseButtonPressed), for: .touchUpInside)
return button
}()
private lazy var titleLabel: UILabel = {
let label: UILabel = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = labelFont()
label.text = (formattedFileName() ?? formattedFileExtension())
label.textColor = controlTintColor
label.textAlignment = .center
label.lineBreakMode = .byTruncatingMiddle
label.isHidden = ((label.text?.count ?? 0) == 0)
return label
}()
private lazy var fileSizeLabel: UILabel = {
let fileSize: UInt = attachment.dataLength
let label: UILabel = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.font = labelFont()
// Format string for file size label in call interstitial view.
// Embeds: {{file size as 'N mb' or 'N kb'}}.
label.text = String(format: "ATTACHMENT_APPROVAL_FILE_SIZE_FORMAT".localized(), OWSFormat.formatFileSize(UInt(fileSize)))
label.textColor = controlTintColor
label.textAlignment = .center
return label
}()
// MARK: - Layout
private func createViews() { private func createViews() {
if attachment.isAnimatedImage { if attachment.isAnimatedImage {
@ -100,7 +180,12 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
createGenericPreview() createGenericPreview()
} }
} }
private func setupLayout() {
// Bottom inset
}
// TODO: Any reason for not just using UIStackView
private func wrapViewsInVerticalStack(subviews: [UIView]) -> UIView { private func wrapViewsInVerticalStack(subviews: [UIView]) -> UIView {
assert(subviews.count > 0) assert(subviews.count > 0)
@ -115,7 +200,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
if lastView == nil { if lastView == nil {
subview.autoPinEdge(toSuperviewEdge: .top) subview.autoPinEdge(toSuperviewEdge: .top)
} else { } else {
subview.autoPinEdge(.top, to: .bottom, of: lastView!, withOffset: stackSpacing()) subview.autoPinEdge(.top, to: .bottom, of: lastView!, withOffset: 10)
} }
lastView = subview lastView = subview
@ -140,7 +225,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
if lastView == nil { if lastView == nil {
subview.autoPinEdge(toSuperviewEdge: .left) subview.autoPinEdge(toSuperviewEdge: .left)
} else { } else {
subview.autoPinEdge(.left, to: .right, of: lastView!, withOffset: stackSpacing()) subview.autoPinEdge(.left, to: .right, of: lastView!, withOffset: 10)
} }
lastView = subview lastView = subview
@ -151,14 +236,14 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
return stackView return stackView
} }
private func stackSpacing() -> CGFloat { // private func stackSpacing() -> CGFloat {
switch mode { // switch mode {
case .large, .attachmentApproval: // case .large, .attachmentApproval:
return CGFloat(10) // return CGFloat(10)
case .small: // case .small:
return CGFloat(5) // return CGFloat(5)
} // }
} // }
private func createAudioPreview() { private func createAudioPreview() {
guard let dataUrl = attachment.dataUrl else { guard let dataUrl = attachment.dataUrl else {
@ -167,41 +252,53 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
} }
audioPlayer = OWSAudioPlayer(mediaUrl: dataUrl, audioBehavior: .playback, delegate: self) audioPlayer = OWSAudioPlayer(mediaUrl: dataUrl, audioBehavior: .playback, delegate: self)
var subviews = [UIView]() imageView.image = UIImage(named: "FileLarge")
fileTypeImageView.image = UIImage(named: "table_ic_notification_sound")
let audioPlayButton = UIButton()
self.audioPlayButton = audioPlayButton
setAudioIconToPlay() setAudioIconToPlay()
audioPlayButton.imageView?.layer.minificationFilter = .trilinear
audioPlayButton.imageView?.layer.magnificationFilter = .trilinear
audioPlayButton.addTarget(self, action: #selector(audioPlayButtonPressed), for: .touchUpInside)
let buttonSize = createHeroViewSize()
audioPlayButton.autoSetDimension(.width, toSize: buttonSize)
audioPlayButton.autoSetDimension(.height, toSize: buttonSize)
subviews.append(audioPlayButton)
let fileNameLabel = createFileNameLabel()
if let fileNameLabel = fileNameLabel {
subviews.append(fileNameLabel)
}
let fileSizeLabel = createFileSizeLabel()
subviews.append(fileSizeLabel)
let stackView = wrapViewsInVerticalStack(subviews: subviews)
self.addSubview(stackView) self.addSubview(stackView)
fileNameLabel?.autoPinWidthToSuperview(withMargin: 32) self.addSubview(audioPlayPauseButton)
stackView.addArrangedSubview(imageView)
stackView.addArrangedSubview(UIView.vSpacer(0))
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(fileSizeLabel)
imageView.addSubview(fileTypeImageView)
// We want to center the stackView in it's superview while also ensuring NSLayoutConstraint.activate([
// it's superview is big enough to contain it. stackView.centerYAnchor.constraint(equalTo: centerYAnchor),
stackView.autoPinWidthToSuperview() stackView.widthAnchor.constraint(equalTo: widthAnchor),
stackView.autoVCenterInSuperview() stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor),
NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultLow) {
stackView.autoPinHeightToSuperview() imageView.widthAnchor.constraint(equalToConstant: 150),
} imageView.heightAnchor.constraint(equalToConstant: 150),
stackView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual) titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)),
stackView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual) fileSizeLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)),
fileTypeImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
fileTypeImageView.centerYAnchor.constraint(
equalTo: imageView.centerYAnchor,
constant: 25
),
fileTypeImageView.widthAnchor.constraint(
equalTo: fileTypeImageView.heightAnchor,
multiplier: ((fileTypeImageView.image?.size.width ?? 1) / (fileTypeImageView.image?.size.height ?? 1))
),
fileTypeImageView.widthAnchor.constraint(
equalTo: imageView.widthAnchor, constant: -75
),
audioPlayPauseButton.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
audioPlayPauseButton.centerYAnchor.constraint(equalTo: imageView.centerYAnchor),
audioPlayPauseButton.widthAnchor.constraint(
equalToConstant: (audioPlayPauseButton.layer.cornerRadius * 2)
),
audioPlayPauseButton.heightAnchor.constraint(
equalToConstant: (audioPlayPauseButton.layer.cornerRadius * 2)
)
])
} }
private func createAnimatedPreview() { private func createAnimatedPreview() {
@ -221,25 +318,38 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
createGenericPreview() createGenericPreview()
return return
} }
let animatedImageView = YYAnimatedImageView()
animatedImageView.image = image animatedImageView.image = image
let aspectRatio = image.size.width / image.size.height let aspectRatio: CGFloat = (image.size.width / image.size.height)
addSubviewWithScaleAspectFitLayout(view: animatedImageView, aspectRatio: aspectRatio) let clampedRatio: CGFloat = CGFloatClamp(aspectRatio, 0.05, 95.0)
addSubview(animatedImageView)
// addSubviewWithScaleAspectFitLayout(view: animatedImageView, aspectRatio: aspectRatio)
contentView = animatedImageView 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) { // private func addSubviewWithScaleAspectFitLayout(view: UIView, aspectRatio: CGFloat) {
self.addSubview(view) // self.addSubview(view)
// This emulates the behavior of contentMode = .scaleAspectFit using // // This emulates the behavior of contentMode = .scaleAspectFit using
// iOS auto layout constraints. // // iOS auto layout constraints.
// // //
// This allows ConversationInputToolbar to place the "cancel" button // // This allows ConversationInputToolbar to place the "cancel" button
// in the upper-right hand corner of the preview content. // // in the upper-right hand corner of the preview content.
view.autoCenterInSuperview() // view.autoCenterInSuperview()
view.autoPin(toAspectRatio: aspectRatio) // view.autoPin(toAspectRatio: aspectRatio)
view.autoMatch(.width, to: .width, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual) // view.autoMatch(.width, to: .width, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual)
view.autoMatch(.height, to: .height, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual) // view.autoMatch(.height, to: .height, of: self, withMultiplier: 1.0, relation: .lessThanOrEqual)
} // }
private func createImagePreview() { private func createImagePreview() {
guard attachment.isValidImage else { guard attachment.isValidImage else {
@ -255,12 +365,26 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
return return
} }
let imageView = UIImageView(image: image) imageView.image = image
imageView.layer.minificationFilter = .trilinear // imageView.layer.minificationFilter = .trilinear
imageView.layer.magnificationFilter = .trilinear // imageView.layer.magnificationFilter = .trilinear
let aspectRatio = image.size.width / image.size.height let aspectRatio = image.size.width / image.size.height
addSubviewWithScaleAspectFitLayout(view: imageView, aspectRatio: aspectRatio) let clampedRatio: CGFloat = CGFloatClamp(aspectRatio, 0.05, 95.0)
// addSubviewWithScaleAspectFitLayout(view: imageView, aspectRatio: aspectRatio)
contentView = imageView 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() { private func createVideoPreview() {
@ -277,30 +401,58 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
return return
} }
let imageView = UIImageView(image: image) // let imageView = UIImageView(image: image)
imageView.layer.minificationFilter = .trilinear imageView.image = image
imageView.layer.magnificationFilter = .trilinear // imageView.layer.minificationFilter = .trilinear
// imageView.layer.magnificationFilter = .trilinear
self.addSubview(imageView)
let aspectRatio = image.size.width / image.size.height let aspectRatio = image.size.width / image.size.height
addSubviewWithScaleAspectFitLayout(view: imageView, aspectRatio: aspectRatio) let clampedRatio: CGFloat = CGFloatClamp(aspectRatio, 0.05, 95.0)
// addSubviewWithScaleAspectFitLayout(view: imageView, aspectRatio: aspectRatio)
contentView = imageView contentView = imageView
// Attachment approval provides it's own play button to keep it
// at the proper zoom scale.
if mode != .attachmentApproval {
self.addSubview(videoPlayButton)
}
NSLayoutConstraint.activate([
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
imageView.widthAnchor.constraint(
equalTo: imageView.heightAnchor,
multiplier: clampedRatio
),
imageView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor),
imageView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor)
])
// attachment approval provides it's own play button to keep it // Attachment approval provides it's own play button to keep it
// at the proper zoom scale. // at the proper zoom scale.
if mode != .attachmentApproval { if mode != .attachmentApproval {
let videoPlayIcon = UIImage(named: "CirclePlay")!
let videoPlayButton = UIImageView(image: videoPlayIcon)
self.videoPlayButton = videoPlayButton
videoPlayButton.contentMode = .scaleAspectFit
self.addSubview(videoPlayButton) self.addSubview(videoPlayButton)
videoPlayButton.autoCenterInSuperview() // videoPlayButton.autoCenterInSuperview()
videoPlayButton.autoSetDimension(.width, toSize: 72) // videoPlayButton.autoSetDimension(.width, toSize: 72)
videoPlayButton.autoSetDimension(.height, toSize: 72) // videoPlayButton.autoSetDimension(.height, toSize: 72)
NSLayoutConstraint.activate([
videoPlayButton.centerXAnchor.constraint(equalTo: centerXAnchor),
videoPlayButton.centerYAnchor.constraint(equalTo: centerYAnchor),
imageView.widthAnchor.constraint(equalToConstant: 72),
imageView.heightAnchor.constraint(equalToConstant: 72)
])
} }
} }
private func createUrlPreview() { private func createUrlPreview() {
// If link previews aren't enabled then use a fallback state // If link previews aren't enabled then use a fallback state
guard let linkPreviewURL: String = OWSLinkPreview.previewURL(forRawBodyText: attachment.text()) else { guard let linkPreviewURL: String = OWSLinkPreview.previewURL(forRawBodyText: attachment.text()) else {
// "vc_share_link_previews_disabled_title" = "Link Previews Disabled";
// "vc_share_link_previews_disabled_explanation" = "Enabling link previews will show previews for URLs you sshare. This can be useful, but Session will need to contact linked websites to generate previews. You can enable link previews in Session's settings.";
// TODO: Show "warning" about disabled link previews instead
createGenericPreview() createGenericPreview()
return return
} }
@ -413,33 +565,40 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
} }
private func createGenericPreview() { private func createGenericPreview() {
var subviews = [UIView]() imageView.image = UIImage(named: "FileLarge")
stackView.backgroundColor = .green
let imageView = createHeroImageView(imageName: "FileLarge")
imageView.contentMode = .center
subviews.append(imageView)
let fileNameLabel = createFileNameLabel()
if let fileNameLabel = fileNameLabel {
subviews.append(fileNameLabel)
}
let fileSizeLabel = createFileSizeLabel()
subviews.append(fileSizeLabel)
let stackView = wrapViewsInVerticalStack(subviews: subviews)
self.addSubview(stackView) self.addSubview(stackView)
fileNameLabel?.autoPinWidthToSuperview(withMargin: 32)
stackView.addArrangedSubview(imageView)
stackView.addArrangedSubview(UIView.vSpacer(0))
stackView.addArrangedSubview(titleLabel)
stackView.addArrangedSubview(fileSizeLabel)
imageView.addSubview(fileTypeImageView)
// We want to center the stackView in it's superview while also ensuring NSLayoutConstraint.activate([
// it's superview is big enough to contain it. stackView.centerYAnchor.constraint(equalTo: centerYAnchor),
stackView.autoPinWidthToSuperview() stackView.widthAnchor.constraint(equalTo: widthAnchor),
stackView.autoVCenterInSuperview() stackView.heightAnchor.constraint(lessThanOrEqualTo: heightAnchor),
NSLayoutConstraint.autoSetPriority(UILayoutPriority.defaultLow) {
stackView.autoPinHeightToSuperview() imageView.widthAnchor.constraint(equalToConstant: 150),
} imageView.heightAnchor.constraint(equalToConstant: 150),
stackView.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual) titleLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)),
stackView.autoPinEdge(toSuperviewEdge: .bottom, withInset: 0, relation: .greaterThanOrEqual) fileSizeLabel.widthAnchor.constraint(equalTo: stackView.widthAnchor, constant: -(32 * 2)),
fileTypeImageView.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
fileTypeImageView.centerYAnchor.constraint(
equalTo: imageView.centerYAnchor,
constant: 25
),
fileTypeImageView.widthAnchor.constraint(
equalTo: fileTypeImageView.heightAnchor,
multiplier: ((fileTypeImageView.image?.size.width ?? 1) / (fileTypeImageView.image?.size.height ?? 1))
),
fileTypeImageView.widthAnchor.constraint(
equalTo: imageView.widthAnchor, constant: -75
)
])
} }
private func createHeroViewSize() -> CGFloat { private func createHeroViewSize() -> CGFloat {
@ -474,10 +633,10 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
private func labelFont() -> UIFont { private func labelFont() -> UIFont {
switch mode { switch mode {
case .large, .attachmentApproval: case .large, .attachmentApproval:
return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(18, 24)) return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(18, 24))
case .small: case .small:
return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(14, 14)) return UIFont.ows_regularFont(withSize: ScaleFromIPhone5To7Plus(14, 14))
} }
} }
@ -495,19 +654,17 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
return nil return nil
} }
return String(format: NSLocalizedString("ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT", //"Format string for file extension label in call interstitial view"
comment: "Format string for file extension label in call interstitial view"), return String(format: "ATTACHMENT_APPROVAL_FILE_EXTENSION_FORMAT".localized(), fileExtension.uppercased())
fileExtension.uppercased())
} }
public func formattedFileName() -> String? { public func formattedFileName() -> String? {
guard let sourceFilename = attachment.sourceFilename else { guard let sourceFilename = attachment.sourceFilename else { return nil }
return nil
}
let filename = sourceFilename.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) let filename = sourceFilename.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
guard filename.count > 0 else {
return nil guard filename.count > 0 else { return nil }
}
return filename return filename
} }
@ -543,8 +700,7 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
// MARK: - Event Handlers // MARK: - Event Handlers
@objc @objc func audioPlayPauseButtonPressed(sender: UIButton) {
func audioPlayButtonPressed(sender: UIButton) {
audioPlayer?.togglePlayState() audioPlayer?.togglePlayState()
} }
@ -580,16 +736,22 @@ public class MediaMessageView: UIView, OWSAudioPlayerDelegate {
} }
private func setAudioIconToPlay() { private func setAudioIconToPlay() {
let image = UIImage(named: "audio_play_black_large")?.withRenderingMode(.alwaysTemplate) //attachment_audio
assert(image != nil) // let image = UIImage(named: "audio_play_black_large")?.withRenderingMode(.alwaysTemplate)
audioPlayButton?.setImage(image, for: .normal) // assert(image != nil)
audioPlayButton?.imageView?.tintColor = controlTintColor // audioPlayButton?.setImage(image, for: .normal)
// audioPlayButton?.imageView?.tintColor = controlTintColor
//let image = UIImage(named: "CirclePlay")
let image = UIImage(named: "Play")
audioPlayPauseButton.setImage(image, for: .normal)
} }
private func setAudioIconToPause() { private func setAudioIconToPause() {
let image = UIImage(named: "audio_pause_black_large")?.withRenderingMode(.alwaysTemplate) // let image = UIImage(named: "audio_pause_black_large")?.withRenderingMode(.alwaysTemplate)
assert(image != nil) // assert(image != nil)
audioPlayButton?.setImage(image, for: .normal) // audioPlayButton?.setImage(image, for: .normal)
audioPlayButton?.imageView?.tintColor = controlTintColor // audioPlayButton?.imageView?.tintColor = controlTintColor
let image = UIImage(named: "Pause")
audioPlayPauseButton.setImage(image, for: .normal)
} }
} }

@ -0,0 +1,43 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit.UIColor
public extension UIColor {
struct HSBA {
public var hue: CGFloat = 0
public var saturation: CGFloat = 0
public var brightness: CGFloat = 0
public var alpha: CGFloat = 0
public init?(color: UIColor) {
// Note: Looks like as of iOS 10 devices use the kCGColorSpaceExtendedGray color
// space for grayscale colors which seems to be compatible with the RGB color space
// meaning we don'e need to check 'getWhite:alpha:' if the below method fails, for
// more info see: https://developer.apple.com/documentation/uikit/uicolor#overview
guard color.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) else {
return nil
}
}
}
var hsba: HSBA? { return HSBA(color: self) }
// MARK: - Functions
func toImage() -> UIImage {
let bounds: CGRect = CGRect(x: 0, y: 0, width: 1, height: 1)
let renderer: UIGraphicsImageRenderer = UIGraphicsImageRenderer(bounds: bounds)
return renderer.image { rendererContext in
rendererContext.cgContext.setFillColor(self.cgColor)
rendererContext.cgContext.fill(bounds)
}
}
func darken(by percentage: CGFloat) -> UIColor {
guard percentage != 0 else { return self }
guard let hsba: HSBA = self.hsba else { return self }
return UIColor(hue: hsba.hue, saturation: hsba.saturation, brightness: (hsba.brightness - percentage), alpha: hsba.alpha)
}
}
Loading…
Cancel
Save