Make attachment limits hot-swappable

This commit is contained in:
Max Radermacher 2026-01-22 13:46:01 -06:00 committed by GitHub
parent 27b9514a70
commit 21d4d8f038
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 371 additions and 134 deletions

View File

@ -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? {

View File

@ -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

View File

@ -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() {

View File

@ -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)
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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,
)
}
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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,
)
}

View File

@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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,
)

View File

@ -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:

View File

@ -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)
})

View File

@ -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,
)
}
}

View File

@ -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

View File

@ -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.

View File

@ -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
}

View File

@ -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)

View File

@ -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? {

View File

@ -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 {

View File

@ -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

View File

@ -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)

View File

@ -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)
}
}
}