stream-chat-react-native 8.13.7 → 9.0.0-beta.10
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/android/build.gradle +55 -1
- package/android/src/main/java/com/streamchatreactnative/StreamChatReactNative.java +0 -153
- package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativeModule.java +2 -15
- package/android/src/main/java/com/streamchatreactnative/StreamChatReactNativePackage.java +8 -1
- package/android/src/main/java/com/streamchatreactnative/shared/StreamShimmerFrameLayout.kt +260 -0
- package/android/src/main/java/com/streamchatreactnative/shared/StreamShimmerViewManager.kt +85 -0
- package/android/src/oldarch/com/streamchatreactnative/StreamChatReactNative.java +1 -1
- package/ios/StreamChatReactNative.mm +16 -148
- package/ios/shared/StreamShimmerView.swift +249 -0
- package/ios/shared/StreamShimmerViewComponentView.h +22 -0
- package/ios/shared/StreamShimmerViewComponentView.mm +108 -0
- package/package.json +16 -7
- package/src/handlers/compressImage.ts +0 -1
- package/src/index.js +2 -0
- package/src/native/NativeStreamChatReactNative.ts +0 -1
- package/src/native/StreamShimmerViewNativeComponent.ts +15 -0
- package/src/native/index.tsx +0 -2
- package/src/optionalDependencies/NativeShimmerView.ts +3 -0
- package/src/optionalDependencies/Video.tsx +14 -1
- package/src/optionalDependencies/index.ts +1 -0
- package/stream-chat-react-native.podspec +10 -27
|
@@ -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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
70
|
+
bool saveImage(NSString * fullPath, UIImage * image, NSString * format, float quality)
|
|
73
71
|
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
|
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,249 @@
|
|
|
1
|
+
import QuartzCore
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/// Native shimmer view used by the Fabric component view.
|
|
5
|
+
///
|
|
6
|
+
/// It renders a base layer and a moving gradient highlight entirely in native code, so shimmer
|
|
7
|
+
/// animation stays off the JS thread. The view updates its gradient when size or colors change and
|
|
8
|
+
/// stops animation when it is not drawable (backgrounded, detached, hidden, or zero sized).
|
|
9
|
+
@objcMembers
|
|
10
|
+
public final class StreamShimmerView: UIView {
|
|
11
|
+
private static let edgeHighlightAlpha: CGFloat = 0.1
|
|
12
|
+
private static let softHighlightAlpha: CGFloat = 0.24
|
|
13
|
+
private static let midHighlightAlpha: CGFloat = 0.48
|
|
14
|
+
private static let innerHighlightAlpha: CGFloat = 0.72
|
|
15
|
+
private static let defaultHighlightAlpha: CGFloat = 0.35
|
|
16
|
+
private static let defaultShimmerDuration: CFTimeInterval = 1.2
|
|
17
|
+
private static let shimmerStripWidthRatio: CGFloat = 1.25
|
|
18
|
+
private static let shimmerAnimationKey = "stream_shimmer_translate_x"
|
|
19
|
+
|
|
20
|
+
private let baseLayer = CALayer()
|
|
21
|
+
private let shimmerLayer = CAGradientLayer()
|
|
22
|
+
|
|
23
|
+
private var baseColor: UIColor = UIColor(white: 1, alpha: 0)
|
|
24
|
+
private var gradientColor: UIColor = UIColor(white: 1, alpha: defaultHighlightAlpha)
|
|
25
|
+
private var enabled = false
|
|
26
|
+
private var shimmerDuration: CFTimeInterval = defaultShimmerDuration
|
|
27
|
+
private var lastAnimatedDuration: CFTimeInterval = 0
|
|
28
|
+
private var lastAnimatedSize: CGSize = .zero
|
|
29
|
+
private var isAppActive = true
|
|
30
|
+
|
|
31
|
+
public override init(frame: CGRect) {
|
|
32
|
+
super.init(frame: frame)
|
|
33
|
+
setupLayers()
|
|
34
|
+
setupLifecycleObservers()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public required init?(coder: NSCoder) {
|
|
38
|
+
super.init(coder: coder)
|
|
39
|
+
setupLayers()
|
|
40
|
+
setupLifecycleObservers()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
deinit {
|
|
44
|
+
NotificationCenter.default.removeObserver(self)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public override func layoutSubviews() {
|
|
48
|
+
super.layoutSubviews()
|
|
49
|
+
updateLayersForCurrentState()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public override func didMoveToWindow() {
|
|
53
|
+
super.didMoveToWindow()
|
|
54
|
+
if window == nil {
|
|
55
|
+
// Detaching from window means this view is no longer drawable. Stop and clear animation so
|
|
56
|
+
// a later reattach starts from a clean state.
|
|
57
|
+
stopAnimation()
|
|
58
|
+
} else {
|
|
59
|
+
// Reattaching (including reparenting across windows) re-evaluates state and restarts only
|
|
60
|
+
// when needed by current bounds/visibility/enablement.
|
|
61
|
+
updateLayersForCurrentState()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
66
|
+
super.traitCollectionDidChange(previousTraitCollection)
|
|
67
|
+
if let previousTraitCollection,
|
|
68
|
+
traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection)
|
|
69
|
+
{
|
|
70
|
+
// In current usage, colors are typically driven by JS props. We still refresh on trait
|
|
71
|
+
// changes so dynamically resolved native colors remain correct if that path is used later.
|
|
72
|
+
updateLayersForCurrentState()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public func apply(
|
|
77
|
+
baseColor: UIColor,
|
|
78
|
+
gradientColor: UIColor,
|
|
79
|
+
durationMilliseconds: Double,
|
|
80
|
+
enabled: Bool
|
|
81
|
+
) {
|
|
82
|
+
self.baseColor = baseColor
|
|
83
|
+
self.gradientColor = gradientColor
|
|
84
|
+
shimmerDuration = Self.normalizedDuration(milliseconds: durationMilliseconds)
|
|
85
|
+
self.enabled = enabled
|
|
86
|
+
updateLayersForCurrentState()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public func stopAnimation() {
|
|
90
|
+
shimmerLayer.removeAnimation(forKey: Self.shimmerAnimationKey)
|
|
91
|
+
lastAnimatedDuration = 0
|
|
92
|
+
lastAnimatedSize = .zero
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private func setupLayers() {
|
|
96
|
+
isUserInteractionEnabled = false
|
|
97
|
+
|
|
98
|
+
shimmerLayer.contentsScale = UIScreen.main.scale
|
|
99
|
+
shimmerLayer.allowsEdgeAntialiasing = true
|
|
100
|
+
shimmerLayer.startPoint = CGPoint(x: 0, y: 0.5)
|
|
101
|
+
shimmerLayer.endPoint = CGPoint(x: 1, y: 0.5)
|
|
102
|
+
shimmerLayer.locations = [0.0, 0.08, 0.2, 0.32, 0.4, 0.5, 0.6, 0.68, 0.8, 0.92, 1.0]
|
|
103
|
+
|
|
104
|
+
layer.addSublayer(baseLayer)
|
|
105
|
+
layer.addSublayer(shimmerLayer)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private func setupLifecycleObservers() {
|
|
109
|
+
NotificationCenter.default.addObserver(
|
|
110
|
+
self,
|
|
111
|
+
selector: #selector(handleWillEnterForeground),
|
|
112
|
+
name: UIApplication.willEnterForegroundNotification,
|
|
113
|
+
object: nil
|
|
114
|
+
)
|
|
115
|
+
NotificationCenter.default.addObserver(
|
|
116
|
+
self,
|
|
117
|
+
selector: #selector(handleDidEnterBackground),
|
|
118
|
+
name: UIApplication.didEnterBackgroundNotification,
|
|
119
|
+
object: nil
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@objc
|
|
124
|
+
private func handleWillEnterForeground() {
|
|
125
|
+
// iOS can drop active layer animations while the app is backgrounded. We explicitly rerun
|
|
126
|
+
// a state update on foreground so shimmer reliably restarts when returning to the app.
|
|
127
|
+
isAppActive = true
|
|
128
|
+
updateLayersForCurrentState()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@objc
|
|
132
|
+
private func handleDidEnterBackground() {
|
|
133
|
+
isAppActive = false
|
|
134
|
+
stopAnimation()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private func updateLayersForCurrentState() {
|
|
138
|
+
let bounds = self.bounds
|
|
139
|
+
guard !bounds.isEmpty else {
|
|
140
|
+
stopAnimation()
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
baseLayer.frame = bounds
|
|
145
|
+
baseLayer.backgroundColor = baseColor.cgColor
|
|
146
|
+
|
|
147
|
+
updateShimmerLayer(for: bounds)
|
|
148
|
+
updateShimmerAnimation(for: bounds)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private func updateShimmerLayer(for bounds: CGRect) {
|
|
152
|
+
// Rebuild the shimmer gradient for current width/colors. Keep this tied to real state changes
|
|
153
|
+
// such as layout/prop updates, not continuous per frame calls.
|
|
154
|
+
let shimmerWidth = max(bounds.width * Self.shimmerStripWidthRatio, 1)
|
|
155
|
+
let transparentHighlight = color(gradientColor, alphaFactor: 0)
|
|
156
|
+
shimmerLayer.frame = CGRect(x: -shimmerWidth, y: 0, width: shimmerWidth, height: bounds.height)
|
|
157
|
+
shimmerLayer.colors = [
|
|
158
|
+
transparentHighlight.cgColor,
|
|
159
|
+
color(gradientColor, alphaFactor: Self.edgeHighlightAlpha).cgColor,
|
|
160
|
+
color(gradientColor, alphaFactor: Self.softHighlightAlpha).cgColor,
|
|
161
|
+
color(gradientColor, alphaFactor: Self.midHighlightAlpha).cgColor,
|
|
162
|
+
color(gradientColor, alphaFactor: Self.innerHighlightAlpha).cgColor,
|
|
163
|
+
gradientColor.cgColor,
|
|
164
|
+
color(gradientColor, alphaFactor: Self.innerHighlightAlpha).cgColor,
|
|
165
|
+
color(gradientColor, alphaFactor: Self.midHighlightAlpha).cgColor,
|
|
166
|
+
color(gradientColor, alphaFactor: Self.softHighlightAlpha).cgColor,
|
|
167
|
+
color(gradientColor, alphaFactor: Self.edgeHighlightAlpha).cgColor,
|
|
168
|
+
transparentHighlight.cgColor,
|
|
169
|
+
]
|
|
170
|
+
shimmerLayer.isHidden = !enabled
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private func updateShimmerAnimation(for bounds: CGRect) {
|
|
174
|
+
guard enabled, isAppActive, window != nil, bounds.width > 0, bounds.height > 0 else {
|
|
175
|
+
stopAnimation()
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// If an animation already exists for the same size, keep it running instead of restarting.
|
|
180
|
+
if shimmerLayer.animation(forKey: Self.shimmerAnimationKey) != nil,
|
|
181
|
+
lastAnimatedSize == bounds.size,
|
|
182
|
+
lastAnimatedDuration == shimmerDuration
|
|
183
|
+
{
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
stopAnimation()
|
|
188
|
+
|
|
189
|
+
// Start just outside the left edge and sweep fully past the right edge for a clean pass.
|
|
190
|
+
let shimmerWidth = max(bounds.width * Self.shimmerStripWidthRatio, 1)
|
|
191
|
+
let animation = CABasicAnimation(keyPath: "transform.translation.x")
|
|
192
|
+
animation.fromValue = 0
|
|
193
|
+
animation.toValue = bounds.width + shimmerWidth
|
|
194
|
+
animation.duration = shimmerDuration
|
|
195
|
+
animation.repeatCount = .infinity
|
|
196
|
+
animation.timingFunction = CAMediaTimingFunction(name: .linear)
|
|
197
|
+
animation.isRemovedOnCompletion = true
|
|
198
|
+
shimmerLayer.add(animation, forKey: Self.shimmerAnimationKey)
|
|
199
|
+
lastAnimatedDuration = shimmerDuration
|
|
200
|
+
lastAnimatedSize = bounds.size
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private static func normalizedDuration(milliseconds: Double) -> CFTimeInterval {
|
|
204
|
+
guard milliseconds > 0 else { return defaultShimmerDuration }
|
|
205
|
+
return milliseconds / 1000
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private func color(_ color: UIColor, alphaFactor: CGFloat) -> UIColor {
|
|
209
|
+
// Preserve the resolved color channels and shape only alpha for smooth highlight falloff.
|
|
210
|
+
let resolvedColor = color.resolvedColor(with: traitCollection)
|
|
211
|
+
|
|
212
|
+
var red: CGFloat = 0
|
|
213
|
+
var green: CGFloat = 0
|
|
214
|
+
var blue: CGFloat = 0
|
|
215
|
+
var alpha: CGFloat = 0
|
|
216
|
+
|
|
217
|
+
if resolvedColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha) {
|
|
218
|
+
return UIColor(red: red, green: green, blue: blue, alpha: alpha * alphaFactor)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
guard
|
|
222
|
+
let converted = resolvedColor.cgColor.converted(
|
|
223
|
+
to: CGColorSpace(name: CGColorSpace.extendedSRGB)!,
|
|
224
|
+
intent: .defaultIntent,
|
|
225
|
+
options: nil
|
|
226
|
+
),
|
|
227
|
+
let components = converted.components
|
|
228
|
+
else {
|
|
229
|
+
return resolvedColor.withAlphaComponent(resolvedColor.cgColor.alpha * alphaFactor)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
switch components.count {
|
|
233
|
+
case 2:
|
|
234
|
+
return UIColor(
|
|
235
|
+
white: components[0],
|
|
236
|
+
alpha: components[1] * alphaFactor
|
|
237
|
+
)
|
|
238
|
+
case 4:
|
|
239
|
+
return UIColor(
|
|
240
|
+
red: components[0],
|
|
241
|
+
green: components[1],
|
|
242
|
+
blue: components[2],
|
|
243
|
+
alpha: components[3] * alphaFactor
|
|
244
|
+
)
|
|
245
|
+
default:
|
|
246
|
+
return resolvedColor.withAlphaComponent(resolvedColor.cgColor.alpha * alphaFactor)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#import <UIKit/UIKit.h>
|
|
2
|
+
|
|
3
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
4
|
+
#import <React/RCTViewComponentView.h>
|
|
5
|
+
#endif
|
|
6
|
+
|
|
7
|
+
#ifndef StreamShimmerViewComponentView_h
|
|
8
|
+
#define StreamShimmerViewComponentView_h
|
|
9
|
+
|
|
10
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
11
|
+
|
|
12
|
+
@interface StreamShimmerViewComponentView :
|
|
13
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
14
|
+
RCTViewComponentView
|
|
15
|
+
#else
|
|
16
|
+
UIView
|
|
17
|
+
#endif
|
|
18
|
+
@end
|
|
19
|
+
|
|
20
|
+
NS_ASSUME_NONNULL_END
|
|
21
|
+
|
|
22
|
+
#endif /* StreamShimmerViewComponentView_h */
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#import "StreamShimmerViewComponentView.h"
|
|
2
|
+
|
|
3
|
+
#ifdef RCT_NEW_ARCH_ENABLED
|
|
4
|
+
|
|
5
|
+
#if __has_include(<react/renderer/components/StreamChatReactNativeSpec/ComponentDescriptors.h>)
|
|
6
|
+
#import <react/renderer/components/StreamChatReactNativeSpec/ComponentDescriptors.h>
|
|
7
|
+
#import <react/renderer/components/StreamChatReactNativeSpec/Props.h>
|
|
8
|
+
#import <react/renderer/components/StreamChatReactNativeSpec/RCTComponentViewHelpers.h>
|
|
9
|
+
#elif __has_include(<react/renderer/components/StreamChatExpoSpec/ComponentDescriptors.h>)
|
|
10
|
+
#import <react/renderer/components/StreamChatExpoSpec/ComponentDescriptors.h>
|
|
11
|
+
#import <react/renderer/components/StreamChatExpoSpec/Props.h>
|
|
12
|
+
#import <react/renderer/components/StreamChatExpoSpec/RCTComponentViewHelpers.h>
|
|
13
|
+
#else
|
|
14
|
+
#error "Unable to find generated codegen headers for StreamShimmerView."
|
|
15
|
+
#endif
|
|
16
|
+
|
|
17
|
+
#if __has_include(<stream_chat_react_native/stream_chat_react_native-Swift.h>)
|
|
18
|
+
#import <stream_chat_react_native/stream_chat_react_native-Swift.h>
|
|
19
|
+
#elif __has_include(<stream_chat_expo/stream_chat_expo-Swift.h>)
|
|
20
|
+
#import <stream_chat_expo/stream_chat_expo-Swift.h>
|
|
21
|
+
#elif __has_include("stream_chat_react_native-Swift.h")
|
|
22
|
+
#import "stream_chat_react_native-Swift.h"
|
|
23
|
+
#elif __has_include("stream_chat_expo-Swift.h")
|
|
24
|
+
#import "stream_chat_expo-Swift.h"
|
|
25
|
+
#else
|
|
26
|
+
#error "Unable to import generated Swift header for StreamShimmerView."
|
|
27
|
+
#endif
|
|
28
|
+
|
|
29
|
+
#import <React/RCTConversions.h>
|
|
30
|
+
|
|
31
|
+
using namespace facebook::react;
|
|
32
|
+
|
|
33
|
+
@interface StreamShimmerViewComponentView () <RCTStreamShimmerViewViewProtocol>
|
|
34
|
+
@end
|
|
35
|
+
|
|
36
|
+
// Fabric bridge for StreamShimmerView. This component view owns the native shimmer instance,
|
|
37
|
+
// applies codegen props, and keeps shimmer rendered as a background layer while Fabric manages
|
|
38
|
+
// React children. Keeping shimmer as a layer avoids child-order conflicts during mount/unmount.
|
|
39
|
+
@implementation StreamShimmerViewComponentView {
|
|
40
|
+
StreamShimmerView *_shimmerView;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider
|
|
44
|
+
{
|
|
45
|
+
return concreteComponentDescriptorProvider<StreamShimmerViewComponentDescriptor>();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
- (instancetype)initWithFrame:(CGRect)frame
|
|
49
|
+
{
|
|
50
|
+
if (self = [super initWithFrame:frame]) {
|
|
51
|
+
static const auto defaultProps = std::make_shared<const StreamShimmerViewProps>();
|
|
52
|
+
_props = defaultProps;
|
|
53
|
+
|
|
54
|
+
_shimmerView = [[StreamShimmerView alloc] initWithFrame:self.bounds];
|
|
55
|
+
_shimmerView.userInteractionEnabled = NO;
|
|
56
|
+
[self.layer insertSublayer:_shimmerView.layer atIndex:0];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return self;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
- (void)layoutSubviews
|
|
63
|
+
{
|
|
64
|
+
[super layoutSubviews];
|
|
65
|
+
_shimmerView.frame = self.bounds;
|
|
66
|
+
|
|
67
|
+
// Keep shimmer pinned as the layer furthest back. Some layer operations can reorder sublayers, and
|
|
68
|
+
// this guard restores expected layering without touching Fabric managed child views.
|
|
69
|
+
BOOL needsReinsert = _shimmerView.layer.superlayer != self.layer;
|
|
70
|
+
if (!needsReinsert) {
|
|
71
|
+
CALayer *firstLayer = self.layer.sublayers.firstObject;
|
|
72
|
+
needsReinsert = firstLayer != _shimmerView.layer;
|
|
73
|
+
}
|
|
74
|
+
if (needsReinsert) {
|
|
75
|
+
[self.layer insertSublayer:_shimmerView.layer atIndex:0];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
- (void)updateProps:(Props::Shared const &)props oldProps:(Props::Shared const &)oldProps
|
|
80
|
+
{
|
|
81
|
+
const auto &newProps = *std::static_pointer_cast<const StreamShimmerViewProps>(props);
|
|
82
|
+
|
|
83
|
+
UIColor *baseColor = RCTUIColorFromSharedColor(newProps.baseColor) ?: [UIColor colorWithWhite:1 alpha:0];
|
|
84
|
+
UIColor *gradientColor = RCTUIColorFromSharedColor(newProps.gradientColor) ?: [UIColor whiteColor];
|
|
85
|
+
|
|
86
|
+
[_shimmerView applyWithBaseColor:baseColor
|
|
87
|
+
gradientColor:gradientColor
|
|
88
|
+
durationMilliseconds:newProps.duration
|
|
89
|
+
enabled:newProps.enabled];
|
|
90
|
+
|
|
91
|
+
[super updateProps:props oldProps:oldProps];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
- (void)prepareForRecycle
|
|
95
|
+
{
|
|
96
|
+
[super prepareForRecycle];
|
|
97
|
+
// Defensive cleanup for recycled cells/views so offscreen instances do not keep animating.
|
|
98
|
+
[_shimmerView stopAnimation];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
- (void)dealloc
|
|
102
|
+
{
|
|
103
|
+
[_shimmerView stopAnimation];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@end
|
|
107
|
+
|
|
108
|
+
#endif
|
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": "
|
|
4
|
+
"version": "9.0.0-beta.10",
|
|
5
5
|
"homepage": "https://www.npmjs.com/package/stream-chat-react-native",
|
|
6
6
|
"author": {
|
|
7
7
|
"company": "Stream.io Inc",
|
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"src",
|
|
17
17
|
"types",
|
|
18
|
-
"android",
|
|
18
|
+
"android/src",
|
|
19
|
+
"android/build.gradle",
|
|
20
|
+
"android/gradle.properties",
|
|
21
|
+
"android/gradle",
|
|
19
22
|
"ios",
|
|
20
23
|
"*.podspec",
|
|
21
24
|
"package.json"
|
|
@@ -26,7 +29,7 @@
|
|
|
26
29
|
"dependencies": {
|
|
27
30
|
"es6-symbol": "^3.1.3",
|
|
28
31
|
"mime": "^4.0.7",
|
|
29
|
-
"stream-chat-react-native-core": "
|
|
32
|
+
"stream-chat-react-native-core": "9.0.0-beta.10"
|
|
30
33
|
},
|
|
31
34
|
"peerDependencies": {
|
|
32
35
|
"@react-native-camera-roll/camera-roll": ">=7.8.0",
|
|
@@ -78,16 +81,22 @@
|
|
|
78
81
|
}
|
|
79
82
|
},
|
|
80
83
|
"scripts": {
|
|
81
|
-
"
|
|
82
|
-
"
|
|
84
|
+
"postinstall": "if [ -f ../scripts/sync-shared-native.sh ] && [ -d ../shared-native/ios ]; then bash ../scripts/sync-shared-native.sh native-package; fi",
|
|
85
|
+
"prepack": "bash ../scripts/sync-shared-native.sh native-package && cp ../../README.md .",
|
|
86
|
+
"postpack": "rm README.md && bash ../scripts/clean-shared-native-copies.sh native-package"
|
|
83
87
|
},
|
|
84
88
|
"devDependencies": {
|
|
85
89
|
"react-native": "^0.79.3"
|
|
86
90
|
},
|
|
87
91
|
"codegenConfig": {
|
|
88
92
|
"name": "StreamChatReactNativeSpec",
|
|
89
|
-
"type": "
|
|
90
|
-
"jsSrcsDir": "src/native"
|
|
93
|
+
"type": "all",
|
|
94
|
+
"jsSrcsDir": "src/native",
|
|
95
|
+
"ios": {
|
|
96
|
+
"componentProvider": {
|
|
97
|
+
"StreamShimmerView": "StreamShimmerViewComponentView"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
91
100
|
},
|
|
92
101
|
"resolutions": {
|
|
93
102
|
"@types/react": "^19.0.0"
|
package/src/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
getLocalAssetUri,
|
|
12
12
|
getPhotos,
|
|
13
13
|
iOS14RefreshGallerySelection,
|
|
14
|
+
NativeShimmerView,
|
|
14
15
|
oniOS14GalleryLibrarySelectionChange,
|
|
15
16
|
overrideAudioRecordingConfiguration,
|
|
16
17
|
pickDocument,
|
|
@@ -32,6 +33,7 @@ registerNativeHandlers({
|
|
|
32
33
|
getLocalAssetUri,
|
|
33
34
|
getPhotos,
|
|
34
35
|
iOS14RefreshGallerySelection,
|
|
36
|
+
NativeShimmerView,
|
|
35
37
|
oniOS14GalleryLibrarySelectionChange,
|
|
36
38
|
overrideAudioRecordingConfiguration,
|
|
37
39
|
pickDocument,
|