Make attachment limits hot-swappable
This commit is contained in:
parent
27b9514a70
commit
21d4d8f038
@ -94,7 +94,7 @@ enum PasteboardAttachment {
|
||||
/// Returns an attachment from the pasteboard, or nil if no attachment
|
||||
/// can be found.
|
||||
@MainActor
|
||||
static func loadPreviewableAttachments() async throws -> [PreviewableAttachment]? {
|
||||
static func loadPreviewableAttachments(attachmentLimits: OutgoingAttachmentLimits) async throws -> [PreviewableAttachment]? {
|
||||
guard
|
||||
UIPasteboard.general.numberOfItems >= 1,
|
||||
let pasteboardUTITypes = UIPasteboard.general.types(forItemSet: nil)
|
||||
@ -107,6 +107,7 @@ enum PasteboardAttachment {
|
||||
let attachment = try await loadPreviewableAttachment(
|
||||
atIndex: IndexSet(integer: index),
|
||||
pasteboardUTIs: utiSet,
|
||||
attachmentLimits: attachmentLimits,
|
||||
retrySinglePixelImages: true,
|
||||
)
|
||||
|
||||
@ -138,7 +139,12 @@ enum PasteboardAttachment {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private static func loadPreviewableAttachment(atIndex index: IndexSet, pasteboardUTIs: [String], retrySinglePixelImages: Bool) async throws -> PreviewableAttachment? {
|
||||
private static func loadPreviewableAttachment(
|
||||
atIndex index: IndexSet,
|
||||
pasteboardUTIs: [String],
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
retrySinglePixelImages: Bool,
|
||||
) async throws -> PreviewableAttachment? {
|
||||
var pasteboardUTISet = Set<String>(filterDynamicUTITypes(pasteboardUTIs))
|
||||
guard pasteboardUTISet.count > 0 else {
|
||||
return nil
|
||||
@ -163,7 +169,12 @@ enum PasteboardAttachment {
|
||||
// pasteboard after a brief delay (once, then give up).
|
||||
if retrySinglePixelImages, (try? dataSource.imageSource())?.imageMetadata()?.pixelSize == CGSize(square: 1) {
|
||||
try? await Task.sleep(nanoseconds: NSEC_PER_MSEC * 50)
|
||||
return try await loadPreviewableAttachment(atIndex: index, pasteboardUTIs: pasteboardUTIs, retrySinglePixelImages: false)
|
||||
return try await loadPreviewableAttachment(
|
||||
atIndex: index,
|
||||
pasteboardUTIs: pasteboardUTIs,
|
||||
attachmentLimits: attachmentLimits,
|
||||
retrySinglePixelImages: false,
|
||||
)
|
||||
}
|
||||
|
||||
return try PreviewableAttachment.imageAttachment(dataSource: dataSource, dataUTI: dataUTI, canBeBorderless: true)
|
||||
@ -176,7 +187,7 @@ enum PasteboardAttachment {
|
||||
}
|
||||
|
||||
// [15M] TODO: Don't ignore errors for pasteboard videos.
|
||||
return try? await PreviewableAttachment.compressVideoAsMp4(dataSource: dataSource)
|
||||
return try? await PreviewableAttachment.compressVideoAsMp4(dataSource: dataSource, attachmentLimits: attachmentLimits)
|
||||
}
|
||||
}
|
||||
for dataUTI in SignalAttachment.audioUTISet {
|
||||
@ -184,7 +195,7 @@ enum PasteboardAttachment {
|
||||
guard let dataSource = buildDataSource(atIndex: index, dataUTI: dataUTI) else {
|
||||
return nil
|
||||
}
|
||||
return try PreviewableAttachment.audioAttachment(dataSource: dataSource, dataUTI: dataUTI)
|
||||
return try PreviewableAttachment.audioAttachment(dataSource: dataSource, dataUTI: dataUTI, attachmentLimits: attachmentLimits)
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,7 +203,7 @@ enum PasteboardAttachment {
|
||||
guard let dataSource = buildDataSource(atIndex: index, dataUTI: dataUTI) else {
|
||||
return nil
|
||||
}
|
||||
return try PreviewableAttachment.genericAttachment(dataSource: dataSource, dataUTI: dataUTI)
|
||||
return try PreviewableAttachment.genericAttachment(dataSource: dataSource, dataUTI: dataUTI, attachmentLimits: attachmentLimits)
|
||||
}
|
||||
|
||||
static func loadPreviewableStickerAttachment() throws -> PreviewableAttachment? {
|
||||
|
||||
@ -8,7 +8,10 @@ import SignalServiceKit
|
||||
import SignalUI
|
||||
|
||||
enum SignalAttachmentCloner {
|
||||
static func cloneAsSignalAttachment(attachment: ReferencedAttachmentStream) throws -> PreviewableAttachment {
|
||||
static func cloneAsSignalAttachment(
|
||||
attachment: ReferencedAttachmentStream,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
) throws -> PreviewableAttachment {
|
||||
guard let dataUTI = MimeTypeUtil.utiTypeForMimeType(attachment.attachmentStream.mimeType) else {
|
||||
throw OWSAssertionError("Missing dataUTI.")
|
||||
}
|
||||
@ -24,14 +27,14 @@ enum SignalAttachmentCloner {
|
||||
let result: PreviewableAttachment
|
||||
switch attachment.reference.renderingFlag {
|
||||
case .default:
|
||||
result = try PreviewableAttachment.buildAttachment(dataSource: decryptedDataSource, dataUTI: dataUTI)
|
||||
result = try PreviewableAttachment.buildAttachment(dataSource: decryptedDataSource, dataUTI: dataUTI, attachmentLimits: attachmentLimits)
|
||||
case .voiceMessage:
|
||||
result = try PreviewableAttachment.voiceMessageAttachment(dataSource: decryptedDataSource, dataUTI: dataUTI)
|
||||
result = try PreviewableAttachment.voiceMessageAttachment(dataSource: decryptedDataSource, dataUTI: dataUTI, attachmentLimits: attachmentLimits)
|
||||
case .borderless:
|
||||
result = try PreviewableAttachment.imageAttachment(dataSource: decryptedDataSource, dataUTI: dataUTI)
|
||||
result.rawValue.isBorderless = true
|
||||
case .shouldLoop:
|
||||
result = try PreviewableAttachment.buildAttachment(dataSource: decryptedDataSource, dataUTI: dataUTI)
|
||||
result = try PreviewableAttachment.buildAttachment(dataSource: decryptedDataSource, dataUTI: dataUTI, attachmentLimits: attachmentLimits)
|
||||
result.rawValue.isLoopingVideo = true
|
||||
}
|
||||
return result
|
||||
|
||||
@ -61,7 +61,7 @@ protocol ConversationInputToolbarDelegate: AnyObject {
|
||||
|
||||
func pollButtonPressed()
|
||||
|
||||
func didSelectRecentPhoto(asset: PHAsset, attachment: PreviewableAttachment)
|
||||
func didSelectRecentPhoto(asset: PHAsset, attachment: PreviewableAttachment, attachmentLimits: OutgoingAttachmentLimits)
|
||||
|
||||
func showUnblockConversationUI(completion: ((Bool) -> Void)?)
|
||||
}
|
||||
@ -3256,8 +3256,8 @@ extension ConversationInputToolbar: StickerKeyboardDelegate {
|
||||
|
||||
extension ConversationInputToolbar: AttachmentKeyboardDelegate {
|
||||
|
||||
func didSelectRecentPhoto(asset: PHAsset, attachment: PreviewableAttachment) {
|
||||
inputToolbarDelegate?.didSelectRecentPhoto(asset: asset, attachment: attachment)
|
||||
func didSelectRecentPhoto(asset: PHAsset, attachment: PreviewableAttachment, attachmentLimits: OutgoingAttachmentLimits) {
|
||||
inputToolbarDelegate?.didSelectRecentPhoto(asset: asset, attachment: attachment, attachmentLimits: attachmentLimits)
|
||||
}
|
||||
|
||||
func didTapPhotos() {
|
||||
|
||||
@ -40,7 +40,11 @@ extension ConversationViewController: BodyRangesTextViewDelegate {
|
||||
|
||||
public func textViewDidInsertMemoji(_ memojiGlyph: OWSAdaptiveImageGlyph) {
|
||||
do {
|
||||
self.didPasteAttachments([try PasteboardAttachment.loadPreviewableMemojiAttachment(fromMemojiGlyph: memojiGlyph)])
|
||||
let attachmentLimits = OutgoingAttachmentLimits.currentLimits()
|
||||
self.didPasteAttachments(
|
||||
[try PasteboardAttachment.loadPreviewableMemojiAttachment(fromMemojiGlyph: memojiGlyph)],
|
||||
attachmentLimits: attachmentLimits,
|
||||
)
|
||||
} catch {
|
||||
self.showErrorAlert(attachmentError: error as? SignalAttachmentError)
|
||||
}
|
||||
|
||||
@ -367,15 +367,17 @@ extension ConversationViewController: ConversationInputToolbarDelegate {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func tryToSendAttachments(
|
||||
fileprivate func tryToSendAttachments(
|
||||
_ approvedAttachments: ApprovedAttachments,
|
||||
from viewController: UIViewController,
|
||||
messageBody: MessageBody?,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
) async throws -> Bool {
|
||||
return try await tryToSendAttachments(
|
||||
approvedAttachments,
|
||||
messageBody: messageBody,
|
||||
from: viewController,
|
||||
attachmentLimits: attachmentLimits,
|
||||
untrustedThreshold: Date().addingTimeInterval(-OWSIdentityManagerImpl.Constants.defaultUntrustedInterval),
|
||||
)
|
||||
}
|
||||
@ -392,6 +394,7 @@ extension ConversationViewController: ConversationInputToolbarDelegate {
|
||||
_ approvedAttachments: ApprovedAttachments,
|
||||
messageBody: MessageBody?,
|
||||
from viewController: UIViewController,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
untrustedThreshold: Date,
|
||||
) async throws -> Bool {
|
||||
AssertIsOnMainThread()
|
||||
@ -401,7 +404,10 @@ extension ConversationViewController: ConversationInputToolbarDelegate {
|
||||
}
|
||||
|
||||
let imageQuality = approvedAttachments.imageQuality
|
||||
let imageQualityLevel = ImageQualityLevel.resolvedValue(imageQuality: imageQuality)
|
||||
let imageQualityLevel = ImageQualityLevel.resolvedValue(
|
||||
imageQuality: imageQuality,
|
||||
standardQualityLevel: attachmentLimits.standardQualityLevel,
|
||||
)
|
||||
let sendableAttachments = try await approvedAttachments.attachments.mapAsync {
|
||||
return try await SendableAttachment.forPreviewableAttachment($0, imageQualityLevel: imageQualityLevel)
|
||||
}
|
||||
@ -539,7 +545,7 @@ extension ConversationViewController: ConversationInputToolbarDelegate {
|
||||
present(OWSNavigationController(rootViewController: newPollViewController), animated: true)
|
||||
}
|
||||
|
||||
public func didSelectRecentPhoto(asset: PHAsset, attachment: PreviewableAttachment) {
|
||||
public func didSelectRecentPhoto(asset: PHAsset, attachment: PreviewableAttachment, attachmentLimits: OutgoingAttachmentLimits) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
dismissKeyBoard()
|
||||
@ -548,6 +554,7 @@ extension ConversationViewController: ConversationInputToolbarDelegate {
|
||||
asset: asset,
|
||||
attachment: attachment,
|
||||
hasQuotedReplyDraft: inputToolbar?.quotedReplyDraft != nil,
|
||||
attachmentLimits: attachmentLimits,
|
||||
delegate: self,
|
||||
dataSource: self,
|
||||
)
|
||||
@ -572,7 +579,7 @@ public extension ConversationViewController {
|
||||
)
|
||||
}
|
||||
|
||||
func showApprovalDialog(forAttachments attachments: [PreviewableAttachment]) {
|
||||
func showApprovalDialog(forAttachments attachments: [PreviewableAttachment], attachmentLimits: OutgoingAttachmentLimits) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard hasViewWillAppearEverBegun else {
|
||||
@ -587,6 +594,7 @@ public extension ConversationViewController {
|
||||
attachments: attachments,
|
||||
initialMessageBody: inputToolbar.messageBodyForSending,
|
||||
hasQuotedReplyDraft: inputToolbar.quotedReplyDraft != nil,
|
||||
attachmentLimits: attachmentLimits,
|
||||
approvalDelegate: self,
|
||||
approvalDataSource: self,
|
||||
stickerSheetDelegate: self,
|
||||
@ -647,6 +655,7 @@ private extension ConversationViewController {
|
||||
func takePictureOrVideo() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let attachmentLimits = OutgoingAttachmentLimits.currentLimits()
|
||||
ows_askForCameraPermissions { [weak self] cameraGranted in
|
||||
guard let self else { return }
|
||||
guard cameraGranted else {
|
||||
@ -663,6 +672,7 @@ private extension ConversationViewController {
|
||||
|
||||
let pickerModal = SendMediaNavigationController.showingCameraFirst(
|
||||
hasQuotedReplyDraft: self.inputToolbar?.quotedReplyDraft != nil,
|
||||
attachmentLimits: attachmentLimits,
|
||||
)
|
||||
pickerModal.sendMediaNavDelegate = self
|
||||
pickerModal.sendMediaNavDataSource = self
|
||||
@ -689,6 +699,7 @@ private extension ConversationViewController {
|
||||
|
||||
let pickerModal = SendMediaNavigationController.showingNativePicker(
|
||||
hasQuotedReplyDraft: inputToolbar?.quotedReplyDraft != nil,
|
||||
attachmentLimits: .currentLimits(),
|
||||
)
|
||||
pickerModal.sendMediaNavDelegate = self
|
||||
pickerModal.sendMediaNavDataSource = self
|
||||
@ -819,17 +830,19 @@ extension ConversationViewController: UIDocumentPickerDelegate {
|
||||
|
||||
let contentTypeIdentifier = (resourceValues?.contentType ?? .data).identifier
|
||||
|
||||
let attachmentLimits = OutgoingAttachmentLimits.currentLimits()
|
||||
|
||||
// Although we want to be able to send higher quality attachments through
|
||||
// the document picker, it's more important that we ensure the sent format
|
||||
// is one all clients can accept (e.g., *not* QuickTime .mov).
|
||||
if SignalAttachment.videoUTISet.contains(contentTypeIdentifier) {
|
||||
self.showApprovalDialogAfterProcessingVideo(dataSource: dataSource)
|
||||
self.showApprovalDialogAfterProcessingVideo(dataSource: dataSource, attachmentLimits: attachmentLimits)
|
||||
return
|
||||
}
|
||||
|
||||
let attachment: PreviewableAttachment
|
||||
do {
|
||||
attachment = try PreviewableAttachment.buildAttachment(dataSource: dataSource, dataUTI: contentTypeIdentifier)
|
||||
attachment = try PreviewableAttachment.buildAttachment(dataSource: dataSource, dataUTI: contentTypeIdentifier, attachmentLimits: attachmentLimits)
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
self.showErrorAlert(attachmentError: error as? SignalAttachmentError)
|
||||
@ -837,10 +850,10 @@ extension ConversationViewController: UIDocumentPickerDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
showApprovalDialog(forAttachments: [attachment])
|
||||
showApprovalDialog(forAttachments: [attachment], attachmentLimits: attachmentLimits)
|
||||
}
|
||||
|
||||
private func showApprovalDialogAfterProcessingVideo(dataSource: DataSourcePath) {
|
||||
private func showApprovalDialogAfterProcessingVideo(dataSource: DataSourcePath, attachmentLimits: OutgoingAttachmentLimits) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
ModalActivityIndicatorViewController.present(
|
||||
@ -848,9 +861,9 @@ extension ConversationViewController: UIDocumentPickerDelegate {
|
||||
canCancel: true,
|
||||
asyncBlock: { modalActivityIndicator in
|
||||
do {
|
||||
let attachment = try await PreviewableAttachment.compressVideoAsMp4(dataSource: dataSource)
|
||||
let attachment = try await PreviewableAttachment.compressVideoAsMp4(dataSource: dataSource, attachmentLimits: attachmentLimits)
|
||||
modalActivityIndicator.dismissIfNotCanceled(completionIfNotCanceled: {
|
||||
self.showApprovalDialog(forAttachments: [attachment])
|
||||
self.showApprovalDialog(forAttachments: [attachment], attachmentLimits: attachmentLimits)
|
||||
})
|
||||
} catch {
|
||||
owsFailDebug("Error: \(error).")
|
||||
@ -881,6 +894,7 @@ extension ConversationViewController: SendMediaNavDelegate {
|
||||
approvedAttachments,
|
||||
messageBody: messageBody,
|
||||
from: sendMediaNavigationController,
|
||||
attachmentLimits: sendMediaNavigationController.attachmentLimits,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -891,6 +905,7 @@ extension ConversationViewController: SendMediaNavDelegate {
|
||||
_ approvedAttachments: ApprovedAttachments,
|
||||
messageBody: MessageBody?,
|
||||
from viewController: UIViewController,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
) async {
|
||||
let didSend: Bool
|
||||
do {
|
||||
@ -898,6 +913,7 @@ extension ConversationViewController: SendMediaNavDelegate {
|
||||
approvedAttachments,
|
||||
from: viewController,
|
||||
messageBody: messageBody,
|
||||
attachmentLimits: attachmentLimits,
|
||||
)
|
||||
} catch {
|
||||
self.showErrorAlert(attachmentError: error as? SignalAttachmentError)
|
||||
|
||||
@ -21,6 +21,7 @@ extension ConversationViewController: AttachmentApprovalViewControllerDelegate {
|
||||
approvedAttachments,
|
||||
messageBody: messageBody,
|
||||
from: attachmentApproval,
|
||||
attachmentLimits: attachmentApproval.attachmentLimits,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -221,11 +222,16 @@ extension ConversationViewController: ConversationHeaderViewDelegate {
|
||||
|
||||
extension ConversationViewController: ConversationInputTextViewDelegate {
|
||||
public func didAttemptAttachmentPaste() {
|
||||
let attachmentLimits = OutgoingAttachmentLimits.currentLimits()
|
||||
|
||||
// If trying to paste a sticker, forego anything async since
|
||||
// the pasteboard will be cleared as soon as paste() exits.
|
||||
if PasteboardAttachment.hasStickerAttachment() {
|
||||
do {
|
||||
self.didPasteAttachments([try PasteboardAttachment.loadPreviewableStickerAttachment()].compacted())
|
||||
self.didPasteAttachments(
|
||||
[try PasteboardAttachment.loadPreviewableStickerAttachment()].compacted(),
|
||||
attachmentLimits: attachmentLimits,
|
||||
)
|
||||
} catch {
|
||||
self.showErrorAlert(attachmentError: error as? SignalAttachmentError)
|
||||
}
|
||||
@ -234,10 +240,10 @@ extension ConversationViewController: ConversationInputTextViewDelegate {
|
||||
|
||||
ModalActivityIndicatorViewController.present(fromViewController: self, asyncBlock: { modal in
|
||||
do {
|
||||
let attachments = try await PasteboardAttachment.loadPreviewableAttachments()
|
||||
let attachments = try await PasteboardAttachment.loadPreviewableAttachments(attachmentLimits: attachmentLimits)
|
||||
modal.dismiss {
|
||||
// Note: attachment array might be nil at this point; that's fine.
|
||||
self.didPasteAttachments(attachments)
|
||||
self.didPasteAttachments(attachments, attachmentLimits: attachmentLimits)
|
||||
}
|
||||
} catch {
|
||||
modal.dismiss {
|
||||
@ -247,7 +253,10 @@ extension ConversationViewController: ConversationInputTextViewDelegate {
|
||||
})
|
||||
}
|
||||
|
||||
func didPasteAttachments(_ attachments: [PreviewableAttachment]?) {
|
||||
func didPasteAttachments(
|
||||
_ attachments: [PreviewableAttachment]?,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard let attachments, attachments.count > 0 else {
|
||||
@ -263,11 +272,12 @@ extension ConversationViewController: ConversationInputTextViewDelegate {
|
||||
ApprovedAttachments(nonViewOnceAttachments: [a], imageQuality: .standard),
|
||||
messageBody: nil,
|
||||
from: self,
|
||||
attachmentLimits: attachmentLimits,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
dismissKeyBoard()
|
||||
showApprovalDialog(forAttachments: attachments)
|
||||
showApprovalDialog(forAttachments: attachments, attachmentLimits: attachmentLimits)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -105,13 +105,16 @@ extension ConversationViewController {
|
||||
func sendVoiceMessageDraft(_ voiceMemoDraft: VoiceMessageSendableDraft) {
|
||||
inputToolbar?.hideVoiceMemoUI(animated: true)
|
||||
|
||||
let attachmentLimits = OutgoingAttachmentLimits.currentLimits()
|
||||
|
||||
do {
|
||||
let attachment = try voiceMemoDraft.prepareAttachment()
|
||||
let attachment = try voiceMemoDraft.prepareAttachment(attachmentLimits: attachmentLimits)
|
||||
Task { @MainActor in
|
||||
await self.sendAttachments(
|
||||
ApprovedAttachments(nonViewOnceAttachments: [attachment], imageQuality: .standard),
|
||||
messageBody: nil,
|
||||
from: self,
|
||||
attachmentLimits: attachmentLimits,
|
||||
)
|
||||
clearVoiceMessageDraft()
|
||||
}
|
||||
|
||||
@ -25,12 +25,16 @@ extension VoiceMessageSendableDraft {
|
||||
)
|
||||
}
|
||||
|
||||
func prepareAttachment() throws -> PreviewableAttachment {
|
||||
func prepareAttachment(attachmentLimits: OutgoingAttachmentLimits) throws -> PreviewableAttachment {
|
||||
let attachmentUrl = try prepareForSending()
|
||||
|
||||
let dataSource = DataSourcePath(fileUrl: attachmentUrl, ownership: .owned)
|
||||
dataSource.sourceFilename = userVisibleFilename(currentDate: Date())
|
||||
|
||||
return try PreviewableAttachment.voiceMessageAttachment(dataSource: dataSource, dataUTI: UTType.mpeg4Audio.identifier)
|
||||
return try PreviewableAttachment.voiceMessageAttachment(
|
||||
dataSource: dataSource,
|
||||
dataUTI: UTType.mpeg4Audio.identifier,
|
||||
attachmentLimits: attachmentLimits,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -161,6 +161,8 @@ extension UsernameLinkScanQRCodeViewController: PHPickerViewControllerDelegate {
|
||||
return
|
||||
}
|
||||
|
||||
let attachmentLimits = OutgoingAttachmentLimits.currentLimits()
|
||||
|
||||
Task { @MainActor in
|
||||
async let dismiss: Void = { @MainActor () async -> Void in
|
||||
await withCheckedContinuation { continuation in
|
||||
@ -171,7 +173,10 @@ extension UsernameLinkScanQRCodeViewController: PHPickerViewControllerDelegate {
|
||||
}()
|
||||
|
||||
do {
|
||||
let attachment = try await TypedItemProvider.buildVisualMediaAttachment(forItemProvider: selectedItem.itemProvider)
|
||||
let attachment = try await TypedItemProvider.buildVisualMediaAttachment(
|
||||
forItemProvider: selectedItem.itemProvider,
|
||||
attachmentLimits: attachmentLimits,
|
||||
)
|
||||
guard
|
||||
let image = attachment.rawValue.image(),
|
||||
let ciImage = CIImage(image: image)
|
||||
|
||||
@ -8,7 +8,7 @@ import SignalServiceKit
|
||||
import SignalUI
|
||||
|
||||
protocol AttachmentKeyboardDelegate: AnyObject {
|
||||
func didSelectRecentPhoto(asset: PHAsset, attachment: PreviewableAttachment)
|
||||
func didSelectRecentPhoto(asset: PHAsset, attachment: PreviewableAttachment, attachmentLimits: OutgoingAttachmentLimits)
|
||||
func didTapPhotos()
|
||||
func didTapGif()
|
||||
func didTapFile()
|
||||
@ -123,8 +123,8 @@ class AttachmentKeyboard: CustomKeyboard {
|
||||
|
||||
extension AttachmentKeyboard: RecentPhotosDelegate {
|
||||
|
||||
func didSelectRecentPhoto(asset: PHAsset, attachment: PreviewableAttachment) {
|
||||
delegate?.didSelectRecentPhoto(asset: asset, attachment: attachment)
|
||||
func didSelectRecentPhoto(asset: PHAsset, attachment: PreviewableAttachment, attachmentLimits: OutgoingAttachmentLimits) {
|
||||
delegate?.didSelectRecentPhoto(asset: asset, attachment: attachment, attachmentLimits: attachmentLimits)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import SignalServiceKit
|
||||
import SignalUI
|
||||
|
||||
protocol RecentPhotosDelegate: AnyObject {
|
||||
func didSelectRecentPhoto(asset: PHAsset, attachment: PreviewableAttachment)
|
||||
func didSelectRecentPhoto(asset: PHAsset, attachment: PreviewableAttachment, attachmentLimits: OutgoingAttachmentLimits)
|
||||
}
|
||||
|
||||
class RecentPhotosCollectionView: UICollectionView {
|
||||
@ -256,13 +256,14 @@ extension RecentPhotosCollectionView: UICollectionViewDelegate, UICollectionView
|
||||
|
||||
self.fetchingAttachmentIndex = indexPath
|
||||
let asset = collectionContents.asset(at: indexPath.item)
|
||||
let attachmentLimits = OutgoingAttachmentLimits.currentLimits()
|
||||
Task {
|
||||
defer {
|
||||
self.fetchingAttachmentIndex = nil
|
||||
}
|
||||
do {
|
||||
let attachment = try await collectionContents.outgoingAttachment(for: asset)
|
||||
self.recentPhotosDelegate?.didSelectRecentPhoto(asset: asset, attachment: attachment)
|
||||
let attachment = try await collectionContents.outgoingAttachment(for: asset, attachmentLimits: attachmentLimits)
|
||||
self.recentPhotosDelegate?.didSelectRecentPhoto(asset: asset, attachment: attachment, attachmentLimits: attachmentLimits)
|
||||
} catch {
|
||||
Logger.warn("\(error)")
|
||||
switch error {
|
||||
|
||||
@ -26,10 +26,12 @@ class CameraFirstCaptureSendFlow {
|
||||
private var selectedConversations: [ConversationItem] { selection.conversations }
|
||||
|
||||
private let storiesOnly: Bool
|
||||
private let attachmentLimits: OutgoingAttachmentLimits
|
||||
private var showsStoriesInPicker = true
|
||||
|
||||
init(storiesOnly: Bool, delegate: CameraFirstCaptureDelegate) {
|
||||
init(storiesOnly: Bool, attachmentLimits: OutgoingAttachmentLimits, delegate: CameraFirstCaptureDelegate) {
|
||||
self.storiesOnly = storiesOnly
|
||||
self.attachmentLimits = attachmentLimits
|
||||
self.delegate = delegate
|
||||
}
|
||||
|
||||
@ -167,12 +169,13 @@ extension CameraFirstCaptureSendFlow: ConversationPickerDelegate {
|
||||
if let approvedAttachments {
|
||||
let approvedMessageBody = self.approvedMessageBody
|
||||
let selectedConversations = self.selectedConversations
|
||||
Task { @MainActor in
|
||||
Task { @MainActor [attachmentLimits] in
|
||||
do {
|
||||
_ = try await AttachmentMultisend.enqueueApprovedMedia(
|
||||
conversations: selectedConversations,
|
||||
approvedMessageBody: approvedMessageBody,
|
||||
approvedAttachments: approvedAttachments,
|
||||
attachmentLimits: attachmentLimits,
|
||||
)
|
||||
self.delegate?.cameraFirstCaptureSendFlowDidComplete(self)
|
||||
} catch {
|
||||
|
||||
@ -20,14 +20,20 @@ class ForwardMessageViewController: OWSNavigationController {
|
||||
private typealias Content = ForwardMessageContent
|
||||
|
||||
private var content: Content
|
||||
private let attachmentLimits: OutgoingAttachmentLimits
|
||||
|
||||
private var textMessage: String?
|
||||
|
||||
private let selection = ConversationPickerSelection()
|
||||
var selectedConversations: [ConversationItem] { selection.conversations }
|
||||
|
||||
private init(content: Content) {
|
||||
private init(
|
||||
content: Content,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
) {
|
||||
self.content = content
|
||||
self.attachmentLimits = attachmentLimits
|
||||
|
||||
self.pickerVC = ConversationPickerViewController(
|
||||
selection: selection,
|
||||
overrideTitle: OWSLocalizedString(
|
||||
@ -71,11 +77,12 @@ class ForwardMessageViewController: OWSNavigationController {
|
||||
from fromViewController: UIViewController,
|
||||
delegate: ForwardMessageDelegate,
|
||||
) {
|
||||
let attachmentLimits = OutgoingAttachmentLimits.currentLimits()
|
||||
do {
|
||||
let content: Content = try SSKEnvironment.shared.databaseStorageRef.read { tx in
|
||||
return try Content.build(itemViewModel: itemViewModel, tx: tx)
|
||||
return try Content.build(itemViewModel: itemViewModel, attachmentLimits: attachmentLimits, tx: tx)
|
||||
}
|
||||
present(content: content, from: fromViewController, delegate: delegate)
|
||||
present(content: content, from: fromViewController, attachmentLimits: attachmentLimits, delegate: delegate)
|
||||
} catch {
|
||||
ForwardMessageViewController.showAlertForForwardError(error: error, forwardedInteractionCount: 1)
|
||||
}
|
||||
@ -86,11 +93,12 @@ class ForwardMessageViewController: OWSNavigationController {
|
||||
from fromViewController: UIViewController,
|
||||
delegate: ForwardMessageDelegate,
|
||||
) {
|
||||
let attachmentLimits = OutgoingAttachmentLimits.currentLimits()
|
||||
do {
|
||||
let content: Content = try SSKEnvironment.shared.databaseStorageRef.read { tx in
|
||||
try Content.build(selectionItems: selectionItems, tx: tx)
|
||||
try Content.build(selectionItems: selectionItems, attachmentLimits: attachmentLimits, tx: tx)
|
||||
}
|
||||
present(content: content, from: fromViewController, delegate: delegate)
|
||||
present(content: content, from: fromViewController, attachmentLimits: attachmentLimits, delegate: delegate)
|
||||
} catch {
|
||||
ForwardMessageViewController.showAlertForForwardError(error: error, forwardedInteractionCount: selectionItems.count)
|
||||
}
|
||||
@ -102,13 +110,15 @@ class ForwardMessageViewController: OWSNavigationController {
|
||||
from fromViewController: UIViewController,
|
||||
delegate: ForwardMessageDelegate,
|
||||
) {
|
||||
let attachmentLimits = OutgoingAttachmentLimits.currentLimits()
|
||||
do {
|
||||
let attachments = try attachmentStreams.map { attachmentStream in
|
||||
try SignalAttachmentCloner.cloneAsSignalAttachment(attachment: attachmentStream)
|
||||
try SignalAttachmentCloner.cloneAsSignalAttachment(attachment: attachmentStream, attachmentLimits: attachmentLimits)
|
||||
}
|
||||
present(
|
||||
content: ForwardMessageContent(allItems: [ForwardMessageItem(interaction: message, attachments: attachments)]),
|
||||
from: fromViewController,
|
||||
attachmentLimits: attachmentLimits,
|
||||
delegate: delegate,
|
||||
)
|
||||
} catch let error {
|
||||
@ -124,6 +134,7 @@ class ForwardMessageViewController: OWSNavigationController {
|
||||
from fromViewController: UIViewController,
|
||||
delegate: ForwardMessageDelegate,
|
||||
) {
|
||||
let attachmentLimits = OutgoingAttachmentLimits.currentLimits()
|
||||
var attachments: [PreviewableAttachment] = []
|
||||
var textAttachment: TextAttachment?
|
||||
switch storyMessage.attachment {
|
||||
@ -144,7 +155,7 @@ class ForwardMessageViewController: OWSNavigationController {
|
||||
guard let attachment else {
|
||||
throw OWSAssertionError("Missing attachment stream for forwarded story message")
|
||||
}
|
||||
let signalAttachment = try SignalAttachmentCloner.cloneAsSignalAttachment(attachment: attachment)
|
||||
let signalAttachment = try SignalAttachmentCloner.cloneAsSignalAttachment(attachment: attachment, attachmentLimits: attachmentLimits)
|
||||
attachments = [signalAttachment]
|
||||
} catch let error {
|
||||
ForwardMessageViewController.showAlertForForwardError(
|
||||
@ -159,6 +170,7 @@ class ForwardMessageViewController: OWSNavigationController {
|
||||
present(
|
||||
content: ForwardMessageContent(allItems: [ForwardMessageItem(attachments: attachments, textAttachment: textAttachment)]),
|
||||
from: fromViewController,
|
||||
attachmentLimits: attachmentLimits,
|
||||
delegate: delegate,
|
||||
)
|
||||
}
|
||||
@ -171,6 +183,7 @@ class ForwardMessageViewController: OWSNavigationController {
|
||||
present(
|
||||
content: ForwardMessageContent(allItems: [ForwardMessageItem(messageBody: messageBody)]),
|
||||
from: fromViewController,
|
||||
attachmentLimits: .currentLimits(),
|
||||
delegate: delegate,
|
||||
)
|
||||
}
|
||||
@ -178,9 +191,10 @@ class ForwardMessageViewController: OWSNavigationController {
|
||||
private class func present(
|
||||
content: Content,
|
||||
from fromViewController: UIViewController,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
delegate: ForwardMessageDelegate,
|
||||
) {
|
||||
let sheet = ForwardMessageViewController(content: content)
|
||||
let sheet = ForwardMessageViewController(content: content, attachmentLimits: attachmentLimits)
|
||||
sheet.forwardMessageDelegate = delegate
|
||||
fromViewController.present(sheet, animated: true) {
|
||||
UIApplication.shared.hideKeyboard()
|
||||
@ -346,6 +360,7 @@ extension ForwardMessageViewController {
|
||||
conversations: conversations,
|
||||
approvedMessageBody: item.messageBody,
|
||||
approvedAttachments: ApprovedAttachments(nonViewOnceAttachments: item.attachments, imageQuality: .high),
|
||||
attachmentLimits: attachmentLimits,
|
||||
)
|
||||
} else if let textAttachment = item.textAttachment {
|
||||
// TODO: we want to reuse the uploaded link preview image attachment instead of re-uploading
|
||||
@ -569,6 +584,7 @@ struct ForwardMessageItem {
|
||||
interaction: TSInteraction,
|
||||
componentState: CVComponentState,
|
||||
selectionType: CVSelectionType,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
transaction: DBReadTransaction,
|
||||
) throws -> Self {
|
||||
let shouldHaveText = (selectionType == .allContent || selectionType == .secondaryContent)
|
||||
@ -622,7 +638,7 @@ struct ForwardMessageItem {
|
||||
}
|
||||
|
||||
attachments = try attachmentStreams.map { attachmentStream in
|
||||
try SignalAttachmentCloner.cloneAsSignalAttachment(attachment: attachmentStream)
|
||||
try SignalAttachmentCloner.cloneAsSignalAttachment(attachment: attachmentStream, attachmentLimits: attachmentLimits)
|
||||
}
|
||||
|
||||
stickerMetadata = componentState.stickerMetadata
|
||||
@ -742,18 +758,21 @@ private struct ForwardMessageContent {
|
||||
|
||||
static func build(
|
||||
itemViewModel: CVItemViewModelImpl,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
tx: DBReadTransaction,
|
||||
) throws -> Self {
|
||||
return Self(allItems: [try ForwardMessageItem.build(
|
||||
interaction: itemViewModel.interaction,
|
||||
componentState: itemViewModel.renderItem.componentState,
|
||||
selectionType: .allContent,
|
||||
attachmentLimits: attachmentLimits,
|
||||
transaction: tx,
|
||||
)])
|
||||
}
|
||||
|
||||
static func build(
|
||||
selectionItems: [CVSelectionItem],
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
tx: DBReadTransaction,
|
||||
) throws -> Self {
|
||||
let items = try selectionItems.map { selectionItem throws -> ForwardMessageItem in
|
||||
@ -766,6 +785,7 @@ private struct ForwardMessageContent {
|
||||
interaction: interaction,
|
||||
componentState: componentState,
|
||||
selectionType: selectionItem.selectionType,
|
||||
attachmentLimits: attachmentLimits,
|
||||
transaction: tx,
|
||||
)
|
||||
}
|
||||
|
||||
@ -31,12 +31,13 @@ class GifPickerNavigationViewController: OWSNavigationController {
|
||||
}
|
||||
|
||||
extension GifPickerNavigationViewController: GifPickerViewControllerDelegate {
|
||||
func gifPickerDidSelect(attachment: PreviewableAttachment) {
|
||||
func gifPickerDidSelect(attachment: PreviewableAttachment, attachmentLimits: OutgoingAttachmentLimits) {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
let attachmentApprovalItem = AttachmentApprovalItem(attachment: attachment, canSave: false)
|
||||
let attachmentApproval = AttachmentApprovalViewController.loadWithSneakyTransaction(
|
||||
attachmentApprovalItems: [attachmentApprovalItem],
|
||||
attachmentLimits: attachmentLimits,
|
||||
options: self.hasQuotedReplyDraft ? [.disallowViewOnce] : [],
|
||||
)
|
||||
attachmentApproval.approvalDataSource = self
|
||||
@ -60,7 +61,11 @@ extension GifPickerNavigationViewController: AttachmentApprovalViewControllerDel
|
||||
didApproveAttachments approvedAttachments: ApprovedAttachments,
|
||||
messageBody: MessageBody?,
|
||||
) {
|
||||
approvalDelegate?.attachmentApproval(attachmentApproval, didApproveAttachments: approvedAttachments, messageBody: messageBody)
|
||||
approvalDelegate?.attachmentApproval(
|
||||
attachmentApproval,
|
||||
didApproveAttachments: approvedAttachments,
|
||||
messageBody: messageBody,
|
||||
)
|
||||
}
|
||||
|
||||
func attachmentApprovalDidCancel() {
|
||||
@ -99,7 +104,7 @@ extension GifPickerNavigationViewController: AttachmentApprovalViewControllerDat
|
||||
|
||||
protocol GifPickerViewControllerDelegate: AnyObject {
|
||||
@MainActor
|
||||
func gifPickerDidSelect(attachment: PreviewableAttachment)
|
||||
func gifPickerDidSelect(attachment: PreviewableAttachment, attachmentLimits: OutgoingAttachmentLimits)
|
||||
@MainActor
|
||||
func gifPickerDidCancel()
|
||||
}
|
||||
@ -495,12 +500,14 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
|
||||
func getFileForCell(_ cell: GifPickerCell) {
|
||||
GiphyDownloader.giphyDownloader.cancelAllRequests()
|
||||
|
||||
let attachmentLimits = OutgoingAttachmentLimits.currentLimits()
|
||||
|
||||
fileForCellTask?.cancel()
|
||||
fileForCellTask = Task {
|
||||
do {
|
||||
let asset = try await cell.requestRenditionForSending()
|
||||
let attachment = try await buildAttachment(forAsset: asset)
|
||||
self.delegate?.gifPickerDidSelect(attachment: attachment)
|
||||
let attachment = try await buildAttachment(forAsset: asset, attachmentLimits: attachmentLimits)
|
||||
self.delegate?.gifPickerDidSelect(attachment: attachment, attachmentLimits: attachmentLimits)
|
||||
} catch {
|
||||
let alert = ActionSheetController(
|
||||
title: OWSLocalizedString("GIF_PICKER_FAILURE_ALERT_TITLE", comment: "Shown when selected GIF couldn't be fetched"),
|
||||
@ -518,7 +525,10 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
|
||||
}
|
||||
|
||||
@concurrent
|
||||
private nonisolated func buildAttachment(forAsset asset: ProxiedContentAsset) async throws -> PreviewableAttachment {
|
||||
private nonisolated func buildAttachment(
|
||||
forAsset asset: ProxiedContentAsset,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
) async throws -> PreviewableAttachment {
|
||||
guard let giphyAsset = asset.assetDescription as? GiphyAsset else {
|
||||
throw OWSAssertionError("Invalid asset description.")
|
||||
}
|
||||
@ -531,7 +541,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
|
||||
try FileManager.default.copyItem(atPath: assetFilePath, toPath: consumableFilePath)
|
||||
let dataSource = DataSourcePath(filePath: consumableFilePath, ownership: .owned)
|
||||
|
||||
let attachment = try PreviewableAttachment.buildAttachment(dataSource: dataSource, dataUTI: assetTypeIdentifier)
|
||||
let attachment = try PreviewableAttachment.buildAttachment(dataSource: dataSource, dataUTI: assetTypeIdentifier, attachmentLimits: attachmentLimits)
|
||||
attachment.rawValue.isLoopingVideo = attachment.isVideo
|
||||
return attachment
|
||||
}
|
||||
|
||||
@ -17,6 +17,8 @@ extension ChatListViewController: CameraFirstCaptureDelegate {
|
||||
// Dismiss any message actions if they're presented
|
||||
conversationSplitViewController?.selectedConversationViewController?.dismissMessageContextMenu(animated: true)
|
||||
|
||||
let attachmentLimits = OutgoingAttachmentLimits.currentLimits()
|
||||
|
||||
ows_askForCameraPermissions { cameraAccessGranted in
|
||||
guard cameraAccessGranted else {
|
||||
Logger.warn("Camera permission denied")
|
||||
@ -29,7 +31,11 @@ extension ChatListViewController: CameraFirstCaptureDelegate {
|
||||
Logger.warn("Proceeding with no microphone access.")
|
||||
}
|
||||
|
||||
let cameraModal = CameraFirstCaptureNavigationController.cameraFirstModal(hasQuotedReplyDraft: false, delegate: self)
|
||||
let cameraModal = CameraFirstCaptureNavigationController.cameraFirstModal(
|
||||
hasQuotedReplyDraft: false,
|
||||
attachmentLimits: attachmentLimits,
|
||||
delegate: self,
|
||||
)
|
||||
cameraModal.modalPresentationStyle = .overFullScreen
|
||||
|
||||
// Defer hiding status bar until modal is fully onscreen
|
||||
|
||||
@ -259,6 +259,8 @@ class StoriesViewController: OWSViewController, StoryListDataSourceDelegate, Hom
|
||||
// Dismiss any message actions if they're presented
|
||||
conversationSplitViewController?.selectedConversationViewController?.dismissMessageContextMenu(animated: true)
|
||||
|
||||
let attachmentLimits = OutgoingAttachmentLimits.currentLimits()
|
||||
|
||||
ows_askForCameraPermissions { cameraGranted in
|
||||
guard cameraGranted else {
|
||||
return Logger.warn("camera permission denied.")
|
||||
@ -273,6 +275,7 @@ class StoriesViewController: OWSViewController, StoryListDataSourceDelegate, Hom
|
||||
let modal = CameraFirstCaptureNavigationController.cameraFirstModal(
|
||||
storiesOnly: true,
|
||||
hasQuotedReplyDraft: false,
|
||||
attachmentLimits: attachmentLimits,
|
||||
delegate: self,
|
||||
)
|
||||
self.presentFullScreen(modal, animated: true)
|
||||
|
||||
@ -61,6 +61,7 @@ protocol CameraCaptureSessionDelegate: AnyObject {
|
||||
class CameraCaptureSession: NSObject {
|
||||
|
||||
private weak var delegate: CameraCaptureSessionDelegate?
|
||||
private let attachmentLimits: OutgoingAttachmentLimits
|
||||
|
||||
// There can only ever be one `CapturePreviewView` per AVCaptureSession
|
||||
lazy var previewView = CapturePreviewView(session: avCaptureSession)
|
||||
@ -84,12 +85,13 @@ class CameraCaptureSession: NSObject {
|
||||
|
||||
init(
|
||||
delegate: CameraCaptureSessionDelegate,
|
||||
maxPlaintextVideoBytes: UInt64,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
qrCodeSampleBufferScanner: QRCodeSampleBufferScanner,
|
||||
) {
|
||||
self.delegate = delegate
|
||||
self.attachmentLimits = attachmentLimits
|
||||
self.videoCapture = VideoCapture(
|
||||
maxPlaintextVideoBytes: maxPlaintextVideoBytes,
|
||||
attachmentLimits: attachmentLimits,
|
||||
qrCodeSampleBufferScanner: qrCodeSampleBufferScanner,
|
||||
)
|
||||
|
||||
@ -838,6 +840,7 @@ class CameraCaptureSession: NSObject {
|
||||
let attachment = try PreviewableAttachment.videoAttachment(
|
||||
dataSource: dataSource,
|
||||
dataUTI: UTType.mpeg4Movie.identifier,
|
||||
attachmentLimits: self.attachmentLimits,
|
||||
)
|
||||
delegate?.cameraCaptureSession(self, didFinishProcessing: attachment)
|
||||
}
|
||||
@ -1144,7 +1147,7 @@ private enum VideoCaptureError: Error {
|
||||
|
||||
private class VideoCapture: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVCaptureAudioDataOutputSampleBufferDelegate {
|
||||
|
||||
private let maxPlaintextVideoBytes: UInt64
|
||||
private let attachmentLimits: OutgoingAttachmentLimits
|
||||
private let qrCodeSampleBufferScanner: QRCodeSampleBufferScanner
|
||||
|
||||
let videoDataOutput = AVCaptureVideoDataOutput()
|
||||
@ -1179,8 +1182,8 @@ private class VideoCapture: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
|
||||
}
|
||||
}
|
||||
|
||||
init(maxPlaintextVideoBytes: UInt64, qrCodeSampleBufferScanner: QRCodeSampleBufferScanner) {
|
||||
self.maxPlaintextVideoBytes = maxPlaintextVideoBytes
|
||||
init(attachmentLimits: OutgoingAttachmentLimits, qrCodeSampleBufferScanner: QRCodeSampleBufferScanner) {
|
||||
self.attachmentLimits = attachmentLimits
|
||||
self.qrCodeSampleBufferScanner = qrCodeSampleBufferScanner
|
||||
|
||||
super.init()
|
||||
@ -1409,7 +1412,7 @@ private class VideoCapture: NSObject, AVCaptureVideoDataOutputSampleBufferDelega
|
||||
if
|
||||
shouldCheckFileSize,
|
||||
let fileSize = (try? OWSFileSystem.fileSize(of: assetWriter.outputURL)),
|
||||
(fileSize + estimatedTeardownOverhead) >= self.maxPlaintextVideoBytes
|
||||
(fileSize + estimatedTeardownOverhead) >= self.attachmentLimits.maxPlaintextVideoBytes
|
||||
{
|
||||
Logger.warn("stopping recording before hitting max file size")
|
||||
needsFinishAssetWriterSession = true
|
||||
|
||||
@ -37,6 +37,12 @@ protocol PhotoCaptureViewControllerDataSource: AnyObject {
|
||||
}
|
||||
|
||||
class PhotoCaptureViewController: OWSViewController, OWSNavigationChildController {
|
||||
private let attachmentLimits: OutgoingAttachmentLimits
|
||||
|
||||
init(attachmentLimits: OutgoingAttachmentLimits) {
|
||||
self.attachmentLimits = attachmentLimits
|
||||
super.init()
|
||||
}
|
||||
|
||||
weak var delegate: PhotoCaptureViewControllerDelegate?
|
||||
weak var dataSource: PhotoCaptureViewControllerDataSource?
|
||||
@ -45,7 +51,7 @@ class PhotoCaptureViewController: OWSViewController, OWSNavigationChildControlle
|
||||
private lazy var qrCodeSampleBufferScanner = QRCodeSampleBufferScanner(delegate: self)
|
||||
private lazy var cameraCaptureSession = CameraCaptureSession(
|
||||
delegate: self,
|
||||
maxPlaintextVideoBytes: OutgoingAttachmentLimits.currentLimits().maxPlaintextVideoBytes,
|
||||
attachmentLimits: attachmentLimits,
|
||||
qrCodeSampleBufferScanner: qrCodeSampleBufferScanner,
|
||||
)
|
||||
|
||||
|
||||
@ -136,14 +136,14 @@ class PhotoAlbumContents {
|
||||
}
|
||||
}
|
||||
|
||||
func outgoingAttachment(for asset: PHAsset) async throws -> PreviewableAttachment {
|
||||
func outgoingAttachment(for asset: PHAsset, attachmentLimits: OutgoingAttachmentLimits) async throws -> PreviewableAttachment {
|
||||
switch asset.mediaType {
|
||||
case .image:
|
||||
let (dataSource, dataUTI) = try await requestImageDataSource(for: asset)
|
||||
return try PreviewableAttachment.imageAttachment(dataSource: dataSource, dataUTI: dataUTI)
|
||||
case .video:
|
||||
let video = try await requestVideoDataSource(for: asset)
|
||||
return try await PreviewableAttachment.compressVideoAsMp4(asset: video, baseFilename: nil)
|
||||
return try await PreviewableAttachment.compressVideoAsMp4(asset: video, baseFilename: nil, attachmentLimits: attachmentLimits)
|
||||
case .unknown, .audio:
|
||||
fallthrough
|
||||
@unknown default:
|
||||
|
||||
@ -13,7 +13,11 @@ protocol SendMediaNavDelegate: AnyObject {
|
||||
|
||||
func sendMediaNavDidCancel(_ sendMediaNavigationController: SendMediaNavigationController)
|
||||
|
||||
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didApproveAttachments approvedAttachments: ApprovedAttachments, messageBody: MessageBody?)
|
||||
func sendMediaNav(
|
||||
_ sendMediaNavigationController: SendMediaNavigationController,
|
||||
didApproveAttachments approvedAttachments: ApprovedAttachments,
|
||||
messageBody: MessageBody?,
|
||||
)
|
||||
|
||||
func sendMediaNav(_ sendMediaNavigationController: SendMediaNavigationController, didFinishWithTextAttachment textAttachment: UnsentTextAttachment)
|
||||
|
||||
@ -48,12 +52,17 @@ class CameraFirstCaptureNavigationController: SendMediaNavigationController {
|
||||
class func cameraFirstModal(
|
||||
storiesOnly: Bool = false,
|
||||
hasQuotedReplyDraft: Bool,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
delegate: CameraFirstCaptureDelegate,
|
||||
) -> CameraFirstCaptureNavigationController {
|
||||
let navController = CameraFirstCaptureNavigationController(hasQuotedReplyDraft: hasQuotedReplyDraft)
|
||||
let navController = CameraFirstCaptureNavigationController(hasQuotedReplyDraft: hasQuotedReplyDraft, attachmentLimits: attachmentLimits)
|
||||
navController.setViewControllers([navController.captureViewController], animated: false)
|
||||
|
||||
let cameraFirstCaptureSendFlow = CameraFirstCaptureSendFlow(storiesOnly: storiesOnly, delegate: delegate)
|
||||
let cameraFirstCaptureSendFlow = CameraFirstCaptureSendFlow(
|
||||
storiesOnly: storiesOnly,
|
||||
attachmentLimits: attachmentLimits,
|
||||
delegate: delegate,
|
||||
)
|
||||
navController.cameraFirstCaptureSendFlow = cameraFirstCaptureSendFlow
|
||||
navController.sendMediaNavDelegate = cameraFirstCaptureSendFlow
|
||||
navController.sendMediaNavDataSource = cameraFirstCaptureSendFlow
|
||||
@ -74,9 +83,11 @@ class SendMediaNavigationController: OWSNavigationController {
|
||||
fileprivate var storiesOnly: Bool = false
|
||||
|
||||
private let hasQuotedReplyDraft: Bool
|
||||
let attachmentLimits: OutgoingAttachmentLimits
|
||||
|
||||
fileprivate init(hasQuotedReplyDraft: Bool) {
|
||||
fileprivate init(hasQuotedReplyDraft: Bool, attachmentLimits: OutgoingAttachmentLimits) {
|
||||
self.hasQuotedReplyDraft = hasQuotedReplyDraft
|
||||
self.attachmentLimits = attachmentLimits
|
||||
super.init()
|
||||
}
|
||||
|
||||
@ -91,8 +102,8 @@ class SendMediaNavigationController: OWSNavigationController {
|
||||
weak var sendMediaNavDelegate: SendMediaNavDelegate?
|
||||
weak var sendMediaNavDataSource: SendMediaNavDataSource?
|
||||
|
||||
class func showingCameraFirst(hasQuotedReplyDraft: Bool) -> SendMediaNavigationController {
|
||||
let navController = SendMediaNavigationController(hasQuotedReplyDraft: hasQuotedReplyDraft)
|
||||
class func showingCameraFirst(hasQuotedReplyDraft: Bool, attachmentLimits: OutgoingAttachmentLimits) -> SendMediaNavigationController {
|
||||
let navController = SendMediaNavigationController(hasQuotedReplyDraft: hasQuotedReplyDraft, attachmentLimits: attachmentLimits)
|
||||
navController.setViewControllers([navController.captureViewController], animated: false)
|
||||
return navController
|
||||
}
|
||||
@ -107,13 +118,13 @@ class SendMediaNavigationController: OWSNavigationController {
|
||||
return config
|
||||
}
|
||||
|
||||
class func showingNativePicker(hasQuotedReplyDraft: Bool) -> SendMediaNavigationController {
|
||||
class func showingNativePicker(hasQuotedReplyDraft: Bool, attachmentLimits: OutgoingAttachmentLimits) -> SendMediaNavigationController {
|
||||
// We want to present the photo picker in a sheet and then have the
|
||||
// editor appear behind it after you select photos, so present this
|
||||
// navigation controller as transparent with an empty view, then when
|
||||
// you select photos, `showApprovalViewController` will make it appear
|
||||
// behind the dismissing sheet and transition to the editor.
|
||||
let navController = SendMediaNavigationController(hasQuotedReplyDraft: hasQuotedReplyDraft)
|
||||
let navController = SendMediaNavigationController(hasQuotedReplyDraft: hasQuotedReplyDraft, attachmentLimits: attachmentLimits)
|
||||
navController.pushViewController(UIViewController(), animated: false)
|
||||
navController.view.layer.opacity = 0
|
||||
navController.modalPresentationStyle = .overCurrentContext
|
||||
@ -141,10 +152,11 @@ class SendMediaNavigationController: OWSNavigationController {
|
||||
asset: PHAsset,
|
||||
attachment: PreviewableAttachment,
|
||||
hasQuotedReplyDraft: Bool,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
delegate: SendMediaNavDelegate,
|
||||
dataSource: SendMediaNavDataSource,
|
||||
) -> SendMediaNavigationController {
|
||||
let navController = SendMediaNavigationController(hasQuotedReplyDraft: hasQuotedReplyDraft)
|
||||
let navController = SendMediaNavigationController(hasQuotedReplyDraft: hasQuotedReplyDraft, attachmentLimits: attachmentLimits)
|
||||
navController.sendMediaNavDelegate = delegate
|
||||
navController.sendMediaNavDataSource = dataSource
|
||||
navController.modalPresentationStyle = .overCurrentContext
|
||||
@ -175,7 +187,7 @@ class SendMediaNavigationController: OWSNavigationController {
|
||||
// MARK: - Child View Controllers
|
||||
|
||||
fileprivate lazy var captureViewController: PhotoCaptureViewController = {
|
||||
let viewController = PhotoCaptureViewController()
|
||||
let viewController = PhotoCaptureViewController(attachmentLimits: attachmentLimits)
|
||||
viewController.delegate = self
|
||||
viewController.dataSource = self
|
||||
return viewController
|
||||
@ -208,6 +220,7 @@ class SendMediaNavigationController: OWSNavigationController {
|
||||
}
|
||||
let approvalViewController = AttachmentApprovalViewController.loadWithSneakyTransaction(
|
||||
attachmentApprovalItems: pendingAttachments.map(\.approvalItem),
|
||||
attachmentLimits: attachmentLimits,
|
||||
options: options,
|
||||
)
|
||||
approvalViewController.approvalDelegate = self
|
||||
@ -408,8 +421,11 @@ extension SendMediaNavigationController: PHPickerViewControllerDelegate {
|
||||
}
|
||||
} else {
|
||||
didAddAttachments = true
|
||||
return {
|
||||
let attachment = try await TypedItemProvider.buildVisualMediaAttachment(forItemProvider: result.itemProvider)
|
||||
return { [attachmentLimits] in
|
||||
let attachment = try await TypedItemProvider.buildVisualMediaAttachment(
|
||||
forItemProvider: result.itemProvider,
|
||||
attachmentLimits: attachmentLimits,
|
||||
)
|
||||
let approvalItem = AttachmentApprovalItem(attachment: attachment, canSave: false)
|
||||
return PendingAttachment(source: .systemLibrary(systemIdentifier: assetIdentifier), approvalItem: approvalItem)
|
||||
}
|
||||
@ -454,8 +470,11 @@ extension SendMediaNavigationController: PHPickerViewControllerDelegate {
|
||||
continue
|
||||
}
|
||||
didAddAttachments = true
|
||||
resolvablePendingAttachments.append({
|
||||
let attachment = try await TypedItemProvider.buildVisualMediaAttachment(forItemProvider: result.itemProvider)
|
||||
resolvablePendingAttachments.append({ [attachmentLimits] in
|
||||
let attachment = try await TypedItemProvider.buildVisualMediaAttachment(
|
||||
forItemProvider: result.itemProvider,
|
||||
attachmentLimits: attachmentLimits,
|
||||
)
|
||||
let approvalItem = AttachmentApprovalItem(attachment: attachment, canSave: false)
|
||||
return PendingAttachment(source: .systemLibrary(systemIdentifier: assetIdentifier), approvalItem: approvalItem)
|
||||
})
|
||||
|
||||
@ -36,13 +36,21 @@ public struct IncomingAttachmentLimits {
|
||||
/// Limits imposed on attachments we send to others.
|
||||
public struct OutgoingAttachmentLimits {
|
||||
private let remoteConfig: RemoteConfig
|
||||
private let callingCode: Int?
|
||||
|
||||
public static func currentLimits(remoteConfig: RemoteConfig = .current) -> Self {
|
||||
return Self(remoteConfig: remoteConfig)
|
||||
public static func currentLimits(
|
||||
remoteConfig: RemoteConfig = .current,
|
||||
callingCode: Int? = ImageQualityLevel.defaultCallingCode(),
|
||||
) -> Self {
|
||||
return Self(remoteConfig: remoteConfig, callingCode: callingCode)
|
||||
}
|
||||
|
||||
init(remoteConfig: RemoteConfig) {
|
||||
init(
|
||||
remoteConfig: RemoteConfig,
|
||||
callingCode: Int?,
|
||||
) {
|
||||
self.remoteConfig = remoteConfig
|
||||
self.callingCode = callingCode
|
||||
}
|
||||
|
||||
// MARK: - Overall
|
||||
@ -68,4 +76,11 @@ public struct OutgoingAttachmentLimits {
|
||||
}
|
||||
return maxPlaintextBytes
|
||||
}
|
||||
|
||||
public var standardQualityLevel: ImageQualityLevel {
|
||||
return ImageQualityLevel.standardQualityLevel(
|
||||
remoteConfig: remoteConfig,
|
||||
callingCode: callingCode,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -610,8 +610,8 @@ private enum ValueFlag: String, FlagType {
|
||||
var isHotSwappable: Bool {
|
||||
switch self {
|
||||
case .applePayDisabledRegions: true
|
||||
case .attachmentMaxEncryptedBytes: false
|
||||
case .attachmentMaxEncryptedReceiveBytes: false
|
||||
case .attachmentMaxEncryptedBytes: true
|
||||
case .attachmentMaxEncryptedReceiveBytes: true
|
||||
case .automaticSessionResetAttemptInterval: true
|
||||
case .backgroundRefreshInterval: true
|
||||
case .callQualitySurveyPPM: true
|
||||
@ -633,7 +633,7 @@ private enum ValueFlag: String, FlagType {
|
||||
case .reactiveProfileKeyAttemptInterval: true
|
||||
case .replaceableInteractionExpiration: false
|
||||
case .sepaEnabledRegions: true
|
||||
case .standardMediaQualityLevel: false
|
||||
case .standardMediaQualityLevel: true
|
||||
case .backupListMediaDefaultRefreshIntervalMs: true
|
||||
case .backupListMediaOutOfQuotaRefreshIntervalMs: true
|
||||
case .pinnedMessageLimit: true
|
||||
|
||||
@ -55,17 +55,19 @@ public enum ImageQualityLevel: UInt, Comparable {
|
||||
// High quality is always level three. If not remotely specified, standard
|
||||
// uses quality level two.
|
||||
public static func standardQualityLevel(
|
||||
remoteConfig: RemoteConfig = .current,
|
||||
callingCode: Int? = { () -> Int? in
|
||||
let phoneNumberUtil = SSKEnvironment.shared.phoneNumberUtilRef
|
||||
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
||||
let localIdentifiers = tsAccountManager.localIdentifiersWithMaybeSneakyTransaction
|
||||
return localIdentifiers.flatMap({ phoneNumberUtil.parseE164($0.phoneNumber) })?.getCallingCode()
|
||||
}(),
|
||||
remoteConfig: RemoteConfig,
|
||||
callingCode: Int?,
|
||||
) -> ImageQualityLevel {
|
||||
return remoteConfig.standardMediaQualityLevel(callingCode: callingCode) ?? .two
|
||||
}
|
||||
|
||||
public static func defaultCallingCode(
|
||||
phoneNumberUtil: PhoneNumberUtil = SSKEnvironment.shared.phoneNumberUtilRef,
|
||||
localIdentifiers: LocalIdentifiers? = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction,
|
||||
) -> Int? {
|
||||
return localIdentifiers.flatMap({ phoneNumberUtil.parseE164($0.phoneNumber) })?.getCallingCode()
|
||||
}
|
||||
|
||||
public var startingTier: ImageQualityTier {
|
||||
switch self {
|
||||
case .one: return .four
|
||||
@ -109,7 +111,7 @@ public enum ImageQualityLevel: UInt, Comparable {
|
||||
|
||||
public static func resolvedValue(
|
||||
imageQuality: ImageQuality,
|
||||
standardQualityLevel: @autoclosure () -> Self = .standardQualityLevel(),
|
||||
standardQualityLevel: Self,
|
||||
maximumForCurrentAppContext: Self = .maximumForCurrentAppContext(),
|
||||
) -> ImageQualityLevel {
|
||||
let targetQualityLevel: Self
|
||||
@ -117,7 +119,7 @@ public enum ImageQualityLevel: UInt, Comparable {
|
||||
case .high:
|
||||
targetQualityLevel = .three
|
||||
case .standard:
|
||||
targetQualityLevel = standardQualityLevel()
|
||||
targetQualityLevel = standardQualityLevel
|
||||
}
|
||||
// If the max quality we allow is less than the stored preference,
|
||||
// we have to restrict ourselves to the max allowed.
|
||||
|
||||
@ -151,9 +151,12 @@ public class ShareViewController: OWSNavigationController, ShareViewDelegate, SA
|
||||
let chatConnectionManager = DependenciesBridge.shared.chatConnectionManager
|
||||
self.connectionTokens.append(chatConnectionManager.requestUnidentifiedConnection())
|
||||
|
||||
let attachmentLimits = OutgoingAttachmentLimits.currentLimits()
|
||||
|
||||
let conversationPicker: SharingThreadPickerViewController
|
||||
conversationPicker = SharingThreadPickerViewController(
|
||||
areAttachmentStoriesCompatPrecheck: typedItemProviders.allSatisfy { $0.isStoriesCompatible },
|
||||
attachmentLimits: attachmentLimits,
|
||||
shareViewDelegate: self,
|
||||
)
|
||||
|
||||
@ -202,6 +205,7 @@ public class ShareViewController: OWSNavigationController, ShareViewDelegate, SA
|
||||
}()
|
||||
typedItems = try await buildAndValidateAttachments(
|
||||
for: typedItemProviders,
|
||||
attachmentLimits: attachmentLimits,
|
||||
setProgress: { loadViewControllerForProgress?.progress = $0 },
|
||||
)
|
||||
} catch {
|
||||
@ -382,6 +386,7 @@ public class ShareViewController: OWSNavigationController, ShareViewDelegate, SA
|
||||
|
||||
private func buildAndValidateAttachments(
|
||||
for typedItemProviders: [TypedItemProvider],
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
setProgress: @MainActor (Progress) -> Void,
|
||||
) async throws -> [TypedItem] {
|
||||
let progress = Progress(totalUnitCount: Int64(typedItemProviders.count))
|
||||
@ -394,7 +399,7 @@ public class ShareViewController: OWSNavigationController, ShareViewDelegate, SA
|
||||
|
||||
setProgress(progress)
|
||||
|
||||
let typedItems = try await self.buildAttachments(for: itemsAndProgresses)
|
||||
let typedItems = try await self.buildAttachments(for: itemsAndProgresses, attachmentLimits: attachmentLimits)
|
||||
try Task.checkCancellation()
|
||||
|
||||
// Make sure the user is not trying to share more than our attachment limit.
|
||||
@ -461,11 +466,14 @@ public class ShareViewController: OWSNavigationController, ShareViewDelegate, SA
|
||||
throw ShareViewControllerError.noConformingInputItem
|
||||
}
|
||||
|
||||
private nonisolated func buildAttachments(for itemsAndProgresses: [(TypedItemProvider, Progress)]) async throws -> [TypedItem] {
|
||||
private nonisolated func buildAttachments(
|
||||
for itemsAndProgresses: [(TypedItemProvider, Progress)],
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
) async throws -> [TypedItem] {
|
||||
// FIXME: does not use a task group because SignalAttachment likes to load things into RAM and resize them; doing this in parallel can exhaust available RAM
|
||||
var result: [TypedItem] = []
|
||||
for (typedItemProvider, progress) in itemsAndProgresses {
|
||||
result.append(try await typedItemProvider.buildAttachment(progress: progress))
|
||||
result.append(try await typedItemProvider.buildAttachment(attachmentLimits: attachmentLimits, progress: progress))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@ -26,6 +26,8 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
|
||||
/// actually sending if stories are selected.
|
||||
let areAttachmentStoriesCompatPrecheck: Bool
|
||||
|
||||
private let attachmentLimits: OutgoingAttachmentLimits
|
||||
|
||||
var typedItems: [TypedItem] {
|
||||
didSet {
|
||||
owsPrecondition(typedItems.count <= 1 || typedItems.allSatisfy(\.isVisualMedia))
|
||||
@ -38,9 +40,14 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
|
||||
|
||||
private var selectedConversations: [ConversationItem] { selection.conversations }
|
||||
|
||||
init(areAttachmentStoriesCompatPrecheck: Bool, shareViewDelegate: ShareViewDelegate) {
|
||||
init(
|
||||
areAttachmentStoriesCompatPrecheck: Bool,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
shareViewDelegate: ShareViewDelegate,
|
||||
) {
|
||||
self.typedItems = []
|
||||
self.areAttachmentStoriesCompatPrecheck = areAttachmentStoriesCompatPrecheck
|
||||
self.attachmentLimits = attachmentLimits
|
||||
self.shareViewDelegate = shareViewDelegate
|
||||
|
||||
super.init(selection: ConversationPickerSelection())
|
||||
@ -165,7 +172,11 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
|
||||
if self.selection.conversations.contains(where: \.isStory) {
|
||||
approvalVCOptions.insert(.disallowViewOnce)
|
||||
}
|
||||
let approvalView = AttachmentApprovalViewController.loadWithSneakyTransaction(attachmentApprovalItems: approvalItems, options: approvalVCOptions)
|
||||
let approvalView = AttachmentApprovalViewController.loadWithSneakyTransaction(
|
||||
attachmentApprovalItems: approvalItems,
|
||||
attachmentLimits: attachmentLimits,
|
||||
options: approvalVCOptions,
|
||||
)
|
||||
approvalVC = approvalView
|
||||
approvalView.approvalDelegate = self
|
||||
approvalView.approvalDataSource = self
|
||||
@ -287,6 +298,7 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
|
||||
conversations: selectedConversations,
|
||||
approvedMessageBody: messageBody,
|
||||
approvedAttachments: attachments,
|
||||
attachmentLimits: attachmentLimits,
|
||||
)
|
||||
} catch {
|
||||
return SendFailure(outgoingMessages: [], error: error)
|
||||
|
||||
@ -151,14 +151,18 @@ public final class AttachmentApprovalViewController: UIPageViewController, UIPag
|
||||
}
|
||||
}
|
||||
|
||||
public let attachmentLimits: OutgoingAttachmentLimits
|
||||
|
||||
public static func loadWithSneakyTransaction(
|
||||
attachmentApprovalItems: [AttachmentApprovalItem],
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
options: AttachmentApprovalViewControllerOptions,
|
||||
) -> Self {
|
||||
let databaseStorage = SSKEnvironment.shared.databaseStorageRef
|
||||
return Self(
|
||||
attachmentApprovalItems: attachmentApprovalItems,
|
||||
defaultImageQuality: databaseStorage.read(block: ImageQuality.fetchValue(tx:)),
|
||||
attachmentLimits: attachmentLimits,
|
||||
options: options,
|
||||
)
|
||||
}
|
||||
@ -166,11 +170,13 @@ public final class AttachmentApprovalViewController: UIPageViewController, UIPag
|
||||
private init(
|
||||
attachmentApprovalItems: [AttachmentApprovalItem],
|
||||
defaultImageQuality: ImageQuality,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
options: AttachmentApprovalViewControllerOptions,
|
||||
) {
|
||||
assert(attachmentApprovalItems.count > 0)
|
||||
|
||||
self.outputImageQuality = defaultImageQuality
|
||||
self.attachmentLimits = attachmentLimits
|
||||
self.receivedOptions = options
|
||||
|
||||
let pageOptions: [UIPageViewController.OptionsKey: Any] = [.interPageSpacing: kSpacingBetweenItems]
|
||||
@ -210,6 +216,7 @@ public final class AttachmentApprovalViewController: UIPageViewController, UIPag
|
||||
attachments: [PreviewableAttachment],
|
||||
initialMessageBody: MessageBody?,
|
||||
hasQuotedReplyDraft: Bool,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
approvalDelegate: AttachmentApprovalViewControllerDelegate,
|
||||
approvalDataSource: AttachmentApprovalViewControllerDataSource,
|
||||
stickerSheetDelegate: StickerPickerSheetDelegate?,
|
||||
@ -221,7 +228,11 @@ public final class AttachmentApprovalViewController: UIPageViewController, UIPag
|
||||
if hasQuotedReplyDraft {
|
||||
options.insert(.disallowViewOnce)
|
||||
}
|
||||
let vc = AttachmentApprovalViewController.loadWithSneakyTransaction(attachmentApprovalItems: attachmentApprovalItems, options: options)
|
||||
let vc = AttachmentApprovalViewController.loadWithSneakyTransaction(
|
||||
attachmentApprovalItems: attachmentApprovalItems,
|
||||
attachmentLimits: attachmentLimits,
|
||||
options: options,
|
||||
)
|
||||
// The data source needs to be set before the message body because it is needed to hydrate mentions.
|
||||
vc.approvalDataSource = approvalDataSource
|
||||
vc.setMessageBody(initialMessageBody, txProvider: DependenciesBridge.shared.db.readTxProvider)
|
||||
@ -771,7 +782,7 @@ public final class AttachmentApprovalViewController: UIPageViewController, UIPag
|
||||
}
|
||||
dataSource.sourceFilename = filename
|
||||
|
||||
return try PreviewableAttachment.videoAttachment(dataSource: dataSource, dataUTI: dataUTI)
|
||||
return try PreviewableAttachment.videoAttachment(dataSource: dataSource, dataUTI: dataUTI, attachmentLimits: attachmentLimits)
|
||||
}
|
||||
|
||||
func attachmentApprovalItem(before currentItem: AttachmentApprovalItem) -> AttachmentApprovalItem? {
|
||||
|
||||
@ -22,6 +22,7 @@ public class AttachmentMultisend {
|
||||
conversations: [ConversationItem],
|
||||
approvedMessageBody: MessageBody?,
|
||||
approvedAttachments: ApprovedAttachments,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
) async throws -> [EnqueueResult] {
|
||||
let destinations = try await prepareDestinations(
|
||||
forSendingMessageBody: approvedMessageBody,
|
||||
@ -40,7 +41,10 @@ public class AttachmentMultisend {
|
||||
}
|
||||
|
||||
let imageQuality = approvedAttachments.imageQuality
|
||||
let imageQualityLevel = ImageQualityLevel.resolvedValue(imageQuality: imageQuality)
|
||||
let imageQualityLevel = ImageQualityLevel.resolvedValue(
|
||||
imageQuality: imageQuality,
|
||||
standardQualityLevel: attachmentLimits.standardQualityLevel,
|
||||
)
|
||||
let sendableAttachments = try await approvedAttachments.attachments.mapAsync {
|
||||
return try await SendableAttachment.forPreviewableAttachment($0, imageQualityLevel: imageQualityLevel)
|
||||
}
|
||||
@ -50,6 +54,7 @@ public class AttachmentMultisend {
|
||||
sendableAttachments: sendableAttachments,
|
||||
hasNonStoryDestination: hasNonStoryDestination,
|
||||
hasStoryDestination: hasStoryDestination,
|
||||
attachmentLimits: attachmentLimits,
|
||||
)
|
||||
|
||||
return try await deps.databaseStorage.awaitableWrite { tx in
|
||||
@ -153,6 +158,7 @@ public class AttachmentMultisend {
|
||||
sendableAttachments: [SendableAttachment],
|
||||
hasNonStoryDestination: Bool,
|
||||
hasStoryDestination: Bool,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
) async throws -> [SegmentAttachmentResult] {
|
||||
let maxSegmentDurations = conversations.compactMap(\.videoAttachmentDurationLimit)
|
||||
guard hasStoryDestination, !maxSegmentDurations.isEmpty, let requiredSegmentDuration = maxSegmentDurations.min() else {
|
||||
@ -174,7 +180,10 @@ public class AttachmentMultisend {
|
||||
|
||||
var segmentedResults = [SegmentAttachmentResult]()
|
||||
for attachment in sendableAttachments {
|
||||
let segmentingResult = try await attachment.segmentedIfNecessary(segmentDuration: requiredSegmentDuration)
|
||||
let segmentingResult = try await attachment.segmentedIfNecessary(
|
||||
segmentDuration: requiredSegmentDuration,
|
||||
attachmentLimits: attachmentLimits,
|
||||
)
|
||||
|
||||
let originalDataSource: AttachmentDataSource?
|
||||
if hasNonStoryDestination || segmentingResult.segmented == nil {
|
||||
|
||||
@ -113,14 +113,18 @@ public struct PreviewableAttachment {
|
||||
}
|
||||
|
||||
// Factory method for video attachments.
|
||||
public static func videoAttachment(dataSource: DataSourcePath, dataUTI: String) throws -> Self {
|
||||
public static func videoAttachment(
|
||||
dataSource: DataSourcePath,
|
||||
dataUTI: String,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
) throws -> Self {
|
||||
try OWSMediaUtils.validateVideoExtension(ofPath: dataSource.fileUrl.path)
|
||||
try OWSMediaUtils.validateVideoAsset(atPath: dataSource.fileUrl.path)
|
||||
return try newAttachment(
|
||||
dataSource: dataSource,
|
||||
dataUTI: dataUTI,
|
||||
validUTISet: SignalAttachment.videoUTISet,
|
||||
maxFileSize: OutgoingAttachmentLimits.currentLimits().maxPlaintextVideoBytes,
|
||||
maxFileSize: attachmentLimits.maxPlaintextVideoBytes,
|
||||
)
|
||||
}
|
||||
|
||||
@ -131,16 +135,26 @@ public struct PreviewableAttachment {
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public static func compressVideoAsMp4(dataSource: DataSourcePath, sessionCallback: (@MainActor (AVAssetExportSession) -> Void)? = nil) async throws -> Self {
|
||||
public static func compressVideoAsMp4(
|
||||
dataSource: DataSourcePath,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
sessionCallback: (@MainActor (AVAssetExportSession) -> Void)? = nil,
|
||||
) async throws -> Self {
|
||||
return try await compressVideoAsMp4(
|
||||
asset: AVAsset(url: dataSource.fileUrl),
|
||||
baseFilename: dataSource.sourceFilename,
|
||||
attachmentLimits: attachmentLimits,
|
||||
sessionCallback: sessionCallback,
|
||||
)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public static func compressVideoAsMp4(asset: AVAsset, baseFilename: String?, sessionCallback: (@MainActor (AVAssetExportSession) -> Void)? = nil) async throws -> Self {
|
||||
public static func compressVideoAsMp4(
|
||||
asset: AVAsset,
|
||||
baseFilename: String?,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
sessionCallback: (@MainActor (AVAssetExportSession) -> Void)? = nil,
|
||||
) async throws -> Self {
|
||||
let startTime = MonotonicDate()
|
||||
|
||||
guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPreset640x480) else {
|
||||
@ -195,25 +209,33 @@ public struct PreviewableAttachment {
|
||||
let formattedDuration = OWSOperation.formattedNs((endTime - startTime).nanoseconds)
|
||||
Logger.info("transcoded video in \(formattedDuration)s")
|
||||
|
||||
return try videoAttachment(dataSource: dataSource, dataUTI: UTType.mpeg4Movie.identifier)
|
||||
return try videoAttachment(dataSource: dataSource, dataUTI: UTType.mpeg4Movie.identifier, attachmentLimits: attachmentLimits)
|
||||
}
|
||||
|
||||
// MARK: Audio Attachments
|
||||
|
||||
// Factory method for audio attachments.
|
||||
public static func audioAttachment(dataSource: DataSourcePath, dataUTI: String) throws(SignalAttachmentError) -> Self {
|
||||
public static func audioAttachment(
|
||||
dataSource: DataSourcePath,
|
||||
dataUTI: String,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
) throws(SignalAttachmentError) -> Self {
|
||||
return try newAttachment(
|
||||
dataSource: dataSource,
|
||||
dataUTI: dataUTI,
|
||||
validUTISet: SignalAttachment.audioUTISet,
|
||||
maxFileSize: OutgoingAttachmentLimits.currentLimits().maxPlaintextAudioBytes,
|
||||
maxFileSize: attachmentLimits.maxPlaintextAudioBytes,
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: Generic Attachments
|
||||
|
||||
// Factory method for generic attachments.
|
||||
public static func genericAttachment(dataSource: DataSourcePath, dataUTI: String) throws(SignalAttachmentError) -> Self {
|
||||
public static func genericAttachment(
|
||||
dataSource: DataSourcePath,
|
||||
dataUTI: String,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
) throws(SignalAttachmentError) -> Self {
|
||||
// [15M] TODO: Enforce this at compile-time rather than runtime.
|
||||
owsPrecondition(!SignalAttachment.videoUTISet.contains(dataUTI))
|
||||
owsPrecondition(!SignalAttachment.inputImageUTISet.contains(dataUTI))
|
||||
@ -221,14 +243,18 @@ public struct PreviewableAttachment {
|
||||
dataSource: dataSource,
|
||||
dataUTI: dataUTI,
|
||||
validUTISet: nil,
|
||||
maxFileSize: OutgoingAttachmentLimits.currentLimits().maxPlaintextBytes,
|
||||
maxFileSize: attachmentLimits.maxPlaintextBytes,
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: Voice Messages
|
||||
|
||||
public static func voiceMessageAttachment(dataSource: DataSourcePath, dataUTI: String) throws(SignalAttachmentError) -> Self {
|
||||
let attachment = try audioAttachment(dataSource: dataSource, dataUTI: dataUTI)
|
||||
public static func voiceMessageAttachment(
|
||||
dataSource: DataSourcePath,
|
||||
dataUTI: String,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
) throws(SignalAttachmentError) -> Self {
|
||||
let attachment = try audioAttachment(dataSource: dataSource, dataUTI: dataUTI, attachmentLimits: attachmentLimits)
|
||||
attachment.rawValue.isVoiceMessage = true
|
||||
return attachment
|
||||
}
|
||||
@ -260,6 +286,7 @@ public struct PreviewableAttachment {
|
||||
ofTypes types: AttachmentTypes = .all,
|
||||
dataSource: DataSourcePath,
|
||||
dataUTI: String,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
options: BuildOptions = [],
|
||||
) throws -> Self {
|
||||
if SignalAttachment.inputImageUTISet.contains(dataUTI) {
|
||||
@ -272,18 +299,18 @@ public struct PreviewableAttachment {
|
||||
guard types.contains(.video) else {
|
||||
throw SignalAttachmentError.invalidFileFormat
|
||||
}
|
||||
return try videoAttachment(dataSource: dataSource, dataUTI: dataUTI)
|
||||
return try videoAttachment(dataSource: dataSource, dataUTI: dataUTI, attachmentLimits: attachmentLimits)
|
||||
}
|
||||
if SignalAttachment.audioUTISet.contains(dataUTI) {
|
||||
guard types.contains(.audio) else {
|
||||
throw SignalAttachmentError.invalidFileFormat
|
||||
}
|
||||
return try audioAttachment(dataSource: dataSource, dataUTI: dataUTI)
|
||||
return try audioAttachment(dataSource: dataSource, dataUTI: dataUTI, attachmentLimits: attachmentLimits)
|
||||
}
|
||||
guard types.contains(.other) else {
|
||||
throw SignalAttachmentError.invalidFileFormat
|
||||
}
|
||||
return try genericAttachment(dataSource: dataSource, dataUTI: dataUTI)
|
||||
return try genericAttachment(dataSource: dataSource, dataUTI: dataUTI, attachmentLimits: attachmentLimits)
|
||||
}
|
||||
|
||||
// MARK: Helper Methods
|
||||
|
||||
@ -105,7 +105,10 @@ public struct SendableAttachment {
|
||||
/// If the attachment is a video longer than `storyVideoSegmentMaxDuration`,
|
||||
/// segments into separate attachments under that duration.
|
||||
/// Otherwise returns a result with only the original and nil segmented attachments.
|
||||
public func segmentedIfNecessary(segmentDuration: TimeInterval) async throws -> SegmentAttachmentResult {
|
||||
public func segmentedIfNecessary(
|
||||
segmentDuration: TimeInterval,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
) async throws -> SegmentAttachmentResult {
|
||||
guard SignalAttachment.videoUTISet.contains(self.dataUTI) else {
|
||||
return SegmentAttachmentResult(self, segmented: nil)
|
||||
}
|
||||
@ -132,7 +135,7 @@ public struct SendableAttachment {
|
||||
let segments = try segmentFileUrls.map { url in
|
||||
let dataSource = DataSourcePath(fileUrl: url, ownership: .owned)
|
||||
// [15M] TODO: This doesn't transfer all SignalAttachment fields.
|
||||
let attachment = try PreviewableAttachment.videoAttachment(dataSource: dataSource, dataUTI: self.dataUTI)
|
||||
let attachment = try PreviewableAttachment.videoAttachment(dataSource: dataSource, dataUTI: self.dataUTI, attachmentLimits: attachmentLimits)
|
||||
return Self(nonImagePreviewableAttachment: attachment)
|
||||
}
|
||||
return SegmentAttachmentResult(self, segmented: segments)
|
||||
|
||||
@ -149,8 +149,11 @@ public struct TypedItemProvider {
|
||||
/// to come earlier in the list than their fallbacks.
|
||||
private static let itemTypeOrder: [TypedItemProvider.ItemType] = [.movie, .image, .contact, .json, .plainText, .text, .pdf, .pkPass, .fileUrl, .webUrl, .data]
|
||||
|
||||
public static func buildVisualMediaAttachment(forItemProvider itemProvider: NSItemProvider) async throws -> PreviewableAttachment {
|
||||
let typedItem = try await make(for: itemProvider).buildAttachment()
|
||||
public static func buildVisualMediaAttachment(
|
||||
forItemProvider itemProvider: NSItemProvider,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
) async throws -> PreviewableAttachment {
|
||||
let typedItem = try await make(for: itemProvider).buildAttachment(attachmentLimits: attachmentLimits)
|
||||
switch typedItem {
|
||||
case .other(let attachment) where attachment.isVisualMedia:
|
||||
return attachment
|
||||
@ -178,7 +181,10 @@ public struct TypedItemProvider {
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
public nonisolated func buildAttachment(progress: Progress? = nil) async throws -> TypedItem {
|
||||
public nonisolated func buildAttachment(
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
progress: Progress? = nil,
|
||||
) async throws -> TypedItem {
|
||||
// Whenever this finishes, mark its progress as fully complete. This
|
||||
// handles item providers that can't provide partial progress updates.
|
||||
defer {
|
||||
@ -197,7 +203,7 @@ public struct TypedItemProvider {
|
||||
// 2) try to load a UIImage directly in the case that is what was sent over
|
||||
// 3) try to NSKeyedUnarchive NSData directly into a UIImage
|
||||
do {
|
||||
attachment = try await buildFileAttachment(mustBeVisualMedia: true, progress: progress)
|
||||
attachment = try await buildFileAttachment(mustBeVisualMedia: true, attachmentLimits: attachmentLimits, progress: progress)
|
||||
} catch SignalAttachmentError.couldNotParseImage, ItemProviderError.fileUrlWasBplist {
|
||||
Logger.warn("failed to parse image directly from file; checking for loading UIImage directly")
|
||||
let image: UIImage = try await loadObjectWithKeyedUnarchiverFallback(
|
||||
@ -207,9 +213,9 @@ public struct TypedItemProvider {
|
||||
attachment = try Self.createAttachment(withImage: image)
|
||||
}
|
||||
case .movie:
|
||||
attachment = try await self.buildFileAttachment(mustBeVisualMedia: true, progress: progress)
|
||||
attachment = try await self.buildFileAttachment(mustBeVisualMedia: true, attachmentLimits: attachmentLimits, progress: progress)
|
||||
case .pdf, .data:
|
||||
attachment = try await self.buildFileAttachment(mustBeVisualMedia: false, progress: progress)
|
||||
attachment = try await self.buildFileAttachment(mustBeVisualMedia: false, attachmentLimits: attachmentLimits, progress: progress)
|
||||
case .fileUrl, .json:
|
||||
let url: NSURL = try await loadObjectWithKeyedUnarchiverFallback(
|
||||
overrideTypeIdentifier: TypedItemProvider.ItemType.fileUrl.typeIdentifier,
|
||||
@ -224,6 +230,7 @@ public struct TypedItemProvider {
|
||||
dataSource: dataSource,
|
||||
dataUTI: dataUTI,
|
||||
mustBeVisualMedia: false,
|
||||
attachmentLimits: attachmentLimits,
|
||||
progress: progress,
|
||||
)
|
||||
case .webUrl:
|
||||
@ -231,7 +238,7 @@ public struct TypedItemProvider {
|
||||
cannotLoadError: .cannotLoadURLObject,
|
||||
failedLoadError: .loadURLObjectFailed,
|
||||
)
|
||||
return try Self.createAttachment(withText: (url as URL).absoluteString)
|
||||
return try Self.createAttachment(withText: (url as URL).absoluteString, attachmentLimits: attachmentLimits)
|
||||
case .contact:
|
||||
let contactData = try await loadDataRepresentation()
|
||||
return .contact(contactData)
|
||||
@ -240,7 +247,7 @@ public struct TypedItemProvider {
|
||||
cannotLoadError: .cannotLoadStringObject,
|
||||
failedLoadError: .loadStringObjectFailed,
|
||||
)
|
||||
return try Self.createAttachment(withText: text as String)
|
||||
return try Self.createAttachment(withText: text as String, attachmentLimits: attachmentLimits)
|
||||
case .pkPass:
|
||||
let pkPass = try await loadDataRepresentation()
|
||||
let fileExtension = MimeTypeUtil.fileExtensionForUtiType(itemType.typeIdentifier)
|
||||
@ -248,12 +255,16 @@ public struct TypedItemProvider {
|
||||
throw SignalAttachmentError.missingData
|
||||
}
|
||||
let dataSource = try DataSourcePath(writingTempFileData: pkPass, fileExtension: fileExtension)
|
||||
attachment = try PreviewableAttachment.genericAttachment(dataSource: dataSource, dataUTI: itemType.typeIdentifier)
|
||||
attachment = try PreviewableAttachment.genericAttachment(dataSource: dataSource, dataUTI: itemType.typeIdentifier, attachmentLimits: attachmentLimits)
|
||||
}
|
||||
return .other(attachment)
|
||||
}
|
||||
|
||||
private nonisolated func buildFileAttachment(mustBeVisualMedia: Bool, progress: Progress?) async throws -> PreviewableAttachment {
|
||||
private nonisolated func buildFileAttachment(
|
||||
mustBeVisualMedia: Bool,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
progress: Progress?,
|
||||
) async throws -> PreviewableAttachment {
|
||||
let (dataSource, dataUTI): (DataSourcePath, String) = try await withCheckedThrowingContinuation { continuation in
|
||||
let typeIdentifier = itemType.typeIdentifier
|
||||
_ = itemProvider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { fileUrl, error in
|
||||
@ -275,7 +286,13 @@ public struct TypedItemProvider {
|
||||
}
|
||||
}
|
||||
|
||||
return try await _buildFileAttachment(dataSource: dataSource, dataUTI: dataUTI, mustBeVisualMedia: mustBeVisualMedia, progress: progress)
|
||||
return try await _buildFileAttachment(
|
||||
dataSource: dataSource,
|
||||
dataUTI: dataUTI,
|
||||
mustBeVisualMedia: mustBeVisualMedia,
|
||||
attachmentLimits: attachmentLimits,
|
||||
progress: progress,
|
||||
)
|
||||
}
|
||||
|
||||
private nonisolated func loadDataRepresentation(
|
||||
@ -337,7 +354,10 @@ public struct TypedItemProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated static func createAttachment(withText text: String) throws -> TypedItem {
|
||||
private nonisolated static func createAttachment(
|
||||
withText text: String,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
) throws -> TypedItem {
|
||||
let filteredText = FilteredString(rawValue: text)
|
||||
if let messageText = TypedItem.MessageText(filteredValue: filteredText) {
|
||||
return .text(messageText)
|
||||
@ -351,6 +371,7 @@ public struct TypedItemProvider {
|
||||
return .other(try PreviewableAttachment.genericAttachment(
|
||||
dataSource: dataSource,
|
||||
dataUTI: UTType.plainText.identifier,
|
||||
attachmentLimits: attachmentLimits,
|
||||
))
|
||||
}
|
||||
}
|
||||
@ -383,6 +404,7 @@ public struct TypedItemProvider {
|
||||
dataSource: DataSourcePath,
|
||||
dataUTI: String,
|
||||
mustBeVisualMedia: Bool,
|
||||
attachmentLimits: OutgoingAttachmentLimits,
|
||||
progress: Progress?,
|
||||
) async throws -> PreviewableAttachment {
|
||||
if SignalAttachment.videoUTISet.contains(dataUTI) {
|
||||
@ -393,6 +415,7 @@ public struct TypedItemProvider {
|
||||
}
|
||||
return try await PreviewableAttachment.compressVideoAsMp4(
|
||||
dataSource: dataSource,
|
||||
attachmentLimits: attachmentLimits,
|
||||
sessionCallback: { exportSession in
|
||||
guard let progress else { return }
|
||||
progressPoller = ProgressPoller(progress: progress, pollInterval: 0.1, fractionCompleted: { return exportSession.progress })
|
||||
@ -404,7 +427,7 @@ public struct TypedItemProvider {
|
||||
// an image or throw an error.
|
||||
return try PreviewableAttachment.imageAttachment(dataSource: dataSource, dataUTI: dataUTI)
|
||||
} else {
|
||||
return try PreviewableAttachment.buildAttachment(dataSource: dataSource, dataUTI: dataUTI)
|
||||
return try PreviewableAttachment.buildAttachment(dataSource: dataSource, dataUTI: dataUTI, attachmentLimits: attachmentLimits)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user