Remove SignalAttachment.isConvertible… properties

This commit is contained in:
Max Radermacher
2025-11-24 13:52:39 -06:00
committed by GitHub
parent 360556d318
commit f686c56883
12 changed files with 184 additions and 162 deletions

View File

@@ -171,8 +171,7 @@ extension UsernameLinkScanQRCodeViewController: PHPickerViewControllerDelegate {
}()
do {
let typedItemProvider = try TypedItemProvider.make(for: selectedItem.itemProvider)
let attachment = try await typedItemProvider.buildAttachment()
let attachment = try await TypedItemProvider.buildVisualMediaAttachment(forItemProvider: selectedItem.itemProvider)
guard
let image = attachment.image(),
let ciImage = CIImage(image: image)

View File

@@ -401,13 +401,8 @@ extension SendMediaNavigationController: PHPickerViewControllerDelegate {
let attachment = PHPickerAttachment(
result: result,
attachmentApprovalItemPromise: Promise.wrapAsync {
let attachment = try await TypedItemProvider
.make(for: result.itemProvider)
.buildAttachment()
return AttachmentApprovalItem(
attachment: attachment,
canSave: false
)
let attachment = try await TypedItemProvider.buildVisualMediaAttachment(forItemProvider: result.itemProvider)
return AttachmentApprovalItem(attachment: attachment, canSave: false)
}
)
return .phPicker(attachment: attachment)
@@ -441,13 +436,8 @@ extension SendMediaNavigationController: PHPickerViewControllerDelegate {
PHPickerAttachment(
result: result,
attachmentApprovalItemPromise: Promise.wrapAsync {
let attachment = try await TypedItemProvider
.make(for: result.itemProvider)
.buildAttachment()
return AttachmentApprovalItem(
attachment: attachment,
canSave: false
)
let attachment = try await TypedItemProvider.buildVisualMediaAttachment(forItemProvider: result.itemProvider)
return AttachmentApprovalItem(attachment: attachment, canSave: false)
}
)
}.forEach { attachment in

View File

@@ -29,8 +29,8 @@ struct SendMessageUnapprovedContent {
struct SendMessageApprovedContent {
let messageBody: MessageBody
let linkPreviewDraft: OWSLinkPreviewDraft?
init?(messageBody: MessageBody?, linkPreviewDraft: OWSLinkPreviewDraft?) {
guard let messageBody, !messageBody.text.isEmpty else {
init?(messageBody: MessageBody, linkPreviewDraft: OWSLinkPreviewDraft?) {
guard !messageBody.text.isEmpty else {
return nil
}
self.messageBody = messageBody
@@ -262,7 +262,7 @@ extension SendMessageFlow: ConversationPickerDelegate {
// MARK: -
extension SendMessageFlow: TextApprovalViewControllerDelegate {
func textApproval(_ textApproval: TextApprovalViewController, didApproveMessage messageBody: MessageBody?, linkPreviewDraft: OWSLinkPreviewDraft?) {
func textApproval(_ textApproval: TextApprovalViewController, didApproveMessage messageBody: MessageBody, linkPreviewDraft: OWSLinkPreviewDraft?) {
guard let approvedContent = SendMessageApprovedContent(messageBody: messageBody, linkPreviewDraft: linkPreviewDraft) else {
owsFailDebug("Missing messageBody.")
fireCancelled()

View File

@@ -71,12 +71,6 @@ public class SignalAttachment: NSObject {
public var captionText: String?
// This flag should be set for text attachments that can be sent as text messages.
public var isConvertibleToTextMessage = false
// This flag should be set for attachments that can be sent as contact shares.
public var isConvertibleToContactShare = false
// Attachment types are identified using UTIs.
//
// See: https://developer.apple.com/library/content/documentation/Miscellaneous/Reference/UTIRef/Articles/System-DeclaredUniformTypeIdentifiers.html
@@ -147,8 +141,6 @@ public class SignalAttachment: NSObject {
private func replacingDataSource(with newDataSource: DataSource, dataUTI: String? = nil) -> SignalAttachment {
let result = SignalAttachment(dataSource: newDataSource, dataUTI: dataUTI ?? self.dataUTI)
result.captionText = captionText
result.isConvertibleToTextMessage = isConvertibleToTextMessage
result.isConvertibleToContactShare = isConvertibleToContactShare
result.isVoiceMessage = isVoiceMessage
result.isBorderless = isBorderless
result.isLoopingVideo = isLoopingVideo

View File

@@ -65,11 +65,6 @@ public class DataSourceValue: DataSource {
self.init(data, fileExtension: fileExtension)
}
public convenience init(oversizeText: String) {
let data = Data(oversizeText.filterForDisplay.utf8)
self.init(data, fileExtension: MimeTypeUtil.oversizeTextAttachmentFileExtension)
}
deinit {
if let _dataUrl {
try? OWSFileSystem.deleteFileIfExists(url: _dataUrl)

View File

@@ -16,6 +16,15 @@ extension NSString {
// MARK: -
public struct FilteredString {
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue.filterStringForDisplay()
}
}
// MARK: -
public extension String {
var stripped: String {
ows_stripped()

View File

@@ -185,7 +185,7 @@ public class ShareViewController: OWSNavigationController, ShareViewDelegate, SA
loadViewControllerForProgress = initialLoadViewController
}
let attachments: [SignalAttachment]
let typedItems: [TypedItem]
do {
// If buildAndValidateAttachments takes longer than 200ms, we want to show
// the new load view. If it takes less than 200ms, we'll exit out of this
@@ -201,7 +201,7 @@ public class ShareViewController: OWSNavigationController, ShareViewDelegate, SA
try Task.checkCancellation()
self.setViewControllers([loadViewControllerToDisplay], animated: false)
}()
attachments = try await buildAndValidateAttachments(
typedItems = try await buildAndValidateAttachments(
for: typedItemProviders,
setProgress: { loadViewControllerForProgress?.progress = $0 }
)
@@ -210,8 +210,8 @@ public class ShareViewController: OWSNavigationController, ShareViewDelegate, SA
return
}
Logger.info("Setting picker attachments: \(attachments)")
conversationPicker.attachments = attachments
Logger.info("Setting picker attachments: \(typedItems.count)")
conversationPicker.typedItems = typedItems
if let preSelectedThread {
let approvalViewController = try conversationPicker.buildApprovalViewController(for: preSelectedThread)
@@ -377,7 +377,7 @@ public class ShareViewController: OWSNavigationController, ShareViewDelegate, SA
private func buildAndValidateAttachments(
for typedItemProviders: [TypedItemProvider],
setProgress: @MainActor (Progress) -> Void
) async throws -> [SignalAttachment] {
) async throws -> [TypedItem] {
let progress = Progress(totalUnitCount: Int64(typedItemProviders.count))
let itemsAndProgresses = typedItemProviders.map {
@@ -388,15 +388,18 @@ public class ShareViewController: OWSNavigationController, ShareViewDelegate, SA
setProgress(progress)
let attachments = try await self.buildAttachments(for: itemsAndProgresses)
let typedItems = try await self.buildAttachments(
for: itemsAndProgresses,
mustBeVisualMedia: itemsAndProgresses.count >= 2,
)
try Task.checkCancellation()
// Make sure the user is not trying to share more than our attachment limit.
guard attachments.filter({ !$0.isConvertibleToTextMessage }).count <= SignalAttachment.maxAttachmentsAllowed else {
guard typedItems.count <= SignalAttachment.maxAttachmentsAllowed else {
throw ShareViewControllerError.tooManyAttachments
}
return attachments
return typedItems
}
private func presentAttachmentError(_ error: any Error) {
@@ -453,11 +456,14 @@ public class ShareViewController: OWSNavigationController, ShareViewDelegate, SA
throw ShareViewControllerError.noConformingInputItem
}
nonisolated private func buildAttachments(for itemsAndProgresses: [(TypedItemProvider, Progress)]) async throws -> [SignalAttachment] {
private nonisolated func buildAttachments(
for itemsAndProgresses: [(TypedItemProvider, Progress)],
mustBeVisualMedia: Bool,
) 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: [SignalAttachment] = []
var result: [TypedItem] = []
for (typedItemProvider, progress) in itemsAndProgresses {
result.append(try await typedItemProvider.buildAttachment(progress: progress))
result.append(try await typedItemProvider.buildAttachment(mustBeVisualMedia: mustBeVisualMedia, progress: progress))
}
return result
}

View File

@@ -26,29 +26,20 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
/// actually sending if stories are selected.
public let areAttachmentStoriesCompatPrecheck: Bool
var attachments: [SignalAttachment]? {
var typedItems: [TypedItem] {
didSet {
owsPrecondition(typedItems.count <= 1 || typedItems.allSatisfy(\.isVisualMedia))
updateStoriesState()
updateApprovalMode()
}
}
private var isTextMessage: Bool {
guard let attachments = attachments, attachments.count == 1, let attachment = attachments.first else { return false }
// TODO: it may be convertible to an oversize text message, check that
return attachment.isConvertibleToTextMessage && attachment.dataSource.dataLength <= OWSMediaUtils.kOversizeTextMessageSizeThresholdBytes
}
private var isContactShare: Bool {
guard let attachments = attachments, attachments.count == 1, let attachment = attachments.first else { return false }
return attachment.isConvertibleToContactShare
}
private var mentionCandidates: [Aci] = []
private var selectedConversations: [ConversationItem] { selection.conversations }
public init(areAttachmentStoriesCompatPrecheck: Bool, shareViewDelegate: ShareViewDelegate) {
self.typedItems = []
self.areAttachmentStoriesCompatPrecheck = areAttachmentStoriesCompatPrecheck
self.shareViewDelegate = shareViewDelegate
@@ -93,21 +84,18 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
}
private func updateStoriesState() {
if areAttachmentStoriesCompatPrecheck == true {
sectionOptions.insert(.stories)
} else if let attachments = attachments, attachments.allSatisfy({ $0.dataSource.isValidImage || $0.dataSource.isValidVideo }) {
sectionOptions.insert(.stories)
} else if isTextMessage {
if areAttachmentStoriesCompatPrecheck || canSendTypedItemsToStory() {
sectionOptions.insert(.stories)
} else {
sectionOptions.remove(.stories)
}
}
}
// MARK: - Approval
private func canSendTypedItemsToStory() -> Bool {
return !typedItems.isEmpty && typedItems.allSatisfy(\.isStoriesCompatible)
}
extension SharingThreadPickerViewController {
// MARK: - Approval
func approve() {
do {
@@ -129,23 +117,22 @@ extension SharingThreadPickerViewController {
}
func buildApprovalViewController(withCancelButton: Bool) throws -> UIViewController {
guard let attachments = attachments, let firstAttachment = attachments.first else {
guard let anyItem = typedItems.first else {
throw OWSAssertionError("Unexpectedly missing attachments")
}
let approvalVC: UIViewController
if isTextMessage {
guard let messageText = String(data: firstAttachment.dataSource.data, encoding: .utf8)?.filterForDisplay else {
throw OWSAssertionError("Missing or invalid message text for text attachment")
}
let approvalView = TextApprovalViewController(messageBody: MessageBody(text: messageText, ranges: .empty))
switch anyItem {
case .text(let inlineMessageText):
let approvalView = TextApprovalViewController(
messageBody: MessageBody(text: inlineMessageText.filteredValue.rawValue, ranges: .empty),
)
approvalVC = approvalView
approvalView.delegate = self
} else if isContactShare {
let cnContact = try SystemContact.parseVCardData(firstAttachment.dataSource.data)
case .contact(let contactData):
let cnContact = try SystemContact.parseVCardData(contactData)
let contactShareDraft = SSKEnvironment.shared.databaseStorageRef.read { tx in
return ContactShareDraft.load(
cnContact: cnContact,
@@ -158,13 +145,22 @@ extension SharingThreadPickerViewController {
tx: tx
)
}
let approvalView = ContactShareViewController(contactShareDraft: contactShareDraft)
approvalVC = approvalView
approvalView.shareDelegate = self
} else {
let approvalItems = attachments.map { AttachmentApprovalItem(attachment: $0, canSave: false) }
case .other:
// We know that the first element of typedItems isn't .text or .contact
// (see prior cases); the others must be visual media (see the precondition
// on `typedItems`), so they also can't be .text or .contact.
let approvalItems = typedItems.map {
switch $0 {
case .text, .contact:
owsFail("not possible")
case .other(let attachment):
return AttachmentApprovalItem(attachment: attachment, canSave: false)
}
}
var approvalVCOptions: AttachmentApprovalViewControllerOptions = withCancelButton ? [ .hasCancel ] : []
if self.selection.conversations.contains(where: \.isStory) {
approvalVCOptions.insert(.disallowViewOnce)
@@ -177,18 +173,16 @@ extension SharingThreadPickerViewController {
return approvalVC
}
}
// MARK: - Sending
// MARK: - Sending
extension SharingThreadPickerViewController {
private enum ApprovedSend {
case text(messageBody: MessageBody, linkPreview: OWSLinkPreviewDraft?)
case contact(contactShare: ContactShareDraft)
case other(attachments: ApprovedAttachments, messageBody: MessageBody?)
}
fileprivate func send(
approvedAttachments: ApprovedAttachments = .empty(),
approvedContactShare: ContactShareDraft? = nil,
approvedLinkPreview: OWSLinkPreviewDraft? = nil,
approvedMessageBody: MessageBody? = nil,
) {
private func send(_ approvedSend: ApprovedSend) {
// Start presenting empty; the attachments will get set later.
self.presentOrUpdateSendProgressSheet(attachmentIds: [])
@@ -197,12 +191,7 @@ extension SharingThreadPickerViewController {
Task {
switch await tryToSend(
selectedConversations: selectedConversations,
isTextMessage: isTextMessage,
isContactShare: isContactShare,
messageBody: approvedMessageBody,
attachments: approvedAttachments,
linkPreviewDraft: approvedLinkPreview,
contactShareDraft: approvedContactShare
approvedSend: approvedSend,
) {
case .success:
self.dismissSendProgressSheet {}
@@ -220,23 +209,18 @@ extension SharingThreadPickerViewController {
private nonisolated func tryToSend(
selectedConversations: [ConversationItem],
isTextMessage: Bool,
isContactShare: Bool,
messageBody: MessageBody?,
attachments: ApprovedAttachments?,
linkPreviewDraft: OWSLinkPreviewDraft?,
contactShareDraft: ContactShareDraft?
approvedSend: ApprovedSend,
) async -> Result<Void, SendError> {
if isTextMessage {
guard let messageBody, !messageBody.text.isEmpty else {
switch approvedSend {
case .text(let messageBody, let linkPreview):
guard !messageBody.text.isEmpty else {
return .failure(.init(outgoingMessages: [], error: OWSAssertionError("Missing body.")))
}
let linkPreviewDataSource: LinkPreviewDataSource?
if let linkPreviewDraft {
linkPreviewDataSource = try? await DependenciesBridge.shared.linkPreviewManager.buildDataSource(
from: linkPreviewDraft
)
if let linkPreview {
let linkPreviewManager = DependenciesBridge.shared.linkPreviewManager
linkPreviewDataSource = try? await linkPreviewManager.buildDataSource(from: linkPreview)
} else {
linkPreviewDataSource = nil
}
@@ -259,20 +243,16 @@ extension SharingThreadPickerViewController {
// as a text story with default styling.
StorySharing.sendTextStory(
with: messageBody,
linkPreviewDraft: linkPreviewDraft,
linkPreviewDraft: linkPreview,
to: storyConversations
)
}
)
} else if isContactShare {
guard let contactShareDraft else {
return .failure(.init(outgoingMessages: [], error: OWSAssertionError("Missing contactShare.")))
}
case .contact(let contactShare):
let contactShareForSending: ContactShareDraft.ForSending
do {
contactShareForSending = try await DependenciesBridge.shared.contactShareManager.validateAndPrepare(
draft: contactShareDraft
)
let contactShareManager = DependenciesBridge.shared.contactShareManager
contactShareForSending = try await contactShareManager.validateAndPrepare(draft: contactShare)
} catch {
return .failure(.init(outgoingMessages: [], error: error))
}
@@ -299,11 +279,7 @@ extension SharingThreadPickerViewController {
// We don't send contact shares to stories
storySendBlock: nil
)
} else {
guard let attachments else {
return .failure(.init(outgoingMessages: [], error: OWSAssertionError("Missing approvedAttachments.")))
}
case .other(let attachments, let messageBody):
// This method will also add threads to the profile whitelist.
let sendResult = AttachmentMultisend.sendApprovedMedia(
conversations: selectedConversations,
@@ -580,18 +556,13 @@ extension SharingThreadPickerViewController: ConversationPickerDelegate {
func conversationPickerDidCompleteSelection(_ conversationPickerViewController: ConversationPickerViewController) {
// Check if the attachments are compatible with sending to stories.
let storySelections = selection.conversations.compactMap({ $0 as? StoryConversationItem })
if !storySelections.isEmpty, let attachments = attachments {
let areImagesOrVideos = attachments.allSatisfy({ $0.dataSource.isValidImage || $0.dataSource.isValidVideo })
let isTextMessage = attachments.count == 1 && attachments.first.map {
$0.isConvertibleToTextMessage && $0.dataSource.dataLength <= OWSMediaUtils.kOversizeTextMessageSizeThresholdBytes
} ?? false
if !areImagesOrVideos && !isTextMessage {
if !storySelections.isEmpty {
if !self.canSendTypedItemsToStory() {
// Can't send to stories!
storySelections.forEach { self.selection.remove($0) }
self.updateUIForCurrentSelection(animated: false)
self.tableView.reloadData()
let vc = ConversationPickerFailedRecipientsSheet(
failedAttachments: attachments,
failedStoryConversationItems: storySelections,
remainingConversationItems: self.selection.conversations,
onApprove: { [weak self] in
@@ -620,7 +591,7 @@ extension SharingThreadPickerViewController: ConversationPickerDelegate {
}
func approvalMode(_ conversationPickerViewController: ConversationPickerViewController) -> ApprovalMode {
return attachments?.isEmpty != false ? .loading : .next
return typedItems.isEmpty ? .loading : .next
}
func conversationPickerDidBeginEditingText() {}
@@ -631,9 +602,9 @@ extension SharingThreadPickerViewController: ConversationPickerDelegate {
// MARK: -
extension SharingThreadPickerViewController: TextApprovalViewControllerDelegate {
func textApproval(_ textApproval: TextApprovalViewController, didApproveMessage messageBody: MessageBody?, linkPreviewDraft: OWSLinkPreviewDraft?) {
assert(messageBody?.text.nilIfEmpty != nil)
send(approvedLinkPreview: linkPreviewDraft, approvedMessageBody: messageBody)
func textApproval(_ textApproval: TextApprovalViewController, didApproveMessage messageBody: MessageBody, linkPreviewDraft: OWSLinkPreviewDraft?) {
assert(messageBody.text.nilIfEmpty != nil)
send(.text(messageBody: messageBody, linkPreview: linkPreviewDraft))
}
func textApprovalDidCancel(_ textApproval: TextApprovalViewController) {
@@ -662,7 +633,7 @@ extension SharingThreadPickerViewController: TextApprovalViewControllerDelegate
extension SharingThreadPickerViewController: ContactShareViewControllerDelegate {
func contactShareViewController(_ viewController: ContactShareViewController, didApproveContactShare contactShare: ContactShareDraft) {
send(approvedContactShare: contactShare)
send(.contact(contactShare: contactShare))
}
func contactShareViewControllerDidCancel(_ viewController: ContactShareViewController) {
@@ -707,7 +678,7 @@ extension SharingThreadPickerViewController: AttachmentApprovalViewControllerDel
didApproveAttachments approvedAttachments: ApprovedAttachments,
messageBody: MessageBody?,
) {
send(approvedAttachments: approvedAttachments, approvedMessageBody: messageBody)
send(.other(attachments: approvedAttachments, messageBody: messageBody))
}
func attachmentApprovalDidCancel() {

View File

@@ -158,9 +158,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC
self.receivedOptions = options
let pageOptions: [UIPageViewController.OptionsKey: Any] = [.interPageSpacing: kSpacingBetweenItems]
super.init(transitionStyle: .scroll,
navigationOrientation: .horizontal,
options: pageOptions)
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: pageOptions)
let isAddMoreVisibleBlock = { [weak self] in
return self?.isAddMoreVisible ?? false

View File

@@ -23,6 +23,41 @@ private enum ItemProviderError: Error {
case fileUrlWasBplist
}
// MARK: - TypedItem
public enum TypedItem {
case text(InlineMessageText)
case contact(Data)
case other(SignalAttachment)
public struct InlineMessageText {
public let filteredValue: FilteredString
public init?(filteredValue: FilteredString) {
guard filteredValue.rawValue.utf8.count <= OWSMediaUtils.kOversizeTextMessageSizeThresholdBytes else {
return nil
}
self.filteredValue = filteredValue
}
}
public var isVisualMedia: Bool {
switch self {
case .text, .contact: false
case .other(let attachment): attachment.isImage || attachment.isVideo
}
}
public var isStoriesCompatible: Bool {
switch self {
case .text: return true
case .contact: return false
case .other(let attachment):
// TODO: Consolidate with isVisualMedia after fixing validity checks.
return attachment.dataSource.isValidImage || attachment.dataSource.isValidVideo
}
}
}
// MARK: - TypedItemProvider
public struct TypedItemProvider {
@@ -117,6 +152,16 @@ 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 -> SignalAttachment {
let typedItem = try await make(for: itemProvider).buildAttachment(mustBeVisualMedia: true)
switch typedItem {
case .text, .contact:
owsFail("not possible because mustBeVisualMedia is true")
case .other(let attachment):
return attachment
}
}
public static func make(for itemProvider: NSItemProvider) throws -> TypedItemProvider {
for typeIdentifier in forcedDataTypeIdentifiers {
if itemProvider.hasItemConformingToTypeIdentifier(typeIdentifier) {
@@ -136,7 +181,7 @@ public struct TypedItemProvider {
// MARK: Methods
public nonisolated func buildAttachment(progress: Progress? = nil) async throws -> SignalAttachment {
public nonisolated func buildAttachment(mustBeVisualMedia: Bool, 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 {
@@ -145,6 +190,7 @@ public struct TypedItemProvider {
}
}
let attachment: SignalAttachment
switch itemType {
case .image:
// some apps send a usable file to us and some throw a UIImage at us, the UIImage can come in either directly
@@ -154,67 +200,72 @@ 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 {
return try await buildFileAttachment(progress: progress)
attachment = try await buildFileAttachment(mustBeVisualMedia: mustBeVisualMedia, 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(
cannotLoadError: .cannotLoadUIImageObject,
failedLoadError: .loadUIImageObjectFailed
)
return try Self.createAttachment(withImage: image)
attachment = try Self.createAttachment(withImage: image)
}
case .movie, .pdf, .data:
return try await self.buildFileAttachment(progress: progress)
attachment = try await self.buildFileAttachment(mustBeVisualMedia: mustBeVisualMedia, progress: progress)
case .fileUrl, .json:
let url: NSURL = try await loadObjectWithKeyedUnarchiverFallback(
overrideTypeIdentifier: TypedItemProvider.ItemType.fileUrl.typeIdentifier,
cannotLoadError: .cannotLoadURLObject,
failedLoadError: .loadURLObjectFailed
)
let (dataSource, dataUTI) = try Self.copyFileUrl(
fileUrl: url as URL,
defaultTypeIdentifier: UTType.data.identifier
)
return try await _buildFileAttachment(
attachment = try await _buildFileAttachment(
dataSource: dataSource,
dataUTI: dataUTI,
progress: progress
mustBeVisualMedia: mustBeVisualMedia,
progress: progress,
)
case .webUrl:
if mustBeVisualMedia {
throw SignalAttachmentError.invalidFileFormat
}
let url: NSURL = try await loadObjectWithKeyedUnarchiverFallback(
cannotLoadError: .cannotLoadURLObject,
failedLoadError: .loadURLObjectFailed
)
return try Self.createAttachment(withText: (url as URL).absoluteString)
case .contact:
let contactData = try await loadDataRepresentation()
let dataSource = DataSourceValue(contactData, utiType: itemType.typeIdentifier)
guard let dataSource else {
throw SignalAttachmentError.missingData
if mustBeVisualMedia {
throw SignalAttachmentError.invalidFileFormat
}
let attachment = try SignalAttachment.genericAttachment(dataSource: dataSource, dataUTI: itemType.typeIdentifier)
attachment.isConvertibleToContactShare = true
return attachment
let contactData = try await loadDataRepresentation()
return .contact(contactData)
case .plainText, .text:
if mustBeVisualMedia {
throw SignalAttachmentError.invalidFileFormat
}
let text: NSString = try await loadObjectWithKeyedUnarchiverFallback(
cannotLoadError: .cannotLoadStringObject,
failedLoadError: .loadStringObjectFailed
)
return try Self.createAttachment(withText: text as String)
case .pkPass:
if mustBeVisualMedia {
throw SignalAttachmentError.invalidFileFormat
}
let pkPass = try await loadDataRepresentation()
let dataSource = DataSourceValue(pkPass, utiType: itemType.typeIdentifier)
guard let dataSource else {
throw SignalAttachmentError.missingData
}
let attachment = try SignalAttachment.genericAttachment(dataSource: dataSource, dataUTI: itemType.typeIdentifier)
return attachment
attachment = try SignalAttachment.genericAttachment(dataSource: dataSource, dataUTI: itemType.typeIdentifier)
}
return .other(attachment)
}
private nonisolated func buildFileAttachment(progress: Progress?) async throws -> SignalAttachment {
private nonisolated func buildFileAttachment(mustBeVisualMedia: Bool, progress: Progress?) async throws -> SignalAttachment {
let (dataSource, dataUTI): (DataSourcePath, String) = try await withCheckedThrowingContinuation { continuation in
let typeIdentifier = itemType.typeIdentifier
_ = itemProvider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { fileUrl, error in
@@ -236,7 +287,7 @@ public struct TypedItemProvider {
}
}
return try await _buildFileAttachment(dataSource: dataSource, dataUTI: dataUTI, progress: progress)
return try await _buildFileAttachment(dataSource: dataSource, dataUTI: dataUTI, mustBeVisualMedia: mustBeVisualMedia, progress: progress)
}
private nonisolated func loadDataRepresentation(
@@ -298,11 +349,21 @@ public struct TypedItemProvider {
}
}
private nonisolated static func createAttachment(withText text: String) throws -> SignalAttachment {
let dataSource = DataSourceValue(oversizeText: text)
let attachment = try SignalAttachment.genericAttachment(dataSource: dataSource, dataUTI: UTType.text.identifier)
attachment.isConvertibleToTextMessage = true
return attachment
private nonisolated static func createAttachment(withText text: String) throws -> TypedItem {
let filteredText = FilteredString(rawValue: text)
if let inlineMessageText = TypedItem.InlineMessageText(filteredValue: filteredText) {
return .text(inlineMessageText)
} else {
// If this is too large to send as an inline message, fall back to treating
// it as a generic attachment that happens to contain text.
return .other(try SignalAttachment.genericAttachment(
dataSource: DataSourceValue(
Data(filteredText.rawValue.utf8),
fileExtension: MimeTypeUtil.oversizeTextAttachmentFileExtension,
),
dataUTI: UTType.text.identifier,
))
}
}
private nonisolated static func createAttachment(withImage image: UIImage) throws -> SignalAttachment {
@@ -339,6 +400,7 @@ public struct TypedItemProvider {
private nonisolated func _buildFileAttachment(
dataSource: DataSourcePath,
dataUTI: String,
mustBeVisualMedia: Bool,
progress: Progress?
) async throws -> SignalAttachment {
if SignalAttachment.videoUTISet.contains(dataUTI) {
@@ -355,6 +417,10 @@ public struct TypedItemProvider {
progressPoller?.startPolling()
}
)
} else if mustBeVisualMedia {
// If it's not a video but must be visual media, then we must parse it as
// an image or throw an error.
return try SignalAttachment.imageAttachment(dataSource: dataSource, dataUTI: dataUTI)
} else {
return try SignalAttachment.attachment(dataSource: dataSource, dataUTI: dataUTI)
}

View File

@@ -4,24 +4,20 @@
//
import Foundation
public import SignalServiceKit
import SignalServiceKit
public class ConversationPickerFailedRecipientsSheet: OWSTableSheetViewController {
let failedAttachments: [SignalAttachment]
let failedStoryConversationItems: [StoryConversationItem]
let remainingConversationItems: [ConversationItem]
let onApprove: () -> Void
public init(
failedAttachments: [SignalAttachment],
failedStoryConversationItems: [StoryConversationItem],
remainingConversationItems: [ConversationItem],
onApprove: @escaping () -> Void
) {
assert(failedAttachments.isEmpty.negated)
assert(failedStoryConversationItems.isEmpty.negated)
self.failedAttachments = failedAttachments
assert(!failedStoryConversationItems.isEmpty)
self.failedStoryConversationItems = failedStoryConversationItems
self.remainingConversationItems = remainingConversationItems
self.onApprove = onApprove

View File

@@ -8,7 +8,7 @@ public import SignalServiceKit
public protocol TextApprovalViewControllerDelegate: AnyObject {
func textApproval(_ textApproval: TextApprovalViewController, didApproveMessage messageBody: MessageBody?, linkPreviewDraft: OWSLinkPreviewDraft?)
func textApproval(_ textApproval: TextApprovalViewController, didApproveMessage messageBody: MessageBody, linkPreviewDraft: OWSLinkPreviewDraft?)
func textApprovalDidCancel(_ textApproval: TextApprovalViewController)