Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/src/ViewControllers/Attachment Keyboard/AttachmentKeyboard.swift
1 views
//
// Copyright 2019 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Photos
import SignalServiceKit
import SignalUI

protocol AttachmentKeyboardDelegate: AnyObject {
    func didSelectRecentPhoto(asset: PHAsset, attachment: PreviewableAttachment, attachmentLimits: OutgoingAttachmentLimits)
    func didTapPhotos()
    func didTapGif()
    func didTapFile()
    func didTapContact()
    func didTapLocation()
    func didTapPayment()
    func didTapPoll()
    var isGroup: Bool { get }
}

class AttachmentKeyboard: CustomKeyboard {

    weak var delegate: AttachmentKeyboardDelegate?

    private lazy var recentPhotosCollectionView: RecentPhotosCollectionView = {
        let collectionView = RecentPhotosCollectionView()
        collectionView.recentPhotosDelegate = self
        return collectionView
    }()

    private lazy var attachmentFormatPickerView: AttachmentFormatPickerView = {
        let pickerView = AttachmentFormatPickerView(isGroup: delegate?.isGroup ?? false)
        pickerView.attachmentFormatPickerDelegate = self
        pickerView.setContentHuggingVerticalHigh()
        pickerView.setCompressionResistanceVerticalHigh()
        return pickerView
    }()

    private lazy var limitedPhotoPermissionsView = LimitedPhotoPermissionsView()

    private var topInset: CGFloat {
        guard #available(iOS 26, *) else { return 12 }
        return traitCollection.verticalSizeClass == .compact ? 20 : 36
    }

    // MARK: -

    init(delegate: AttachmentKeyboardDelegate?) {
        self.delegate = delegate

        super.init()

        backgroundColor = if #available(iOS 26, *) { .clear } else { .Signal.background }

        let stackView = UIStackView(arrangedSubviews: [
            limitedPhotoPermissionsView,
            recentPhotosCollectionView,
            attachmentFormatPickerView,
        ])
        stackView.axis = .vertical
        stackView.setCustomSpacing(12, after: limitedPhotoPermissionsView)
        limitedPhotoPermissionsView.isHiddenInStackView = true
        contentView.addSubview(stackView)
        stackView.translatesAutoresizingMaskIntoConstraints = false
        let topEdgeConstraint = stackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: topInset)
        NSLayoutConstraint.activate([
            topEdgeConstraint,
            stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
            stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
            stackView.bottomAnchor.constraint(equalTo: contentView.safeAreaLayoutGuide.bottomAnchor),
        ])

        // Variable top inset on iOS 26.
        if #available(iOS 26, *) {
            registerForTraitChanges([UITraitVerticalSizeClass.self]) { (self: Self, _) in
                topEdgeConstraint.constant = self.topInset
            }
        }
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK: -

    override func willPresent() {
        super.willPresent()
        checkPermissions()
        recentPhotosCollectionView.prepareForPresentation()
        attachmentFormatPickerView.prepareForPresentation()
    }

    override func wasPresented() {
        super.wasPresented()
        recentPhotosCollectionView.performPresentationAnimation()
        attachmentFormatPickerView.performPresentationAnimation()
    }

    private func checkPermissions() {
        let setAuthorizationStatus: (PHAuthorizationStatus) -> Void = { status in
            self.recentPhotosCollectionView.mediaLibraryAuthorizationStatus = status
            let isLimited = switch status {
            case .limited:
                true
            case .notDetermined, .restricted, .denied, .authorized:
                false
            @unknown default:
                false
            }
            self.attachmentFormatPickerView.shouldLeaveSpaceForPermissions = isLimited
            self.limitedPhotoPermissionsView.isHiddenInStackView = !isLimited
        }
        let authorizationStatus: PHAuthorizationStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite)
        guard authorizationStatus != .notDetermined else {
            PHPhotoLibrary.requestAuthorization(for: .readWrite, handler: setAuthorizationStatus)
            return
        }
        setAuthorizationStatus(authorizationStatus)
    }
}

extension AttachmentKeyboard: RecentPhotosDelegate {

    func didSelectRecentPhoto(asset: PHAsset, attachment: PreviewableAttachment, attachmentLimits: OutgoingAttachmentLimits) {
        delegate?.didSelectRecentPhoto(asset: asset, attachment: attachment, attachmentLimits: attachmentLimits)
    }
}

extension AttachmentKeyboard: AttachmentFormatPickerDelegate {
    func didTapPhotos() {
        delegate?.didTapPhotos()
    }

    func didTapGif() {
        delegate?.didTapGif()
    }

    func didTapFile() {
        delegate?.didTapFile()
    }

    func didTapContact() {
        delegate?.didTapContact()
    }

    func didTapLocation() {
        delegate?.didTapLocation()
    }

    func didTapPayment() {
        delegate?.didTapPayment()
    }

    func didTapPoll() {
        delegate?.didTapPoll()
    }
}