mirror of
https://github.com/signalapp/Signal-iOS.git
synced 2025-12-05 01:10:41 +00:00
Remove SignalAttachment.isConvertible… properties
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user