Bluesky app fork with some witchin' additions 馃挮
at main 210 lines 6.8 kB view raw
1import UIKit 2import AVKit 3 4let IMAGE_EXTENSIONS: [String] = ["png", "jpg", "jpeg", "gif", "heic"] 5let MOVIE_EXTENSIONS: [String] = ["mov", "mp4", "m4v"] 6 7enum URLType: String, CaseIterable { 8 case image 9 case movie 10 case other 11} 12 13class ShareViewController: UIViewController { 14 // This allows other forks to use this extension while also changing their 15 // scheme. 16 let appScheme = Bundle.main.object(forInfoDictionaryKey: "MainAppScheme") as? String ?? "bluesky" 17 18 override func viewDidAppear(_ animated: Bool) { 19 super.viewDidAppear(animated) 20 21 guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem, 22 let attachments = extensionItem.attachments, 23 let firstAttachment = extensionItem.attachments?.first 24 else { 25 self.completeRequest() 26 return 27 } 28 29 Task { 30 if firstAttachment.hasItemConformingToTypeIdentifier("public.text") { 31 await self.handleText(item: firstAttachment) 32 } else if firstAttachment.hasItemConformingToTypeIdentifier("public.url") { 33 await self.handleUrl(item: firstAttachment) 34 } else if firstAttachment.hasItemConformingToTypeIdentifier("public.image") { 35 await self.handleImages(items: attachments) 36 } else if firstAttachment.hasItemConformingToTypeIdentifier("public.movie") { 37 await self.handleVideos(items: attachments) 38 } else { 39 self.completeRequest() 40 } 41 } 42 } 43 44 private func handleText(item: NSItemProvider) async { 45 if let data = try? await item.loadItem(forTypeIdentifier: "public.text") as? String { 46 if let encoded = data.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), 47 let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") { 48 _ = self.openURL(url) 49 } 50 } 51 self.completeRequest() 52 } 53 54 private func handleUrl(item: NSItemProvider) async { 55 if let data = try? await item.loadItem(forTypeIdentifier: "public.url") as? URL { 56 switch data.type { 57 case .image: 58 await handleImages(items: [item]) 59 return 60 case .movie: 61 await handleVideos(items: [item]) 62 return 63 case .other: 64 if let encoded = data.absoluteString.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), 65 let url = URL(string: "\(self.appScheme)://intent/compose?text=\(encoded)") { 66 _ = self.openURL(url) 67 } 68 } 69 } 70 self.completeRequest() 71 } 72 73 private func handleImages(items: [NSItemProvider]) async { 74 let firstFourItems: [NSItemProvider] 75 if items.count < 4 { 76 firstFourItems = items 77 } else { 78 firstFourItems = Array(items[0...3]) 79 } 80 81 var valid = true 82 var imageUris = "" 83 84 for (index, item) in firstFourItems.enumerated() { 85 var imageUriInfo: String? 86 87 do { 88 if let dataUri = try await item.loadItem(forTypeIdentifier: "public.image") as? URL { 89 // We need to duplicate this image, since we don't have access to the outgoing temp directory 90 // We also will get the image dimensions here, sinze RN makes it difficult to get those dimensions for local files 91 let data = try Data(contentsOf: dataUri) 92 let image = UIImage(data: data) 93 imageUriInfo = self.saveImageWithInfo(image) 94 } else if let image = try await item.loadItem(forTypeIdentifier: "public.image") as? UIImage { 95 imageUriInfo = self.saveImageWithInfo(image) 96 } 97 } catch { 98 valid = false 99 } 100 101 if let imageUriInfo = imageUriInfo { 102 imageUris.append(imageUriInfo) 103 if index < items.count - 1 { 104 imageUris.append(",") 105 } 106 } else { 107 valid = false 108 } 109 } 110 111 if valid, 112 let encoded = imageUris.addingPercentEncoding(withAllowedCharacters: .urlHostAllowed), 113 let url = URL(string: "\(self.appScheme)://intent/compose?imageUris=\(encoded)") { 114 _ = self.openURL(url) 115 } 116 117 self.completeRequest() 118 } 119 120 private func handleVideos(items: [NSItemProvider]) async { 121 let firstItem = items.first 122 123 if let dataUrl = try? await firstItem?.loadItem(forTypeIdentifier: "public.movie") as? URL { 124 let ext = String(dataUrl.lastPathComponent.split(separator: ".").last ?? "mp4") 125 if let videoUriInfo = saveVideoWithInfo(dataUrl), 126 let url = URL(string: "\(self.appScheme)://intent/compose?videoUri=\(videoUriInfo)") { 127 _ = self.openURL(url) 128 } 129 } 130 131 self.completeRequest() 132 } 133 134 private func saveImageWithInfo(_ image: UIImage?) -> String? { 135 guard let image = image else { 136 return nil 137 } 138 139 do { 140 // Saving this file to the bundle group's directory lets us access it from 141 // inside of the app. Otherwise, we wouldn't have access even though the 142 // extension does. 143 if let tempUrl = getTempUrl(ext: "jpeg"), 144 let jpegData = image.jpegData(compressionQuality: 1) { 145 try jpegData.write(to: tempUrl) 146 return "\(tempUrl.absoluteString)|\(image.size.width)|\(image.size.height)" 147 } 148 } catch {} 149 return nil 150 } 151 152 private func saveVideoWithInfo(_ dataUrl: URL) -> String? { 153 let ext = String(dataUrl.lastPathComponent.split(separator: ".").last ?? "mp4") 154 guard let tempUrl = getTempUrl(ext: ext) else { 155 return nil 156 } 157 158 let data = try? Data(contentsOf: dataUrl) 159 try? data?.write(to: tempUrl) 160 161 guard let track = AVURLAsset(url: dataUrl).tracks(withMediaType: AVMediaType.video).first else { 162 _ = try? FileManager().removeItem(at: tempUrl) 163 return nil 164 } 165 166 let size = track.naturalSize.applying(track.preferredTransform) 167 return "\(tempUrl.absoluteString)|\(size.width)|\(size.height)" 168 } 169 170 private func completeRequest() { 171 self.extensionContext?.completeRequest(returningItems: nil) 172 } 173 174 private func getTempUrl(ext: String) -> URL? { 175 if let dir = FileManager().containerURL(forSecurityApplicationGroupIdentifier: "group.app.witchsky") { 176 return URL(string: "\(dir.absoluteString)\(ProcessInfo.processInfo.globallyUniqueString).\(ext)")! 177 } 178 return nil 179 } 180 181 @objc func openURL(_ url: URL) -> Bool { 182 var responder: UIResponder? = self 183 while responder != nil { 184 if let application = responder as? UIApplication { 185 application.open(url) 186 return true 187 } 188 responder = responder?.next 189 } 190 return false 191 } 192} 193 194extension URL { 195 var type: URLType { 196 get { 197 guard self.absoluteString.starts(with: "file://"), 198 let ext = self.pathComponents.last?.split(separator: ".").last?.lowercased() else { 199 return .other 200 } 201 202 if IMAGE_EXTENSIONS.contains(ext) { 203 return .image 204 } else if MOVIE_EXTENSIONS.contains(ext) { 205 return .movie 206 } 207 return .other 208 } 209 } 210}