forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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}