stream-chat-react-native 9.0.0-beta.3 → 9.0.0-beta.30

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.
@@ -1,7 +1,5 @@
1
1
  #import "StreamChatReactNative.h"
2
2
  #import <React/RCTLog.h>
3
- #import <AssetsLibrary/AssetsLibrary.h>
4
- #import <MobileCoreServices/MobileCoreServices.h>
5
3
 
6
4
  #if __has_include(<React/RCTLog.h>)
7
5
  #import <React/RCTLog.h>
@@ -19,12 +17,12 @@ NSString *moduleName = @"StreamChatReactNative";
19
17
 
20
18
  RCT_EXPORT_MODULE()
21
19
 
22
- RCT_REMAP_METHOD(createResizedImage, uri:(NSString *)uri width:(double)width height:(double)height format:(NSString *)format quality:(double)quality mode:(NSString *)mode onlyScaleDown:(BOOL)onlyScaleDown rotation:(nonnull NSNumber *)rotation outputPath:(NSString *)outputPath keepMeta:(nonnull NSNumber *)keepMeta resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
20
+ RCT_REMAP_METHOD(createResizedImage, uri:(NSString *)uri width:(double)width height:(double)height format:(NSString *)format quality:(double)quality mode:(NSString *)mode onlyScaleDown:(BOOL)onlyScaleDown rotation:(nonnull NSNumber *)rotation outputPath:(NSString *)outputPath resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
23
21
  {
24
- [self createResizedImage:uri width:width height:height format:format quality:quality mode:mode onlyScaleDown:onlyScaleDown rotation:rotation outputPath:outputPath keepMeta:keepMeta resolve:resolve reject:reject];
22
+ [self createResizedImage:uri width:width height:height format:format quality:quality mode:mode onlyScaleDown:onlyScaleDown rotation:rotation outputPath:outputPath resolve:resolve reject:reject];
25
23
  }
26
24
 
27
- - (void)createResizedImage:(NSString *)uri width:(double)width height:(double)height format:(NSString *)format quality:(double)quality mode:(NSString *)mode onlyScaleDown:(BOOL)onlyScaleDown rotation:(nonnull NSNumber *)rotation outputPath:(NSString *)outputPath keepMeta:(nonnull NSNumber *)keepMeta resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
25
+ - (void)createResizedImage:(NSString *)uri width:(double)width height:(double)height format:(NSString *)format quality:(double)quality mode:(NSString *)mode onlyScaleDown:(BOOL)onlyScaleDown rotation:(nonnull NSNumber *)rotation outputPath:(NSString *)outputPath resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
28
26
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
29
27
  @try {
30
28
  CGSize newSize = CGSizeMake(width, height);
@@ -57,7 +55,7 @@ RCT_REMAP_METHOD(createResizedImage, uri:(NSString *)uri width:(double)width hei
57
55
  reject([NSString stringWithFormat: @"%ld", (long)error.code], error.description, nil);
58
56
  return;
59
57
  }
60
- NSDictionary * response = transformImage(image, uri, [rotation integerValue], newSize, fullPath, format, (int)quality, [keepMeta boolValue], @{@"mode": mode, @"onlyScaleDown": [NSNumber numberWithBool:onlyScaleDown]});
58
+ NSDictionary * response = transformImage(image, [rotation integerValue], newSize, fullPath, format, (int)quality, @{@"mode": mode, @"onlyScaleDown": [NSNumber numberWithBool:onlyScaleDown]});
61
59
  resolve(response);
62
60
  }];
63
61
  } @catch (NSException *exception) {
@@ -69,65 +67,21 @@ RCT_REMAP_METHOD(createResizedImage, uri:(NSString *)uri width:(double)width hei
69
67
 
70
68
 
71
69
 
72
- bool saveImage(NSString * fullPath, UIImage * image, NSString * format, float quality, NSMutableDictionary *metadata)
70
+ bool saveImage(NSString * fullPath, UIImage * image, NSString * format, float quality)
73
71
  {
74
- if(metadata == nil){
75
- NSData* data = nil;
76
- if ([format isEqualToString:@"JPEG"]) {
77
- data = UIImageJPEGRepresentation(image, quality / 100.0);
78
- } else if ([format isEqualToString:@"PNG"]) {
79
- data = UIImagePNGRepresentation(image);
80
- }
81
-
82
- if (data == nil) {
83
- return NO;
84
- }
85
-
86
- NSFileManager* fileManager = [NSFileManager defaultManager];
87
- return [fileManager createFileAtPath:fullPath contents:data attributes:nil];
72
+ NSData* data = nil;
73
+ if ([format isEqualToString:@"JPEG"]) {
74
+ data = UIImageJPEGRepresentation(image, quality / 100.0);
75
+ } else if ([format isEqualToString:@"PNG"]) {
76
+ data = UIImagePNGRepresentation(image);
88
77
  }
89
78
 
90
- // process / write metadata together with image data
91
- else{
92
-
93
- CFStringRef imgType = kUTTypeJPEG;
94
-
95
- if ([format isEqualToString:@"JPEG"]) {
96
- [metadata setObject:@(quality / 100.0) forKey:(__bridge NSString *)kCGImageDestinationLossyCompressionQuality];
97
- }
98
- else if([format isEqualToString:@"PNG"]){
99
- imgType = kUTTypePNG;
100
- }
101
- else{
102
- return NO;
103
- }
104
-
105
- NSMutableData * destData = [NSMutableData data];
106
-
107
- CGImageDestinationRef destination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)destData, imgType, 1, NULL);
108
-
109
- @try{
110
- CGImageDestinationAddImage(destination, image.CGImage, (__bridge CFDictionaryRef) metadata);
111
-
112
- // write final image data with metadata to our destination
113
- if (CGImageDestinationFinalize(destination)){
114
-
115
- NSFileManager* fileManager = [NSFileManager defaultManager];
116
- return [fileManager createFileAtPath:fullPath contents:destData attributes:nil];
117
- }
118
- else{
119
- return NO;
120
- }
121
- }
122
- @finally{
123
- @try{
124
- CFRelease(destination);
125
- }
126
- @catch(NSException *exception){
127
- NSLog(@"Failed to release CGImageDestinationRef: %@", exception);
128
- }
129
- }
79
+ if (data == nil) {
80
+ return NO;
130
81
  }
82
+
83
+ NSFileManager* fileManager = [NSFileManager defaultManager];
84
+ return [fileManager createFileAtPath:fullPath contents:data attributes:nil];
131
85
  }
132
86
 
133
87
  NSString * generateFilePath(NSString * ext, NSString * outputPath)
@@ -268,81 +222,12 @@ UIImage* scaleImage (UIImage* image, CGSize toSize, NSString* mode, bool onlySca
268
222
  return newImage;
269
223
  }
270
224
 
271
- // Returns the image's metadata, or nil if failed to retrieve it.
272
- NSMutableDictionary * getImageMeta(NSString * path)
273
- {
274
- if([path hasPrefix:@"assets-library"]) {
275
-
276
- __block NSMutableDictionary* res = nil;
277
-
278
- ALAssetsLibraryAssetForURLResultBlock resultblock = ^(ALAsset *myasset)
279
- {
280
-
281
- NSDictionary *exif = [[myasset defaultRepresentation] metadata];
282
- res = [exif mutableCopy];
283
-
284
- };
285
-
286
- ALAssetsLibrary* assetslibrary = [[ALAssetsLibrary alloc] init];
287
- NSURL *url = [NSURL URLWithString:[path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
288
-
289
- [assetslibrary assetForURL:url resultBlock:resultblock failureBlock:^(NSError *error) { NSLog(@"error couldn't image from assets library"); }];
290
-
291
- return res;
292
-
293
- } else {
294
-
295
- NSData* imageData = nil;
296
-
297
- if ([path hasPrefix:@"data:"] || [path hasPrefix:@"file:"]) {
298
- NSURL *imageUrl = [[NSURL alloc] initWithString:path];
299
- imageData = [NSData dataWithContentsOfURL:imageUrl];
300
-
301
- } else {
302
- imageData = [NSData dataWithContentsOfFile:path];
303
- }
304
-
305
- if(imageData == nil){
306
- NSLog(@"Could not get image file data to extract metadata.");
307
- return nil;
308
- }
309
-
310
- CGImageSourceRef source = CGImageSourceCreateWithData((CFDataRef)imageData, NULL);
311
-
312
-
313
- if(source != nil){
314
-
315
- CFDictionaryRef metaRef = CGImageSourceCopyPropertiesAtIndex(source, 0, NULL);
316
-
317
- // release CF image
318
- CFRelease(source);
319
-
320
- CFMutableDictionaryRef metaRefMutable = CFDictionaryCreateMutableCopy(NULL, 0, metaRef);
321
-
322
- // release the source meta ref now that we've copie it
323
- CFRelease(metaRef);
324
-
325
- // bridge CF object so it auto releases
326
- NSMutableDictionary* res = (NSMutableDictionary *)CFBridgingRelease(metaRefMutable);
327
-
328
- return res;
329
-
330
- }
331
- else{
332
- return nil;
333
- }
334
-
335
- }
336
- }
337
-
338
225
  NSDictionary * transformImage(UIImage *image,
339
- NSString * originalPath,
340
226
  int rotation,
341
227
  CGSize newSize,
342
228
  NSString* fullPath,
343
229
  NSString* format,
344
230
  int quality,
345
- BOOL keepMeta,
346
231
  NSDictionary* options)
347
232
  {
348
233
  if (image == nil) {
@@ -368,25 +253,8 @@ NSDictionary * transformImage(UIImage *image,
368
253
  if (scaledImage == nil) {
369
254
  [NSException raise:moduleName format:@"Can't resize the image."];
370
255
  }
371
-
372
-
373
- NSMutableDictionary *metadata = nil;
374
-
375
- // to be consistent with Android, we will only allow JPEG
376
- // to do this.
377
- if(keepMeta && [format isEqualToString:@"JPEG"]){
378
-
379
- metadata = getImageMeta(originalPath);
380
-
381
- // remove orientation (since we fix it)
382
- // width/height meta is adjusted automatically
383
- // NOTE: This might still leave some stale values due to resize
384
- metadata[(NSString*)kCGImagePropertyOrientation] = @(1);
385
-
386
- }
387
-
388
256
  // Compress and save the image
389
- if (!saveImage(fullPath, scaledImage, format, quality, metadata)) {
257
+ if (!saveImage(fullPath, scaledImage, format, quality)) {
390
258
  [NSException raise:moduleName format:@"Can't save the image. Check your compression format and your output path"];
391
259
  }
392
260
 
@@ -0,0 +1,14 @@
1
+ #ifdef RCT_NEW_ARCH_ENABLED
2
+
3
+ #if __has_include("StreamChatReactNativeSpec.h")
4
+ #import "StreamChatReactNativeSpec.h"
5
+ #elif __has_include("StreamChatExpoSpec.h")
6
+ #import "StreamChatExpoSpec.h"
7
+ #else
8
+ #error "Unable to find generated codegen spec header for StreamVideoThumbnail."
9
+ #endif
10
+
11
+ @interface StreamVideoThumbnail : NSObject <NativeStreamVideoThumbnailSpec>
12
+ @end
13
+
14
+ #endif
@@ -0,0 +1,56 @@
1
+ #import "StreamVideoThumbnail.h"
2
+
3
+ #ifdef RCT_NEW_ARCH_ENABLED
4
+
5
+ #if __has_include(<stream_chat_react_native/stream_chat_react_native-Swift.h>)
6
+ #import <stream_chat_react_native/stream_chat_react_native-Swift.h>
7
+ #elif __has_include(<stream_chat_expo/stream_chat_expo-Swift.h>)
8
+ #import <stream_chat_expo/stream_chat_expo-Swift.h>
9
+ #elif __has_include("stream_chat_react_native-Swift.h")
10
+ #import "stream_chat_react_native-Swift.h"
11
+ #elif __has_include("stream_chat_expo-Swift.h")
12
+ #import "stream_chat_expo-Swift.h"
13
+ #else
14
+ #error "Unable to import generated Swift header for StreamVideoThumbnail."
15
+ #endif
16
+
17
+ @implementation StreamVideoThumbnail
18
+
19
+ RCT_EXPORT_MODULE(StreamVideoThumbnail)
20
+
21
+ RCT_REMAP_METHOD(createVideoThumbnails, urls:(NSArray<NSString *> *)urls resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
22
+ {
23
+ [self createVideoThumbnails:urls resolve:resolve reject:reject];
24
+ }
25
+
26
+ - (void)createVideoThumbnails:(NSArray<NSString *> *)urls
27
+ resolve:(RCTPromiseResolveBlock)resolve
28
+ reject:(RCTPromiseRejectBlock)reject
29
+ {
30
+ [StreamVideoThumbnailGenerator generateThumbnailsWithUrls:urls completion:^(NSArray<StreamVideoThumbnailResult *> *thumbnails) {
31
+ NSMutableArray<NSDictionary<NSString *, id> *> *payload = [NSMutableArray arrayWithCapacity:thumbnails.count];
32
+
33
+ for (StreamVideoThumbnailResult *thumbnail in thumbnails) {
34
+ NSMutableDictionary<NSString *, id> *entry = [NSMutableDictionary dictionaryWithCapacity:2];
35
+ entry[@"uri"] = thumbnail.uri ?: [NSNull null];
36
+ entry[@"error"] = thumbnail.error ?: [NSNull null];
37
+ [payload addObject:entry];
38
+ }
39
+
40
+ @try {
41
+ resolve(payload);
42
+ } @catch (NSException *exception) {
43
+ reject(@"stream_video_thumbnail_error", exception.reason, nil);
44
+ }
45
+ }];
46
+ }
47
+
48
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
49
+ (const facebook::react::ObjCTurboModule::InitParams &)params
50
+ {
51
+ return std::make_shared<facebook::react::NativeStreamVideoThumbnailSpecJSI>(params);
52
+ }
53
+
54
+ @end
55
+
56
+ #endif
@@ -0,0 +1,338 @@
1
+ import AVFoundation
2
+ import Photos
3
+ import UIKit
4
+
5
+ private final class StreamPhotoLibraryAssetRequestState: @unchecked Sendable {
6
+ let lock = NSLock()
7
+ var didResume = false
8
+ var requestID: PHImageRequestID = PHInvalidImageRequestID
9
+ }
10
+
11
+ @objcMembers
12
+ public final class StreamVideoThumbnailResult: NSObject {
13
+ public let error: String?
14
+ public let uri: String?
15
+
16
+ public init(error: String? = nil, uri: String? = nil) {
17
+ self.error = error
18
+ self.uri = uri
19
+ }
20
+ }
21
+
22
+ @objcMembers
23
+ public final class StreamVideoThumbnailGenerator: NSObject {
24
+ private static let compressionQuality: CGFloat = 0.8
25
+ private static let maxDimension: CGFloat = 512
26
+ private static let cacheVersion = "v1"
27
+ private static let cacheDirectoryName = "@stream-io-stream-video-thumbnails"
28
+ private static let maxConcurrentGenerations = 5
29
+ private static let photoLibraryAssetResolutionTimeout: TimeInterval = 3
30
+
31
+ @objc(generateThumbnailsWithUrls:completion:)
32
+ public static func generateThumbnails(
33
+ urls: [String],
34
+ completion: @escaping ([StreamVideoThumbnailResult]) -> Void
35
+ ) {
36
+ Task(priority: .userInitiated) {
37
+ completion(await generateThumbnailsAsync(urls: urls))
38
+ }
39
+ }
40
+
41
+ private static func generateThumbnailsAsync(urls: [String]) async -> [StreamVideoThumbnailResult] {
42
+ guard !urls.isEmpty else {
43
+ return []
44
+ }
45
+
46
+ if urls.count == 1 {
47
+ return [await generateThumbnailResult(url: urls[0])]
48
+ }
49
+
50
+ let parallelism = min(maxConcurrentGenerations, urls.count)
51
+
52
+ return await withTaskGroup(
53
+ of: (Int, StreamVideoThumbnailResult).self,
54
+ returning: [StreamVideoThumbnailResult].self
55
+ ) { group in
56
+ var thumbnails = Array<StreamVideoThumbnailResult?>(repeating: nil, count: urls.count)
57
+ var nextIndexToSchedule = 0
58
+
59
+ while nextIndexToSchedule < parallelism {
60
+ let index = nextIndexToSchedule
61
+ let url = urls[index]
62
+ group.addTask {
63
+ (index, await generateThumbnailResult(url: url))
64
+ }
65
+ nextIndexToSchedule += 1
66
+ }
67
+
68
+ while let (index, thumbnail) = await group.next() {
69
+ thumbnails[index] = thumbnail
70
+
71
+ if nextIndexToSchedule < urls.count {
72
+ let nextIndex = nextIndexToSchedule
73
+ let nextURL = urls[nextIndex]
74
+ group.addTask {
75
+ (nextIndex, await generateThumbnailResult(url: nextURL))
76
+ }
77
+ nextIndexToSchedule += 1
78
+ }
79
+ }
80
+
81
+ return thumbnails.enumerated().map { index, thumbnail in
82
+ thumbnail ?? StreamVideoThumbnailResult(
83
+ error: "Thumbnail generation produced no output for index \(index)",
84
+ uri: nil
85
+ )
86
+ }
87
+ }
88
+ }
89
+
90
+ private static func generateThumbnailResult(url: String) async -> StreamVideoThumbnailResult {
91
+ do {
92
+ return StreamVideoThumbnailResult(uri: try await generateThumbnail(url: url))
93
+ } catch {
94
+ return StreamVideoThumbnailResult(
95
+ error: error.localizedDescription,
96
+ uri: nil
97
+ )
98
+ }
99
+ }
100
+
101
+ private static func generateThumbnail(url: String) async throws -> String {
102
+ let outputDirectory = try thumbnailCacheDirectory()
103
+ let outputURL = outputDirectory
104
+ .appendingPathComponent(buildCacheFileName(url: url))
105
+ .appendingPathExtension("jpg")
106
+
107
+ if
108
+ FileManager.default.fileExists(atPath: outputURL.path),
109
+ let attributes = try? FileManager.default.attributesOfItem(atPath: outputURL.path),
110
+ let fileSize = attributes[.size] as? NSNumber,
111
+ fileSize.intValue > 0
112
+ {
113
+ return outputURL.absoluteString
114
+ }
115
+
116
+ let asset = try await resolveAsset(url: url)
117
+ let generator = AVAssetImageGenerator(asset: asset)
118
+ generator.appliesPreferredTrackTransform = true
119
+ generator.maximumSize = CGSize(width: maxDimension, height: maxDimension)
120
+
121
+ let requestedTime = thumbnailTime(for: asset)
122
+
123
+ do {
124
+ let cgImage = try generator.copyCGImage(at: requestedTime, actualTime: nil)
125
+ let image = UIImage(cgImage: cgImage)
126
+ guard let data = image.jpegData(compressionQuality: compressionQuality) else {
127
+ throw thumbnailError(code: 2, message: "Failed to encode JPEG thumbnail for \(url)")
128
+ }
129
+
130
+ try data.write(to: outputURL, options: .atomic)
131
+ return outputURL.absoluteString
132
+ } catch {
133
+ throw thumbnailError(error, code: 3, message: "Thumbnail generation failed for \(url)")
134
+ }
135
+ }
136
+
137
+ private static func thumbnailCacheDirectory() throws -> URL {
138
+ let outputDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
139
+ .appendingPathComponent(cacheDirectoryName, isDirectory: true)
140
+ try FileManager.default.createDirectory(
141
+ at: outputDirectory,
142
+ withIntermediateDirectories: true
143
+ )
144
+ return outputDirectory
145
+ }
146
+
147
+ private static func buildCacheFileName(url: String) -> String {
148
+ let cacheKey = fnv1a64("\(cacheVersion)|\(Int(maxDimension))|\(compressionQuality)|\(url)")
149
+ return "stream-video-thumbnail-\(cacheKey)"
150
+ }
151
+
152
+ private static func fnv1a64(_ value: String) -> String {
153
+ var hash: UInt64 = 0xcbf29ce484222325
154
+ let prime: UInt64 = 0x100000001b3
155
+
156
+ for byte in value.utf8 {
157
+ hash ^= UInt64(byte)
158
+ hash &*= prime
159
+ }
160
+
161
+ return String(hash, radix: 16)
162
+ }
163
+
164
+ private static func thumbnailTime(for asset: AVAsset) -> CMTime {
165
+ let durationSeconds = asset.duration.seconds
166
+ if durationSeconds.isFinite, durationSeconds > 0.1 {
167
+ return CMTime(seconds: 0.1, preferredTimescale: 600)
168
+ }
169
+
170
+ return .zero
171
+ }
172
+
173
+ private static func resolveAsset(url: String) async throws -> AVAsset {
174
+ if isPhotoLibraryURL(url) {
175
+ return try await resolvePhotoLibraryAsset(url: url)
176
+ }
177
+
178
+ if let normalizedURL = normalizeLocalURL(url) {
179
+ return AVURLAsset(url: normalizedURL)
180
+ }
181
+
182
+ throw thumbnailError(code: 5, message: "Unsupported video URL for thumbnail generation: \(url)")
183
+ }
184
+
185
+ private static func isPhotoLibraryURL(_ url: String) -> Bool {
186
+ url.lowercased().hasPrefix("ph://")
187
+ }
188
+
189
+ private static func resolvePhotoLibraryAsset(url: String) async throws -> AVAsset {
190
+ let identifier = photoLibraryIdentifier(from: url)
191
+
192
+ guard !identifier.isEmpty else {
193
+ throw thumbnailError(code: 6, message: "Missing photo library identifier for \(url)")
194
+ }
195
+
196
+ let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: nil)
197
+ guard let asset = fetchResult.firstObject else {
198
+ throw thumbnailError(code: 7, message: "Failed to find photo library asset for \(url)")
199
+ }
200
+
201
+ let options = PHVideoRequestOptions()
202
+ options.deliveryMode = .highQualityFormat
203
+ options.isNetworkAccessAllowed = true
204
+ options.version = .current
205
+
206
+ return try await withThrowingTaskGroup(of: AVAsset.self) { group in
207
+ group.addTask {
208
+ try await requestPhotoLibraryAsset(url: url, asset: asset, options: options)
209
+ }
210
+ group.addTask {
211
+ try await Task.sleep(nanoseconds: UInt64(photoLibraryAssetResolutionTimeout * 1_000_000_000))
212
+ throw thumbnailError(
213
+ code: 11,
214
+ message: "Timed out resolving photo library asset for \(url)"
215
+ )
216
+ }
217
+
218
+ guard let resolvedAsset = try await group.next() else {
219
+ throw thumbnailError(
220
+ code: 10,
221
+ message: "Failed to resolve photo library asset for \(url)"
222
+ )
223
+ }
224
+
225
+ group.cancelAll()
226
+ return resolvedAsset
227
+ }
228
+ }
229
+
230
+ private static func requestPhotoLibraryAsset(
231
+ url: String,
232
+ asset: PHAsset,
233
+ options: PHVideoRequestOptions
234
+ ) async throws -> AVAsset {
235
+ let imageManager = PHImageManager.default()
236
+ let state = StreamPhotoLibraryAssetRequestState()
237
+
238
+ return try await withTaskCancellationHandler(operation: {
239
+ try await withCheckedThrowingContinuation { continuation in
240
+ let requestID = imageManager.requestAVAsset(forVideo: asset, options: options) {
241
+ avAsset, _, info in
242
+ state.lock.lock()
243
+ if state.didResume {
244
+ state.lock.unlock()
245
+ return
246
+ }
247
+ state.didResume = true
248
+ state.lock.unlock()
249
+
250
+ if let isCancelled = (info?[PHImageCancelledKey] as? NSNumber)?.boolValue, isCancelled {
251
+ continuation.resume(
252
+ throwing: thumbnailError(
253
+ code: 8,
254
+ message: "Photo library asset request was cancelled for \(url)"
255
+ )
256
+ )
257
+ return
258
+ }
259
+
260
+ if let error = info?[PHImageErrorKey] as? Error {
261
+ continuation.resume(
262
+ throwing: thumbnailError(
263
+ error,
264
+ code: 9,
265
+ message: "Photo library asset request failed for \(url)"
266
+ )
267
+ )
268
+ return
269
+ }
270
+
271
+ guard let avAsset else {
272
+ continuation.resume(
273
+ throwing: thumbnailError(
274
+ code: 10,
275
+ message: "Failed to resolve photo library asset for \(url)"
276
+ )
277
+ )
278
+ return
279
+ }
280
+
281
+ continuation.resume(returning: avAsset)
282
+ }
283
+
284
+ state.lock.lock()
285
+ state.requestID = requestID
286
+ state.lock.unlock()
287
+ }
288
+ }, onCancel: {
289
+ state.lock.lock()
290
+ let requestID = state.requestID
291
+ state.lock.unlock()
292
+
293
+ if requestID != PHInvalidImageRequestID {
294
+ imageManager.cancelImageRequest(requestID)
295
+ }
296
+ })
297
+ }
298
+
299
+ private static func photoLibraryIdentifier(from url: String) -> String {
300
+ guard let parsedURL = URL(string: url), parsedURL.scheme?.lowercased() == "ph" else {
301
+ return url
302
+ .replacingOccurrences(of: "ph://", with: "", options: [.caseInsensitive])
303
+ .removingPercentEncoding?
304
+ .trimmingCharacters(in: CharacterSet(charactersIn: "/")) ?? ""
305
+ }
306
+
307
+ let host = parsedURL.host ?? ""
308
+ let path = parsedURL.path
309
+ let combined = host.isEmpty ? path : "\(host)\(path)"
310
+ return combined.removingPercentEncoding?
311
+ .trimmingCharacters(in: CharacterSet(charactersIn: "/")) ?? ""
312
+ }
313
+
314
+ private static func normalizeLocalURL(_ url: String) -> URL? {
315
+ if let parsedURL = URL(string: url), let scheme = parsedURL.scheme?.lowercased() {
316
+ if scheme == "file" {
317
+ return parsedURL
318
+ }
319
+
320
+ return nil
321
+ }
322
+
323
+ return URL(fileURLWithPath: url)
324
+ }
325
+
326
+ private static func thumbnailError(
327
+ _ error: Error? = nil,
328
+ code: Int,
329
+ message: String
330
+ ) -> Error {
331
+ let description = error.map { "\(message): \($0.localizedDescription)" } ?? message
332
+ return NSError(
333
+ domain: "StreamVideoThumbnail",
334
+ code: code,
335
+ userInfo: [NSLocalizedDescriptionKey: description]
336
+ )
337
+ }
338
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stream-chat-react-native",
3
3
  "description": "The official React Native SDK for Stream Chat, a service for building chat applications",
4
- "version": "9.0.0-beta.3",
4
+ "version": "9.0.0-beta.30",
5
5
  "homepage": "https://www.npmjs.com/package/stream-chat-react-native",
6
6
  "author": {
7
7
  "company": "Stream.io Inc",
@@ -21,6 +21,7 @@
21
21
  "android/gradle",
22
22
  "ios",
23
23
  "*.podspec",
24
+ "react-native.config.js",
24
25
  "package.json"
25
26
  ],
26
27
  "license": "SEE LICENSE IN LICENSE",
@@ -29,7 +30,7 @@
29
30
  "dependencies": {
30
31
  "es6-symbol": "^3.1.3",
31
32
  "mime": "^4.0.7",
32
- "stream-chat-react-native-core": "9.0.0-beta.3"
33
+ "stream-chat-react-native-core": "9.0.0-beta.30"
33
34
  },
34
35
  "peerDependencies": {
35
36
  "@react-native-camera-roll/camera-roll": ">=7.8.0",
@@ -81,7 +82,6 @@
81
82
  }
82
83
  },
83
84
  "scripts": {
84
- "postinstall": "if [ -f ../scripts/sync-shared-native.sh ] && [ -d ../shared-native/ios ]; then bash ../scripts/sync-shared-native.sh native-package; fi",
85
85
  "prepack": "bash ../scripts/sync-shared-native.sh native-package && cp ../../README.md .",
86
86
  "postpack": "rm README.md && bash ../scripts/clean-shared-native-copies.sh native-package"
87
87
  },
@@ -92,7 +92,14 @@
92
92
  "name": "StreamChatReactNativeSpec",
93
93
  "type": "all",
94
94
  "jsSrcsDir": "src/native",
95
+ "android": {
96
+ "javaPackageName": "com.streamchatreactnative"
97
+ },
95
98
  "ios": {
99
+ "modulesProvider": {
100
+ "StreamChatReactNative": "StreamChatReactNative",
101
+ "StreamVideoThumbnail": "StreamVideoThumbnail"
102
+ },
96
103
  "componentProvider": {
97
104
  "StreamShimmerView": "StreamShimmerViewComponentView"
98
105
  }
@@ -0,0 +1,13 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ android: {
5
+ packageImportPath: 'import com.streamchatreactnative.StreamChatReactNativePackage;',
6
+ packageInstance: 'new StreamChatReactNativePackage()',
7
+ },
8
+ ios: {
9
+ podspecPath: 'stream-chat-react-native.podspec',
10
+ },
11
+ },
12
+ },
13
+ };
@@ -22,7 +22,6 @@ export const compressImage = async ({
22
22
  Math.min(Math.max(0, compressImageQuality), 1) * 100,
23
23
  0,
24
24
  undefined,
25
- false,
26
25
  { mode: 'cover' },
27
26
  );
28
27
  return compressedUri;