react-native-advanced-share-intent 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js ADDED
@@ -0,0 +1,92 @@
1
+ const {
2
+ NativeEventEmitter,
3
+ NativeModules,
4
+ Platform,
5
+ } = require('react-native');
6
+
7
+ const LINKING_ERROR =
8
+ 'react-native-advanced-share-intent is not linked. Rebuild the native app after installing the package.';
9
+
10
+ const NativeAdvancedShareIntent = NativeModules.AdvancedShareIntent;
11
+ const EVENT_NAME = 'AdvancedShareIntentReceived';
12
+
13
+ const emitter = NativeAdvancedShareIntent
14
+ ? new NativeEventEmitter(NativeAdvancedShareIntent)
15
+ : null;
16
+
17
+ function getNativeModule() {
18
+ if (!NativeAdvancedShareIntent) {
19
+ throw new Error(LINKING_ERROR);
20
+ }
21
+ return NativeAdvancedShareIntent;
22
+ }
23
+
24
+ function normalizePayload(payload) {
25
+ if (!payload) {
26
+ return null;
27
+ }
28
+
29
+ return {
30
+ ...payload,
31
+ files: Array.isArray(payload.files) ? payload.files : [],
32
+ receivedAt: payload.receivedAt ?? Date.now(),
33
+ isInitial: Boolean(payload.isInitial),
34
+ };
35
+ }
36
+
37
+ const ShareIntent = {
38
+ async getInitialShare() {
39
+ return normalizePayload(await getNativeModule().getInitialShare());
40
+ },
41
+
42
+ addShareListener(listener) {
43
+ if (!emitter) {
44
+ throw new Error(LINKING_ERROR);
45
+ }
46
+
47
+ return emitter.addListener(EVENT_NAME, payload => {
48
+ const normalizedPayload = normalizePayload(payload);
49
+ if (normalizedPayload) {
50
+ listener(normalizedPayload);
51
+ }
52
+ });
53
+ },
54
+
55
+ async clearSharedData() {
56
+ await getNativeModule().clearSharedData();
57
+ },
58
+
59
+ async setAppGroupIdentifier(identifier) {
60
+ if (Platform.OS !== 'ios') {
61
+ return;
62
+ }
63
+
64
+ const nativeModule = getNativeModule();
65
+ if (!nativeModule.setAppGroupIdentifier) {
66
+ throw new Error('setAppGroupIdentifier is not available on this platform.');
67
+ }
68
+
69
+ await nativeModule.setAppGroupIdentifier(identifier);
70
+ },
71
+
72
+ async setContainingAppScheme(scheme) {
73
+ if (Platform.OS !== 'ios') {
74
+ return;
75
+ }
76
+
77
+ const nativeModule = getNativeModule();
78
+ if (!nativeModule.setContainingAppScheme) {
79
+ throw new Error('setContainingAppScheme is not available on this platform.');
80
+ }
81
+
82
+ await nativeModule.setContainingAppScheme(scheme);
83
+ },
84
+ };
85
+
86
+ module.exports = ShareIntent;
87
+ module.exports.default = ShareIntent;
88
+ module.exports.getInitialShare = ShareIntent.getInitialShare;
89
+ module.exports.addShareListener = ShareIntent.addShareListener;
90
+ module.exports.clearSharedData = ShareIntent.clearSharedData;
91
+ module.exports.setAppGroupIdentifier = ShareIntent.setAppGroupIdentifier;
92
+ module.exports.setContainingAppScheme = ShareIntent.setContainingAppScheme;
package/index.mjs ADDED
@@ -0,0 +1,9 @@
1
+ import ShareIntent from './index.js';
2
+
3
+ export const getInitialShare = ShareIntent.getInitialShare;
4
+ export const addShareListener = ShareIntent.addShareListener;
5
+ export const clearSharedData = ShareIntent.clearSharedData;
6
+ export const setAppGroupIdentifier = ShareIntent.setAppGroupIdentifier;
7
+ export const setContainingAppScheme = ShareIntent.setContainingAppScheme;
8
+
9
+ export default ShareIntent;
@@ -0,0 +1,5 @@
1
+ #import <React/RCTBridgeModule.h>
2
+ #import <React/RCTEventEmitter.h>
3
+
4
+ @interface AdvancedShareIntent : RCTEventEmitter <RCTBridgeModule>
5
+ @end
@@ -0,0 +1,186 @@
1
+ #import "AdvancedShareIntent.h"
2
+
3
+ #import <MobileCoreServices/MobileCoreServices.h>
4
+ #import <React/RCTConvert.h>
5
+ #import <UIKit/UIKit.h>
6
+
7
+ static NSString *const AdvancedShareIntentEventName = @"AdvancedShareIntentReceived";
8
+ static NSString *const AdvancedShareIntentDefaultsKey = @"AdvancedShareIntentPayload";
9
+ static NSString *const AdvancedShareIntentAppGroupKey = @"AdvancedShareIntentAppGroupIdentifier";
10
+ static NSString *const AdvancedShareIntentContainingAppSchemeKey = @"AdvancedShareIntentContainingAppScheme";
11
+
12
+ @interface AdvancedShareIntent ()
13
+ @property (nonatomic, assign) BOOL hasListeners;
14
+ @property (nonatomic, copy, nullable) NSDictionary *initialPayload;
15
+ @property (nonatomic, copy, nullable) NSString *appGroupIdentifier;
16
+ @property (nonatomic, copy, nullable) NSString *containingAppScheme;
17
+ @end
18
+
19
+ @implementation AdvancedShareIntent
20
+
21
+ RCT_EXPORT_MODULE(AdvancedShareIntent)
22
+
23
+ + (BOOL)requiresMainQueueSetup
24
+ {
25
+ return YES;
26
+ }
27
+
28
+ - (instancetype)init
29
+ {
30
+ if ((self = [super init])) {
31
+ _appGroupIdentifier = [[NSUserDefaults standardUserDefaults] stringForKey:AdvancedShareIntentAppGroupKey];
32
+ _containingAppScheme = [[NSUserDefaults standardUserDefaults] stringForKey:AdvancedShareIntentContainingAppSchemeKey];
33
+ _initialPayload = [self readPayloadWithInitial:YES];
34
+
35
+ [[NSNotificationCenter defaultCenter] addObserver:self
36
+ selector:@selector(applicationDidBecomeActive)
37
+ name:UIApplicationDidBecomeActiveNotification
38
+ object:nil];
39
+ }
40
+
41
+ return self;
42
+ }
43
+
44
+ - (void)dealloc
45
+ {
46
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
47
+ }
48
+
49
+ - (NSArray<NSString *> *)supportedEvents
50
+ {
51
+ return @[AdvancedShareIntentEventName];
52
+ }
53
+
54
+ - (void)startObserving
55
+ {
56
+ self.hasListeners = YES;
57
+ NSDictionary *payload = [self readPayloadWithInitial:NO];
58
+ if (payload != nil) {
59
+ [self sendEventWithName:AdvancedShareIntentEventName body:payload];
60
+ }
61
+ }
62
+
63
+ - (void)stopObserving
64
+ {
65
+ self.hasListeners = NO;
66
+ }
67
+
68
+ RCT_REMAP_METHOD(getInitialShare,
69
+ getInitialShareWithResolver:(RCTPromiseResolveBlock)resolve
70
+ rejecter:(RCTPromiseRejectBlock)reject)
71
+ {
72
+ NSDictionary *payload = self.initialPayload ?: [self readPayloadWithInitial:YES];
73
+ self.initialPayload = payload;
74
+ resolve(payload ?: nil);
75
+ }
76
+
77
+ RCT_REMAP_METHOD(clearSharedData,
78
+ clearSharedDataWithResolver:(RCTPromiseResolveBlock)resolve
79
+ rejecter:(RCTPromiseRejectBlock)reject)
80
+ {
81
+ self.initialPayload = nil;
82
+ NSUserDefaults *defaults = [self sharedDefaults];
83
+ [defaults removeObjectForKey:AdvancedShareIntentDefaultsKey];
84
+ [defaults synchronize];
85
+ [self removeSharedFiles];
86
+ resolve(nil);
87
+ }
88
+
89
+ RCT_REMAP_METHOD(setAppGroupIdentifier,
90
+ setAppGroupIdentifier:(NSString *)identifier
91
+ resolver:(RCTPromiseResolveBlock)resolve
92
+ rejecter:(RCTPromiseRejectBlock)reject)
93
+ {
94
+ self.appGroupIdentifier = identifier;
95
+ [[NSUserDefaults standardUserDefaults] setObject:identifier forKey:AdvancedShareIntentAppGroupKey];
96
+ [[NSUserDefaults standardUserDefaults] synchronize];
97
+ self.initialPayload = [self readPayloadWithInitial:YES];
98
+ resolve(nil);
99
+ }
100
+
101
+ RCT_REMAP_METHOD(setContainingAppScheme,
102
+ setContainingAppScheme:(NSString *)scheme
103
+ schemeResolver:(RCTPromiseResolveBlock)resolve
104
+ schemeRejecter:(RCTPromiseRejectBlock)reject)
105
+ {
106
+ self.containingAppScheme = scheme;
107
+ [[NSUserDefaults standardUserDefaults] setObject:scheme forKey:AdvancedShareIntentContainingAppSchemeKey];
108
+ [[NSUserDefaults standardUserDefaults] synchronize];
109
+ resolve(nil);
110
+ }
111
+
112
+ - (void)applicationDidBecomeActive
113
+ {
114
+ if (!self.hasListeners) {
115
+ return;
116
+ }
117
+
118
+ NSDictionary *payload = [self readPayloadWithInitial:NO];
119
+ if (payload != nil) {
120
+ [self sendEventWithName:AdvancedShareIntentEventName body:payload];
121
+ }
122
+ }
123
+
124
+ - (NSUserDefaults *)sharedDefaults
125
+ {
126
+ if (self.appGroupIdentifier.length > 0) {
127
+ NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:self.appGroupIdentifier];
128
+ if (defaults != nil) {
129
+ return defaults;
130
+ }
131
+ }
132
+
133
+ return [NSUserDefaults standardUserDefaults];
134
+ }
135
+
136
+ - (NSDictionary *)readPayloadWithInitial:(BOOL)isInitial
137
+ {
138
+ NSUserDefaults *defaults = [self sharedDefaults];
139
+ NSDictionary *storedPayload = [defaults dictionaryForKey:AdvancedShareIntentDefaultsKey];
140
+ if (storedPayload == nil) {
141
+ return nil;
142
+ }
143
+
144
+ NSMutableDictionary *payload = [storedPayload mutableCopy];
145
+ payload[@"isInitial"] = @(isInitial);
146
+ if (payload[@"files"] == nil) {
147
+ payload[@"files"] = @[];
148
+ }
149
+ if (payload[@"receivedAt"] == nil) {
150
+ payload[@"receivedAt"] = @([[NSDate date] timeIntervalSince1970] * 1000);
151
+ }
152
+
153
+ return payload;
154
+ }
155
+
156
+ - (NSString *)classifyMimeType:(NSString *)mimeType
157
+ {
158
+ if ([mimeType hasPrefix:@"image/"] || [mimeType hasPrefix:@"public.image"]) {
159
+ return @"image";
160
+ }
161
+ if ([mimeType hasPrefix:@"video/"] || [mimeType hasPrefix:@"public.movie"]) {
162
+ return @"video";
163
+ }
164
+ if ([mimeType hasPrefix:@"text/"]) {
165
+ return @"text";
166
+ }
167
+ if ([mimeType isEqualToString:@"photos/asset"]) {
168
+ return @"image";
169
+ }
170
+ return @"document";
171
+ }
172
+
173
+ - (void)removeSharedFiles
174
+ {
175
+ if (self.appGroupIdentifier.length == 0) {
176
+ return;
177
+ }
178
+
179
+ NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:self.appGroupIdentifier];
180
+ NSURL *directoryURL = [containerURL URLByAppendingPathComponent:@"AdvancedShareIntent" isDirectory:YES];
181
+ if (directoryURL != nil) {
182
+ [[NSFileManager defaultManager] removeItemAtURL:directoryURL error:nil];
183
+ }
184
+ }
185
+
186
+ @end
@@ -0,0 +1,363 @@
1
+ import Foundation
2
+ import MobileCoreServices
3
+ import Photos
4
+ import Social
5
+ import UIKit
6
+
7
+ @objc(open) class AdvancedShareIntentShareExtension: UIViewController {
8
+ @objc open var appGroupIdentifier: String { "" }
9
+ @objc open var containingAppScheme: String { "" }
10
+ @objc open var sharedDirectoryName: String { "AdvancedShareIntent" }
11
+
12
+ private var activityIndicator: UIActivityIndicatorView?
13
+ private var cancelButton: UIButton?
14
+
15
+ open override func viewDidLoad() {
16
+ super.viewDidLoad()
17
+ setupLoadingView()
18
+ processSharedItems()
19
+ }
20
+
21
+ @objc private func cancel() {
22
+ extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
23
+ }
24
+
25
+ private func setupLoadingView() {
26
+ view.subviews.forEach { $0.removeFromSuperview() }
27
+ view.backgroundColor = .systemBackground
28
+
29
+ let indicator = UIActivityIndicatorView(style: .large)
30
+ indicator.translatesAutoresizingMaskIntoConstraints = false
31
+ indicator.startAnimating()
32
+ view.addSubview(indicator)
33
+ activityIndicator = indicator
34
+
35
+ let button = UIButton(type: .system)
36
+ button.setTitle("Cancel", for: .normal)
37
+ button.setTitleColor(.systemRed, for: .normal)
38
+ button.translatesAutoresizingMaskIntoConstraints = false
39
+ button.addTarget(self, action: #selector(cancel), for: .touchUpInside)
40
+ view.addSubview(button)
41
+ cancelButton = button
42
+
43
+ NSLayoutConstraint.activate([
44
+ indicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
45
+ indicator.centerYAnchor.constraint(equalTo: view.centerYAnchor),
46
+ button.topAnchor.constraint(equalTo: indicator.bottomAnchor, constant: 40),
47
+ button.centerXAnchor.constraint(equalTo: view.centerXAnchor),
48
+ button.heightAnchor.constraint(equalToConstant: 44)
49
+ ])
50
+ }
51
+
52
+ private func processSharedItems() {
53
+ Task {
54
+ let payload = await ShareExtensionPayloadBuilder(
55
+ context: extensionContext,
56
+ appGroupIdentifier: appGroupIdentifier,
57
+ sharedDirectoryName: sharedDirectoryName
58
+ ).build()
59
+
60
+ save(payload: payload)
61
+ openContainingApp()
62
+ }
63
+ }
64
+
65
+ private func save(payload: [String: Any]) {
66
+ guard !appGroupIdentifier.isEmpty,
67
+ let defaults = UserDefaults(suiteName: appGroupIdentifier) else {
68
+ return
69
+ }
70
+
71
+ defaults.set(payload, forKey: "AdvancedShareIntentPayload")
72
+ defaults.synchronize()
73
+ }
74
+
75
+ private func openContainingApp() {
76
+ guard !containingAppScheme.isEmpty,
77
+ let url = URL(string: "\(containingAppScheme)://share-intent?source=share_extension&timestamp=\(Int(Date().timeIntervalSince1970))") else {
78
+ extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
79
+ return
80
+ }
81
+
82
+ extensionContext?.open(url) { [weak self] _ in
83
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.8) {
84
+ self?.extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ private final class ShareExtensionPayloadBuilder {
91
+ private let context: NSExtensionContext?
92
+ private let appGroupIdentifier: String
93
+ private let sharedDirectoryName: String
94
+ private let lock = NSLock()
95
+ private var indexedFiles: [Int: [String: Any]] = [:]
96
+ private var indexedText: [Int: String] = [:]
97
+
98
+ init(context: NSExtensionContext?, appGroupIdentifier: String, sharedDirectoryName: String) {
99
+ self.context = context
100
+ self.appGroupIdentifier = appGroupIdentifier
101
+ self.sharedDirectoryName = sharedDirectoryName
102
+ }
103
+
104
+ func build() async -> [String: Any] {
105
+ let providers = (context?.inputItems as? [NSExtensionItem])?
106
+ .flatMap { $0.attachments ?? [] } ?? []
107
+
108
+ for (index, provider) in providers.enumerated() {
109
+ await process(provider: provider, index: index)
110
+ }
111
+
112
+ let files = orderedFiles()
113
+ let text = orderedText().joined(separator: "\n")
114
+ let firstMimeType = files.first?["mimeType"] as? String
115
+
116
+ var payload: [String: Any] = [
117
+ "files": files,
118
+ "isInitial": true,
119
+ "receivedAt": Date().timeIntervalSince1970 * 1000
120
+ ]
121
+
122
+ if !text.isEmpty {
123
+ payload["text"] = text
124
+ payload["webUrl"] = text.firstWebURL
125
+ }
126
+ if let firstMimeType {
127
+ payload["mimeType"] = firstMimeType
128
+ }
129
+
130
+ return payload
131
+ }
132
+
133
+ private func process(provider: NSItemProvider, index: Int) async {
134
+ if let text = await loadText(provider: provider) {
135
+ store(text: text, index: index)
136
+ return
137
+ }
138
+
139
+ if let assetFile = await loadPhotosAsset(provider: provider, index: index) {
140
+ store(file: assetFile, index: index)
141
+ return
142
+ }
143
+
144
+ if let file = await loadFile(provider: provider, index: index) {
145
+ store(file: file, index: index)
146
+ }
147
+ }
148
+
149
+ private func loadText(provider: NSItemProvider) async -> String? {
150
+ let identifiers = [kUTTypeURL as String, kUTTypePlainText as String, kUTTypeText as String]
151
+ guard let identifier = identifiers.first(where: { provider.hasItemConformingToTypeIdentifier($0) }) else {
152
+ return nil
153
+ }
154
+
155
+ return await withCheckedContinuation { continuation in
156
+ provider.loadItem(forTypeIdentifier: identifier, options: nil) { item, _ in
157
+ if let url = item as? URL {
158
+ continuation.resume(returning: url.absoluteString)
159
+ } else if let string = item as? String {
160
+ continuation.resume(returning: string)
161
+ } else if let data = item as? Data, let string = String(data: data, encoding: .utf8) {
162
+ continuation.resume(returning: string)
163
+ } else {
164
+ continuation.resume(returning: nil)
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ private func loadPhotosAsset(provider: NSItemProvider, index: Int) async -> [String: Any]? {
171
+ let identifiers = [
172
+ "com.apple.photos.asset",
173
+ "com.apple.photos.asset-id",
174
+ "com.apple.photos.asset-url",
175
+ "com.apple.private.auto-fill-photos-asset",
176
+ "com.apple.private.photos-cloud-sharing",
177
+ "com.apple.photos.library-asset",
178
+ "com.apple.photos.asset-identifier"
179
+ ]
180
+
181
+ for identifier in identifiers where provider.hasItemConformingToTypeIdentifier(identifier) {
182
+ if let assetId = await loadAssetIdentifier(provider: provider, identifier: identifier) {
183
+ return photosAssetPayload(assetId: assetId)
184
+ }
185
+ }
186
+
187
+ return nil
188
+ }
189
+
190
+ private func loadAssetIdentifier(provider: NSItemProvider, identifier: String) async -> String? {
191
+ await withCheckedContinuation { continuation in
192
+ provider.loadItem(forTypeIdentifier: identifier, options: nil) { item, _ in
193
+ if let assetId = item as? String {
194
+ continuation.resume(returning: assetId)
195
+ } else if let url = item as? URL, url.scheme == "ph" {
196
+ continuation.resume(returning: url.absoluteString.replacingOccurrences(of: "ph://", with: ""))
197
+ } else if let data = item as? Data,
198
+ let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any],
199
+ let assetId = plist["assetID"] as? String {
200
+ continuation.resume(returning: assetId)
201
+ } else {
202
+ continuation.resume(returning: nil)
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ private func photosAssetPayload(assetId: String) -> [String: Any] {
209
+ let assets = PHAsset.fetchAssets(withLocalIdentifiers: [assetId], options: nil)
210
+ let asset = assets.firstObject
211
+ let mediaType = asset?.mediaType == .video ? "video" : "image"
212
+ let mimeType = mediaType == "video" ? "photos/video-asset" : "photos/image-asset"
213
+ let dateTaken = asset?.creationDate?.timeIntervalSince1970 ?? 0
214
+
215
+ return [
216
+ "uri": "ph://\(assetId)",
217
+ "fileName": "Photo Asset",
218
+ "name": "Photo Asset",
219
+ "mimeType": mimeType,
220
+ "type": mediaType,
221
+ "dateTaken": dateTaken * 1000,
222
+ "localIdentifier": assetId,
223
+ "originalUri": "ph://\(assetId)"
224
+ ]
225
+ }
226
+
227
+ private func loadFile(provider: NSItemProvider, index: Int) async -> [String: Any]? {
228
+ guard let identifier = preferredFileIdentifier(provider: provider) else {
229
+ return nil
230
+ }
231
+
232
+ return await withCheckedContinuation { continuation in
233
+ provider.loadFileRepresentation(forTypeIdentifier: identifier) { [weak self] url, _ in
234
+ guard let self, let url else {
235
+ continuation.resume(returning: nil)
236
+ return
237
+ }
238
+
239
+ let destination = self.sharedContainerDirectory()
240
+ .appendingPathComponent(UUID().uuidString)
241
+ .appendingPathExtension(url.pathExtension)
242
+
243
+ do {
244
+ if FileManager.default.fileExists(atPath: destination.path) {
245
+ try FileManager.default.removeItem(at: destination)
246
+ }
247
+ try FileManager.default.copyItem(at: url, to: destination)
248
+ continuation.resume(returning: self.filePayload(for: destination, provider: provider))
249
+ } catch {
250
+ continuation.resume(returning: nil)
251
+ }
252
+ }
253
+ }
254
+ }
255
+
256
+ private func preferredFileIdentifier(provider: NSItemProvider) -> String? {
257
+ let preferred = [
258
+ kUTTypeImage as String,
259
+ kUTTypeMovie as String,
260
+ kUTTypePDF as String,
261
+ kUTTypeData as String
262
+ ]
263
+
264
+ return preferred.first(where: { provider.hasItemConformingToTypeIdentifier($0) })
265
+ ?? provider.registeredTypeIdentifiers.first
266
+ }
267
+
268
+ private func filePayload(for url: URL, provider: NSItemProvider) -> [String: Any] {
269
+ let mimeType = provider.registeredTypeIdentifiers.first ?? "application/octet-stream"
270
+ let attributes = try? FileManager.default.attributesOfItem(atPath: url.path)
271
+ let createdDate = (attributes?[.creationDate] as? Date)?.timeIntervalSince1970 ?? Date().timeIntervalSince1970
272
+
273
+ var payload: [String: Any] = [
274
+ "uri": url.absoluteString,
275
+ "fileName": url.lastPathComponent,
276
+ "name": url.lastPathComponent,
277
+ "mimeType": mimeType,
278
+ "type": classify(mimeType: mimeType),
279
+ "dateTaken": createdDate * 1000,
280
+ "originalUri": url.absoluteString
281
+ ]
282
+
283
+ if let size = attributes?[.size] as? NSNumber {
284
+ payload["size"] = size
285
+ }
286
+
287
+ return payload
288
+ }
289
+
290
+ private func orderedFiles() -> [[String: Any]] {
291
+ lock.lock()
292
+ let files = indexedFiles.keys.sorted().compactMap { indexedFiles[$0] }
293
+ lock.unlock()
294
+
295
+ let hasImages = files.contains { ($0["type"] as? String) == "image" }
296
+ let hasVideos = files.contains { ($0["type"] as? String) == "video" }
297
+ guard hasImages && hasVideos else {
298
+ return files
299
+ }
300
+
301
+ let images = files.filter { ($0["type"] as? String) == "image" }
302
+ guard let first = images.first?["dateTaken"] as? Double,
303
+ let last = images.last?["dateTaken"] as? Double else {
304
+ return files
305
+ }
306
+
307
+ let ascending = first < last
308
+ return files.sorted {
309
+ let left = $0["dateTaken"] as? Double ?? 0
310
+ let right = $1["dateTaken"] as? Double ?? 0
311
+ return ascending ? left < right : left > right
312
+ }
313
+ }
314
+
315
+ private func orderedText() -> [String] {
316
+ lock.lock()
317
+ let text = indexedText.keys.sorted().compactMap { indexedText[$0] }
318
+ lock.unlock()
319
+ return text
320
+ }
321
+
322
+ private func store(file: [String: Any], index: Int) {
323
+ lock.lock()
324
+ indexedFiles[index] = file
325
+ lock.unlock()
326
+ }
327
+
328
+ private func store(text: String, index: Int) {
329
+ lock.lock()
330
+ indexedText[index] = text
331
+ lock.unlock()
332
+ }
333
+
334
+ private func sharedContainerDirectory() -> URL {
335
+ if !appGroupIdentifier.isEmpty,
336
+ let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier) {
337
+ let directory = container.appendingPathComponent(sharedDirectoryName, isDirectory: true)
338
+ try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
339
+ return directory
340
+ }
341
+
342
+ return FileManager.default.temporaryDirectory
343
+ }
344
+
345
+ private func classify(mimeType: String) -> String {
346
+ if mimeType.hasPrefix("public.image") || mimeType.hasPrefix("image/") {
347
+ return "image"
348
+ }
349
+ if mimeType.hasPrefix("public.movie") || mimeType.hasPrefix("video/") {
350
+ return "video"
351
+ }
352
+ if mimeType.hasPrefix("public.text") || mimeType.hasPrefix("text/") {
353
+ return "text"
354
+ }
355
+ return "document"
356
+ }
357
+ }
358
+
359
+ private extension String {
360
+ var firstWebURL: String? {
361
+ range(of: #"https?://\S+"#, options: .regularExpression).map { String(self[$0]) }
362
+ }
363
+ }