start simplifying share view controller

This commit is contained in:
Ehren Kret 2024-04-03 12:03:10 -05:00
parent a23571ded3
commit 33c5d976a1
5 changed files with 104 additions and 157 deletions

View File

@ -63,25 +63,6 @@ extension NSItemProvider {
}
}
@MainActor
func loadAttributedText(forTypeIdentifier typeIdentifier: String) async throws -> NSAttributedString {
try await withCheckedThrowingContinuation { continuation in
self.ows_loadAttributedText(forTypeIdentifier: typeIdentifier, options: nil) { attributedText, error in
if let error = error {
continuation.resume(throwing: error)
return
}
guard let attributedText else {
continuation.resume(throwing: OWSAssertionError("attributed text was unexpectedly nil"))
return
}
continuation.resume(returning: attributedText)
}
}
}
@MainActor
func loadImage(forTypeIdentifier typeIdentifier: String) async throws -> UIImage {
try await withCheckedThrowingContinuation { continuation in

View File

@ -29,11 +29,6 @@ NS_ASSUME_NONNULL_BEGIN
options:(nullable NSDictionary *)options
completionHandler:(void (^_Nullable)(NSString *_Nullable, NSError *_Nullable))completionHandler;
- (void)ows_loadAttributedTextForTypeIdentifier:(NSString *)typeIdentifier
options:(nullable NSDictionary *)options
completionHandler:(void (^_Nullable)(NSAttributedString *_Nullable,
NSError *_Nullable))completionHandler;
- (void)ows_loadImageForTypeIdentifier:(NSString *)typeIdentifier
options:(nullable NSDictionary *)options
completionHandler:(void (^_Nullable)(UIImage *_Nullable, NSError *_Nullable))completionHandler;

View File

@ -30,14 +30,6 @@ NS_ASSUME_NONNULL_BEGIN
[self loadItemForTypeIdentifier:typeIdentifier options:options completionHandler:completionHandler];
}
- (void)ows_loadAttributedTextForTypeIdentifier:(NSString *)typeIdentifier
options:(nullable NSDictionary *)options
completionHandler:(void (^_Nullable)(
NSAttributedString *_Nullable, NSError *_Nullable))completionHandler
{
[self loadItemForTypeIdentifier:typeIdentifier options:options completionHandler:completionHandler];
}
- (void)ows_loadImageForTypeIdentifier:(NSString *)typeIdentifier
options:(nullable NSDictionary *)options
completionHandler:(void (^_Nullable)(UIImage *_Nullable, NSError *_Nullable))completionHandler

View File

@ -18,6 +18,10 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
case obsoleteShare
case screenLockEnabled
case tooManyAttachments
case nilInputItems
case noInputItems
case nilAttachments
case noAttachments
}
private var hasInitialRootViewController = false
@ -468,19 +472,21 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
loadTask = Task {
do {
guard let inputItems = self.extensionContext?.inputItems as? [NSExtensionItem] else {
throw OWSAssertionError("no input item")
throw ShareViewControllerError.nilInputItems
}
let unloadedItems = try Self.itemsToLoad(inputItems: inputItems)
self.conversationPicker.areAttachmentStoriesCompatPrecheck = unloadedItems.allSatisfy { item in
switch item.itemType {
case .movie, .image, .webUrl, .text, .richText:
return true
case .fileUrl, .contact, .pdf, .pkPass, .other:
return false
}
guard let inputItem = inputItems.first else {
throw ShareViewControllerError.noInputItems
}
guard let itemProviders = inputItem.attachments else {
throw ShareViewControllerError.nilAttachments
}
guard !itemProviders.isEmpty else {
throw ShareViewControllerError.noAttachments
}
let loadedItems = try await self.loadItems(unloadedItems: unloadedItems)
let typedItemProviders = try Self.typedItemProviders(for: itemProviders)
self.conversationPicker.areAttachmentStoriesCompatPrecheck = typedItemProviders.allSatisfy { $0.isStoriesCompatible }
let loadedItems = try await self.loadItems(unloadedItems: typedItemProviders)
let attachments = try await self.buildAttachments(loadedItems: loadedItems)
try Task.checkCancellation()
@ -536,81 +542,84 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
Logger.info("showing screen lock")
}
private static func itemsToLoad(inputItems: [NSExtensionItem]) throws -> [UnloadedItem] {
for inputItem in inputItems {
guard let itemProviders = inputItem.attachments else {
throw OWSAssertionError("attachments was empty")
}
private struct TypedItemProvider {
enum ItemType {
case movie
case image
case webUrl
case fileUrl
case contact
case text
case pdf
case pkPass
case other
let itemsToLoad: [UnloadedItem] = itemProviders.map { itemProvider in
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeMovie as String) {
return UnloadedItem(itemProvider: itemProvider, itemType: .movie)
var typeIdentifier: String {
switch self {
case .movie:
kUTTypeMovie as String
case .image:
kUTTypeImage as String
case .webUrl:
kUTTypeURL as String
case .fileUrl:
kUTTypeFileURL as String
case .contact:
kUTTypeVCard as String
case .text:
kUTTypeText as String
case .pdf:
kUTTypePDF as String
case .pkPass:
"com.apple.pkpass"
case .other:
""
}
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeImage as String) {
return UnloadedItem(itemProvider: itemProvider, itemType: .image)
}
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeFileURL as String) {
return UnloadedItem(itemProvider: itemProvider, itemType: .fileUrl)
}
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeURL as String) {
return UnloadedItem(itemProvider: itemProvider, itemType: .webUrl)
}
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeVCard as String) {
return UnloadedItem(itemProvider: itemProvider, itemType: .contact)
}
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeRTF as String) {
return UnloadedItem(itemProvider: itemProvider, itemType: .richText)
}
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypeText as String) {
return UnloadedItem(itemProvider: itemProvider, itemType: .text)
}
if itemProvider.hasItemConformingToTypeIdentifier(kUTTypePDF as String) {
return UnloadedItem(itemProvider: itemProvider, itemType: .pdf)
}
if itemProvider.hasItemConformingToTypeIdentifier("com.apple.pkpass") {
return UnloadedItem(itemProvider: itemProvider, itemType: .pkPass)
}
owsFailDebug("unexpected share item: \(itemProvider)")
return UnloadedItem(itemProvider: itemProvider, itemType: .other)
}
// A single inputItem can have multiple attachments, e.g. sharing from Firefox gives
// one url attachment and another text attachment, where the url would be https://some-news.com/articles/123-cat-stuck-in-tree
// and the text attachment would be something like "Breaking news - cat stuck in tree"
//
// FIXME: For now, we prefer the URL provider and discard the text provider, since it's more useful to share the URL than the caption
// but we *should* include both. This will be a bigger change though since our share extension is currently heavily predicated
// on one itemProvider per share.
//
// Prefer a URL if available. If there's an image item and a URL item,
// the URL is generally more useful. e.g. when sharing an app from the
// App Store the image would be the app icon and the URL is the link
// to the application.
if let urlItem = itemsToLoad.first(where: { $0.itemType == .webUrl }) {
return [urlItem]
}
let visualMediaItems = itemsToLoad.filter { $0.itemType == .image || $0.itemType == .movie }
// We only allow sharing 1 item, unless they are visual media items. And if they are
// visualMediaItems we share *only* the visual media items - a mix of visual and non
// visual items is not supported.
if visualMediaItems.count > 0 {
return visualMediaItems
} else if itemsToLoad.count > 0 {
return Array(itemsToLoad.prefix(1))
}
}
throw OWSAssertionError("no input item")
let itemProvider: NSItemProvider
let itemType: ItemType
var isWebUrl: Bool {
itemType == .webUrl
}
var isVisualMedia: Bool {
itemType == .image || itemType == .movie
}
var isStoriesCompatible: Bool {
switch itemType {
case .movie, .image, .webUrl, .text:
return true
case .fileUrl, .contact, .pdf, .pkPass, .other:
return false
}
}
}
private static func typedItemProviders(for itemProviders: [NSItemProvider]) throws -> [TypedItemProvider] {
// due to UT conformance fallbacks the order these are checked is important; more specific types need to come earlier in the list than their fallbacks
let itemTypeOrder: [TypedItemProvider.ItemType] = [.movie, .image, .fileUrl, .webUrl, .contact, .text, .pdf, .pkPass]
let candidates: [TypedItemProvider] = itemProviders.map { itemProvider in
for itemType in itemTypeOrder {
if itemProvider.hasItemConformingToTypeIdentifier(itemType.typeIdentifier) {
return TypedItemProvider(itemProvider: itemProvider, itemType: itemType)
}
}
owsFailDebug("unexpected share item: \(itemProvider)")
return TypedItemProvider(itemProvider: itemProvider, itemType: .other)
}
// URL shares can come in with text preview and favicon attachments so we ignore other attachments with a URL
if let webUrlCandidate = candidates.first(where: { $0.isWebUrl }) {
return [webUrlCandidate]
}
// only 1 attachment is supported unless it's visual media so select just the first or just the visual media elements with a preference for visual media
let visualMediaCandidates = candidates.filter { $0.isVisualMedia }
return visualMediaCandidates.isEmpty ? Array(candidates.prefix(1)) : visualMediaCandidates
}
private struct LoadedItem {
@ -628,25 +637,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
let payload: LoadedItemPayload
}
private struct UnloadedItem {
enum ItemType {
case movie
case image
case webUrl
case fileUrl
case contact
case richText
case text
case pdf
case pkPass
case other
}
let itemProvider: NSItemProvider
let itemType: ItemType
}
private func loadItems(unloadedItems: [UnloadedItem]) async throws -> [LoadedItem] {
private func loadItems(unloadedItems: [TypedItemProvider]) async throws -> [LoadedItem] {
try await withThrowingTaskGroup(of: LoadedItem.self) { group in
for unloadedItem in unloadedItems {
_ = group.addTaskUnlessCancelled {
@ -662,7 +653,7 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
}
}
private func loadItem(unloadedItem: UnloadedItem) async throws -> LoadedItem {
private func loadItem(unloadedItem: TypedItemProvider) async throws -> LoadedItem {
Logger.info("unloadedItem: \(unloadedItem)")
let itemProvider = unloadedItem.itemProvider
@ -703,8 +694,6 @@ public class ShareViewController: UIViewController, ShareViewDelegate, SAEFailed
registeredTypeIdentifiers: itemProvider.registeredTypeIdentifiers))
case .contact:
return LoadedItem(payload: .contact(try await itemProvider.loadData(forTypeIdentifier: kUTTypeContact as String)))
case .richText:
return LoadedItem(payload: .richText(try await itemProvider.loadAttributedText(forTypeIdentifier: kUTTypeRTF as String)))
case .text:
return LoadedItem(payload: .text(try await itemProvider.loadText(forTypeIdentifier: kUTTypeText as String)))
case .pdf:

View File

@ -37,21 +37,15 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
}
}
private var isViewOnce = false {
didSet {
updateStoriesState()
}
}
lazy var isTextMessage: Bool = {
var isTextMessage: Bool {
guard let attachments = attachments, attachments.count == 1, let attachment = attachments.first else { return false }
return attachment.isConvertibleToTextMessage && attachment.dataLength < kOversizeTextMessageSizeThreshold
}()
}
lazy var isContactShare: Bool = {
var isContactShare: Bool {
guard let attachments = attachments, attachments.count == 1, let attachment = attachments.first else { return false }
return attachment.isConvertibleToContactShare
}()
}
var approvedAttachments: [SignalAttachment]?
var approvedContactShare: ContactShareDraft?
@ -103,18 +97,14 @@ class SharingThreadPickerViewController: ConversationPickerViewController {
}
private func updateStoriesState() {
if isViewOnce {
sectionOptions.remove(.stories)
if areAttachmentStoriesCompatPrecheck == true {
sectionOptions.insert(.stories)
} else if let attachments = attachments, attachments.allSatisfy({ $0.isValidImage || $0.isValidVideo }) {
sectionOptions.insert(.stories)
} else if isTextMessage {
sectionOptions.insert(.stories)
} else {
if areAttachmentStoriesCompatPrecheck == true {
sectionOptions.insert(.stories)
} else if let attachments = attachments, attachments.allSatisfy({ $0.isValidImage || $0.isValidVideo }) {
sectionOptions.insert(.stories)
} else if isTextMessage {
sectionOptions.insert(.stories)
} else {
sectionOptions.remove(.stories)
}
sectionOptions.remove(.stories)
}
}
}
@ -692,7 +682,7 @@ extension SharingThreadPickerViewController: AttachmentApprovalViewControllerDel
}
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didChangeViewOnceState isViewOnce: Bool) {
self.isViewOnce = isViewOnce
// We can ignore this event.
}
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didRemoveAttachment attachment: SignalAttachment) {