358 lines
13 KiB
Swift
358 lines
13 KiB
Swift
//
|
|
// Copyright 2019 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import SignalCoreKit
|
|
import SignalServiceKit
|
|
import SignalUI
|
|
import UIKit
|
|
|
|
protocol AttachmentFormatPickerDelegate: AnyObject {
|
|
func didTapPhotos()
|
|
func didTapGif()
|
|
func didTapFile()
|
|
func didTapContact()
|
|
func didTapLocation()
|
|
func didTapPayment()
|
|
}
|
|
|
|
class AttachmentFormatPickerView: UIView {
|
|
|
|
weak var attachmentFormatPickerDelegate: AttachmentFormatPickerDelegate?
|
|
|
|
private let scrollView: UIScrollView = {
|
|
let scrollView = UIScrollView()
|
|
scrollView.showsHorizontalScrollIndicator = false
|
|
scrollView.contentInsetAdjustmentBehavior = .never
|
|
return scrollView
|
|
}()
|
|
|
|
private lazy var contentView: UIStackView = {
|
|
let buttons = AttachmentType.cases(isGroup: isGroup).map {
|
|
let button = AttachmentTypeButton(attachmentType: $0)
|
|
button.isVerticallyCompactAppearance = traitCollection.verticalSizeClass == .compact
|
|
button.addTarget(self, action: #selector(didTapAttachmentButton), for: .touchUpInside)
|
|
return button
|
|
}
|
|
let stackView = UIStackView(arrangedSubviews: buttons)
|
|
stackView.spacing = 12
|
|
stackView.axis = .horizontal
|
|
stackView.alignment = .top
|
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
return stackView
|
|
}()
|
|
|
|
private let isGroup: Bool
|
|
|
|
@objc
|
|
private func didTapAttachmentButton(sender: Any) {
|
|
guard
|
|
let delegate = attachmentFormatPickerDelegate,
|
|
let attachmentTypeButton = sender as? AttachmentTypeButton
|
|
else {
|
|
return
|
|
}
|
|
// Delay event handling a bit so that pressed state of the button is visible.
|
|
DispatchQueue.main.async {
|
|
switch attachmentTypeButton.attachmentType {
|
|
case .photo:
|
|
delegate.didTapPhotos()
|
|
case .gif:
|
|
delegate.didTapGif()
|
|
case .file:
|
|
delegate.didTapFile()
|
|
case .payment:
|
|
delegate.didTapPayment()
|
|
case .contact:
|
|
delegate.didTapContact()
|
|
case .location:
|
|
delegate.didTapLocation()
|
|
}
|
|
}
|
|
}
|
|
|
|
init(isGroup: Bool) {
|
|
self.isGroup = isGroup
|
|
|
|
super.init(frame: .zero)
|
|
|
|
backgroundColor = .clear
|
|
|
|
addSubview(scrollView)
|
|
scrollView.autoPinEdgesToSuperviewEdges()
|
|
|
|
scrollView.addSubview(contentView)
|
|
NSLayoutConstraint.activate([
|
|
contentView.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
|
|
contentView.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
|
|
contentView.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor),
|
|
contentView.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor)
|
|
])
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
super.traitCollectionDidChange(previousTraitCollection)
|
|
let isLandscapeLayout = traitCollection.verticalSizeClass == .compact
|
|
contentView.arrangedSubviews.forEach { subview in
|
|
guard let button = subview as? AttachmentTypeButton else { return }
|
|
button.isVerticallyCompactAppearance = isLandscapeLayout
|
|
}
|
|
invalidateIntrinsicContentSize()
|
|
}
|
|
|
|
override var intrinsicContentSize: CGSize {
|
|
let height: CGFloat = traitCollection.verticalSizeClass == .compact ? 86 : 122
|
|
return CGSize(width: UIView.noIntrinsicMetric, height: height)
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
|
|
// Dispatch async is needed for the subviews to have their final frames.
|
|
DispatchQueue.main.async {
|
|
self.updateScrollViewContentInsets()
|
|
}
|
|
}
|
|
|
|
private func updateScrollViewContentInsets() {
|
|
// Center button row horizontally and vertically in the scroll view.
|
|
let scrollViewSize = scrollView.frame.size
|
|
let contentSize = contentView.bounds.size
|
|
guard scrollViewSize.isNonEmpty, contentSize.isNonEmpty else { return }
|
|
let horizontalInset = max(OWSTableViewController2.defaultHOuterMargin, 0.5 * (scrollViewSize.width - contentSize.width))
|
|
let verticalInset = max(0, 0.5 * (scrollViewSize.height - contentSize.height))
|
|
let contentInset = UIEdgeInsets(hMargin: horizontalInset, vMargin: verticalInset)
|
|
guard scrollView.contentInset != contentInset else { return }
|
|
scrollView.contentInset = contentInset
|
|
scrollView.contentOffset = CGPoint(x: -contentInset.leading, y: -contentInset.top)
|
|
}
|
|
|
|
// Set initial state for all buttons - get them ready to be animated in.
|
|
func prepareForPresentation() {
|
|
let buttons = contentView.arrangedSubviews
|
|
guard !buttons.isEmpty else { return }
|
|
|
|
UIView.performWithoutAnimation {
|
|
buttons.forEach { button in
|
|
button.alpha = 0
|
|
button.transform = .scale(0.5)
|
|
}
|
|
}
|
|
}
|
|
|
|
func performPresentationAnimation() {
|
|
let buttons = contentView.arrangedSubviews
|
|
guard !buttons.isEmpty else { return }
|
|
|
|
// Chain animations for buttons.
|
|
let delay = 1 / CGFloat(buttons.count)
|
|
let animator = UIViewPropertyAnimator(duration: 0.5, springDamping: 1, springResponse: 0.2)
|
|
for (index, button) in buttons.enumerated() {
|
|
animator.addAnimations({
|
|
button.alpha = 1
|
|
button.transform = .identity
|
|
}, delayFactor: CGFloat(index) * delay)
|
|
}
|
|
animator.startAnimation()
|
|
}
|
|
|
|
private enum AttachmentType: String, CaseIterable, Dependencies {
|
|
case photo
|
|
case gif
|
|
case file
|
|
case contact
|
|
case location
|
|
case payment
|
|
|
|
private static var contactCases: [AttachmentType] {
|
|
if payments.shouldShowPaymentsUI {
|
|
return cases(except: [])
|
|
} else {
|
|
return cases(except: [.payment])
|
|
}
|
|
}
|
|
|
|
private static var groupCases: [AttachmentType] {
|
|
cases(except: [.payment])
|
|
}
|
|
|
|
private static func cases(except: [AttachmentType]) -> [AttachmentType] {
|
|
let showGifSearch = RemoteConfig.enableGifSearch
|
|
return allCases.filter { (value: AttachmentType) in
|
|
if value == .gif && showGifSearch.negated { return false }
|
|
return except.contains(value).negated
|
|
}
|
|
}
|
|
|
|
static func cases(isGroup: Bool) -> [AttachmentType] {
|
|
return isGroup ? groupCases : contactCases
|
|
}
|
|
}
|
|
|
|
private class AttachmentTypeButton: UIControl {
|
|
|
|
private class DimmablePillView: PillView {
|
|
|
|
private let dimmerView: UIView = {
|
|
let view = UIView()
|
|
view.backgroundColor = Theme.isDarkThemeEnabled ? .ows_whiteAlpha10 : .ows_blackAlpha10
|
|
view.alpha = 0
|
|
return view
|
|
}()
|
|
|
|
// Implicitly animatable.
|
|
var isDimmed: Bool = false {
|
|
didSet {
|
|
dimmerView.alpha = isDimmed ? 1 : 0
|
|
}
|
|
}
|
|
|
|
override init(frame: CGRect) {
|
|
super.init(frame: frame)
|
|
addSubview(dimmerView)
|
|
}
|
|
|
|
required init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func layoutSubviews() {
|
|
super.layoutSubviews()
|
|
dimmerView.frame = bounds
|
|
}
|
|
}
|
|
|
|
let attachmentType: AttachmentType
|
|
|
|
var isVerticallyCompactAppearance = false {
|
|
didSet {
|
|
imageViewPillBoxHeightConstraint.constant = isVerticallyCompactAppearance ? 40 : 50
|
|
}
|
|
}
|
|
|
|
private let imageViewPillBox: DimmablePillView = {
|
|
let pillView = DimmablePillView()
|
|
pillView.isUserInteractionEnabled = false
|
|
pillView.backgroundColor = Theme.isDarkThemeEnabled ? UIColor(white: 1, alpha: 0.16) : UIColor(white: 0, alpha: 0.08)
|
|
return pillView
|
|
}()
|
|
|
|
private lazy var imageViewPillBoxHeightConstraint: NSLayoutConstraint = {
|
|
imageViewPillBox.heightAnchor.constraint(equalToConstant: 50)
|
|
}()
|
|
|
|
private let iconImageView: UIImageView = {
|
|
let imageView = UIImageView()
|
|
imageView.contentMode = .center
|
|
imageView.tintColor = Theme.isDarkThemeEnabled ? .white : .black
|
|
return imageView
|
|
}()
|
|
|
|
private let textLabel: UILabel = {
|
|
let label = UILabel()
|
|
label.font = .dynamicTypeFootnoteClamped.medium()
|
|
label.textColor = Theme.secondaryTextAndIconColor
|
|
label.textAlignment = .center
|
|
label.numberOfLines = 2
|
|
label.adjustsFontSizeToFitWidth = true
|
|
label.lineBreakMode = .byCharWrapping
|
|
return label
|
|
}()
|
|
|
|
init(attachmentType: AttachmentType) {
|
|
self.attachmentType = attachmentType
|
|
|
|
super.init(frame: .zero)
|
|
|
|
translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
imageViewPillBox.addSubview(iconImageView)
|
|
iconImageView.autoPinEdgesToSuperviewEdges()
|
|
|
|
addSubview(imageViewPillBox)
|
|
NSLayoutConstraint.activate([
|
|
imageViewPillBox.widthAnchor.constraint(equalToConstant: 76),
|
|
imageViewPillBoxHeightConstraint
|
|
])
|
|
imageViewPillBox.autoPinEdges(toSuperviewEdgesExcludingEdge: .bottom)
|
|
|
|
addSubview(textLabel)
|
|
textLabel.autoPinEdge(.top, to: .bottom, of: imageViewPillBox, withOffset: 8)
|
|
textLabel.autoPinEdges(toSuperviewEdgesExcludingEdge: .top)
|
|
|
|
configure()
|
|
}
|
|
|
|
override var isHighlighted: Bool {
|
|
didSet {
|
|
setIsPressed(isHighlighted, animated: window != nil)
|
|
}
|
|
}
|
|
|
|
private var _isPressed = false
|
|
|
|
private var isPressed: Bool {
|
|
get { _isPressed }
|
|
set { setIsPressed(newValue, animated: false) }
|
|
}
|
|
|
|
private func setIsPressed(_ isPressed: Bool, animated: Bool) {
|
|
_isPressed = isPressed
|
|
|
|
let changes = {
|
|
self.imageViewPillBox.isDimmed = isPressed
|
|
self.imageViewPillBox.transform = isPressed ? .scale(0.9) : .identity
|
|
}
|
|
guard animated else {
|
|
changes()
|
|
return
|
|
}
|
|
|
|
let animator = UIViewPropertyAnimator(duration: 0.15, springDamping: 0.64, springResponse: 0.25)
|
|
animator.addAnimations(changes)
|
|
animator.startAnimation()
|
|
}
|
|
|
|
@available(*, unavailable, message: "Unimplemented")
|
|
required public init?(coder aDecoder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
private func configure() {
|
|
let imageName: String
|
|
let text: String
|
|
|
|
switch attachmentType {
|
|
case .photo:
|
|
text = OWSLocalizedString("ATTACHMENT_KEYBOARD_PHOTOS", comment: "A button to open the photo picker from the Attachment Keyboard")
|
|
imageName = "album-tilt-28"
|
|
case .contact:
|
|
text = OWSLocalizedString("ATTACHMENT_KEYBOARD_CONTACT", comment: "A button to select a contact from the Attachment Keyboard")
|
|
imageName = "person-circle-28"
|
|
case .file:
|
|
text = OWSLocalizedString("ATTACHMENT_KEYBOARD_FILE", comment: "A button to select a file from the Attachment Keyboard")
|
|
imageName = "file-28"
|
|
case .gif:
|
|
text = OWSLocalizedString("ATTACHMENT_KEYBOARD_GIF", comment: "A button to select a GIF from the Attachment Keyboard")
|
|
imageName = "gif-28"
|
|
case .location:
|
|
text = OWSLocalizedString("ATTACHMENT_KEYBOARD_LOCATION", comment: "A button to select a location from the Attachment Keyboard")
|
|
imageName = "location-28"
|
|
case .payment:
|
|
text = OWSLocalizedString("ATTACHMENT_KEYBOARD_PAYMENT", comment: "A button to select a payment from the Attachment Keyboard")
|
|
imageName = "payment-28"
|
|
}
|
|
|
|
textLabel.text = text
|
|
iconImageView.image = UIImage(imageLiteralResourceName: imageName)
|
|
accessibilityIdentifier = UIView.accessibilityIdentifier(in: self, name: "format-\(attachmentType.rawValue)")
|
|
}
|
|
}
|
|
|
|
}
|