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.
@@ -1,3 +1,8 @@
1
+ def kotlinVersion =
2
+ rootProject.ext.has("kotlinVersion")
3
+ ? rootProject.ext.get("kotlinVersion")
4
+ : project.properties["ImageResizer_kotlinVersion"]
5
+
1
6
  buildscript {
2
7
  repositories {
3
8
  google()
@@ -6,7 +11,7 @@ buildscript {
6
11
 
7
12
  dependencies {
8
13
  classpath "com.android.tools.build:gradle:7.2.1"
9
-
14
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
10
15
  }
11
16
  }
12
17
 
@@ -15,6 +20,7 @@ def isNewArchitectureEnabled() {
15
20
  }
16
21
 
17
22
  apply plugin: "com.android.library"
23
+ apply plugin: "kotlin-android"
18
24
 
19
25
 
20
26
  def appProject = rootProject.allprojects.find { it.plugins.hasPlugin('com.android.application') }
@@ -30,6 +36,11 @@ def getExtOrDefault(name) {
30
36
  def getExtOrIntegerDefault(name) {
31
37
  return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["ImageResizer_" + name]).toInteger()
32
38
  }
39
+ def localSharedNativeRootDir = new File(projectDir, "src/main/java/com/streamchatreactnative/shared")
40
+ def sharedNativeRootDir = new File(projectDir, "../../shared-native/android")
41
+ def hasNativeSources = { File dir ->
42
+ dir.exists() && !fileTree(dir).matching { include "**/*.kt"; include "**/*.java" }.files.isEmpty()
43
+ }
33
44
 
34
45
  android {
35
46
  compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
@@ -65,11 +76,16 @@ android {
65
76
  targetCompatibility JavaVersion.VERSION_1_8
66
77
  }
67
78
 
79
+ kotlinOptions {
80
+ jvmTarget = "17"
81
+ }
82
+
68
83
  sourceSets {
69
84
  main {
70
85
  if (isNewArchitectureEnabled()) {
71
86
  java.srcDirs += [
72
87
  "src/newarch",
88
+ "src/main/java/com/streamchatreactnative/shared",
73
89
  // This is needed to build Kotlin project with NewArch enabled
74
90
  "${project.buildDir}/generated/source/codegen/java"
75
91
  ]
@@ -80,6 +96,44 @@ android {
80
96
  }
81
97
  }
82
98
 
99
+ tasks.register("syncSharedShimmerSources") {
100
+ outputs.dir(localSharedNativeRootDir)
101
+ outputs.upToDateWhen { false }
102
+ doLast {
103
+ def sourceRootDir = null
104
+ if (hasNativeSources(localSharedNativeRootDir)) {
105
+ sourceRootDir = localSharedNativeRootDir
106
+ } else if (hasNativeSources(sharedNativeRootDir)) {
107
+ sourceRootDir = sharedNativeRootDir
108
+ }
109
+
110
+ if (sourceRootDir == null) {
111
+ throw new GradleException(
112
+ "Missing shared native Android sources. Expected either src/main/java/com/streamchatreactnative/shared/**/*.{kt,java} " +
113
+ "or ../../shared-native/android/**/*.{kt,java}."
114
+ )
115
+ }
116
+
117
+ if (sourceRootDir != localSharedNativeRootDir) {
118
+ project.delete(localSharedNativeRootDir)
119
+ project.copy {
120
+ from(sourceRootDir)
121
+ into(localSharedNativeRootDir)
122
+ }
123
+ } else if (!hasNativeSources(localSharedNativeRootDir)) {
124
+ throw new GradleException("Shared native source directory exists but has no Kotlin/Java files.")
125
+ }
126
+ }
127
+ }
128
+
129
+ tasks.matching { it.name == "preBuild" }.configureEach {
130
+ dependsOn("syncSharedShimmerSources")
131
+ }
132
+
133
+ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
134
+ dependsOn("syncSharedShimmerSources")
135
+ }
136
+
83
137
  repositories {
84
138
  mavenCentral()
85
139
  google()
@@ -11,7 +11,6 @@ import android.net.Uri;
11
11
  import android.os.Build;
12
12
  import android.provider.MediaStore;
13
13
  import android.util.Base64;
14
- import android.util.Log;
15
14
 
16
15
  import java.io.ByteArrayOutputStream;
17
16
  import java.io.File;
@@ -33,108 +32,6 @@ public class StreamChatReactNative {
33
32
  private final static String SCHEME_FILE = "file";
34
33
  private final static String SCHEME_HTTP = "http";
35
34
  private final static String SCHEME_HTTPS = "https";
36
-
37
-
38
- // List of known EXIF tags we will be copying.
39
- // Orientation, width, height, and some others are ignored
40
- // TODO: Find any missing tag that might be useful
41
- private final static String[] EXIF_TO_COPY_ROTATED = new String[]
42
- {
43
- ExifInterface.TAG_APERTURE_VALUE,
44
- ExifInterface.TAG_MAX_APERTURE_VALUE,
45
- ExifInterface.TAG_METERING_MODE,
46
- ExifInterface.TAG_ARTIST,
47
- ExifInterface.TAG_BITS_PER_SAMPLE,
48
- ExifInterface.TAG_COMPRESSION,
49
- ExifInterface.TAG_BODY_SERIAL_NUMBER,
50
- ExifInterface.TAG_BRIGHTNESS_VALUE,
51
- ExifInterface.TAG_CONTRAST,
52
- ExifInterface.TAG_CAMERA_OWNER_NAME,
53
- ExifInterface.TAG_COLOR_SPACE,
54
- ExifInterface.TAG_COPYRIGHT,
55
- ExifInterface.TAG_DATETIME,
56
- ExifInterface.TAG_DATETIME_DIGITIZED,
57
- ExifInterface.TAG_DATETIME_ORIGINAL,
58
- ExifInterface.TAG_DEVICE_SETTING_DESCRIPTION,
59
- ExifInterface.TAG_DIGITAL_ZOOM_RATIO,
60
- ExifInterface.TAG_EXIF_VERSION,
61
- ExifInterface.TAG_EXPOSURE_BIAS_VALUE,
62
- ExifInterface.TAG_EXPOSURE_INDEX,
63
- ExifInterface.TAG_EXPOSURE_MODE,
64
- ExifInterface.TAG_EXPOSURE_TIME,
65
- ExifInterface.TAG_EXPOSURE_PROGRAM,
66
- ExifInterface.TAG_FLASH,
67
- ExifInterface.TAG_FLASH_ENERGY,
68
- ExifInterface.TAG_FOCAL_LENGTH,
69
- ExifInterface.TAG_FOCAL_LENGTH_IN_35MM_FILM,
70
- ExifInterface.TAG_FOCAL_PLANE_RESOLUTION_UNIT,
71
- ExifInterface.TAG_FOCAL_PLANE_X_RESOLUTION,
72
- ExifInterface.TAG_FOCAL_PLANE_Y_RESOLUTION,
73
- ExifInterface.TAG_PHOTOMETRIC_INTERPRETATION,
74
- ExifInterface.TAG_PLANAR_CONFIGURATION,
75
- ExifInterface.TAG_F_NUMBER,
76
- ExifInterface.TAG_GAIN_CONTROL,
77
- ExifInterface.TAG_GAMMA,
78
- ExifInterface.TAG_GPS_ALTITUDE,
79
- ExifInterface.TAG_GPS_ALTITUDE_REF,
80
- ExifInterface.TAG_GPS_AREA_INFORMATION,
81
- ExifInterface.TAG_GPS_DATESTAMP,
82
- ExifInterface.TAG_GPS_DOP,
83
- ExifInterface.TAG_GPS_LATITUDE,
84
- ExifInterface.TAG_GPS_LATITUDE_REF,
85
- ExifInterface.TAG_GPS_LONGITUDE,
86
- ExifInterface.TAG_GPS_LONGITUDE_REF,
87
- ExifInterface.TAG_GPS_STATUS,
88
- ExifInterface.TAG_GPS_DEST_BEARING,
89
- ExifInterface.TAG_GPS_DEST_BEARING_REF,
90
- ExifInterface.TAG_GPS_DEST_DISTANCE,
91
- ExifInterface.TAG_GPS_DEST_DISTANCE_REF,
92
- ExifInterface.TAG_GPS_DEST_LATITUDE,
93
- ExifInterface.TAG_GPS_DEST_LATITUDE_REF,
94
- ExifInterface.TAG_GPS_DEST_LONGITUDE,
95
- ExifInterface.TAG_GPS_DEST_LONGITUDE_REF,
96
- ExifInterface.TAG_GPS_DIFFERENTIAL,
97
- ExifInterface.TAG_GPS_IMG_DIRECTION,
98
- ExifInterface.TAG_GPS_IMG_DIRECTION_REF,
99
- ExifInterface.TAG_GPS_MAP_DATUM,
100
- ExifInterface.TAG_GPS_MEASURE_MODE,
101
- ExifInterface.TAG_GPS_PROCESSING_METHOD,
102
- ExifInterface.TAG_GPS_SATELLITES,
103
- ExifInterface.TAG_GPS_SPEED,
104
- ExifInterface.TAG_GPS_SPEED_REF,
105
- ExifInterface.TAG_GPS_STATUS,
106
- ExifInterface.TAG_GPS_TIMESTAMP,
107
- ExifInterface.TAG_GPS_TRACK,
108
- ExifInterface.TAG_GPS_TRACK_REF,
109
- ExifInterface.TAG_GPS_VERSION_ID,
110
- ExifInterface.TAG_IMAGE_DESCRIPTION,
111
- ExifInterface.TAG_IMAGE_UNIQUE_ID,
112
- ExifInterface.TAG_ISO_SPEED,
113
- ExifInterface.TAG_PHOTOGRAPHIC_SENSITIVITY,
114
- ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT,
115
- ExifInterface.TAG_JPEG_INTERCHANGE_FORMAT_LENGTH,
116
- ExifInterface.TAG_LENS_MAKE,
117
- ExifInterface.TAG_LENS_MODEL,
118
- ExifInterface.TAG_LENS_SERIAL_NUMBER,
119
- ExifInterface.TAG_LENS_SPECIFICATION,
120
- ExifInterface.TAG_LIGHT_SOURCE,
121
- ExifInterface.TAG_MAKE,
122
- ExifInterface.TAG_MAKER_NOTE,
123
- ExifInterface.TAG_MODEL,
124
- // ExifInterface.TAG_ORIENTATION, // removed
125
- ExifInterface.TAG_SATURATION,
126
- ExifInterface.TAG_SHARPNESS,
127
- ExifInterface.TAG_SHUTTER_SPEED_VALUE,
128
- ExifInterface.TAG_SOFTWARE,
129
- ExifInterface.TAG_SUBJECT_DISTANCE,
130
- ExifInterface.TAG_SUBJECT_DISTANCE_RANGE,
131
- ExifInterface.TAG_SUBJECT_LOCATION,
132
- ExifInterface.TAG_USER_COMMENT,
133
- ExifInterface.TAG_WHITE_BALANCE
134
- };
135
-
136
-
137
-
138
35
  /**
139
36
  * Resize the specified bitmap.
140
37
  */
@@ -263,55 +160,6 @@ public class StreamChatReactNative {
263
160
  return file;
264
161
  }
265
162
 
266
- /**
267
- * Attempts to copy exif info from one file to another. Note: orientation, width, and height
268
- exif attributes are not copied since those are lost after image rotation.
269
-
270
- * imageUri: original image URI as provided from JS
271
- * dstPath: final image output path
272
- * Returns true if copy was successful, false otherwise.
273
- */
274
- public static boolean copyExif(Context context, Uri imageUri, String dstPath){
275
- ExifInterface src = null;
276
- ExifInterface dst = null;
277
-
278
- try {
279
-
280
- File file = getFileFromUri(context, imageUri);
281
- if (!file.exists()) {
282
- return false;
283
- }
284
-
285
- src = new ExifInterface(file.getAbsolutePath());
286
- dst = new ExifInterface(dstPath);
287
-
288
- } catch (Exception ignored) {
289
- Log.e("StreamChatReactNative::copyExif", "EXIF read failed", ignored);
290
- }
291
-
292
- if(src == null || dst == null){
293
- return false;
294
- }
295
-
296
- try{
297
-
298
- for (String attr : EXIF_TO_COPY_ROTATED)
299
- {
300
- String value = src.getAttribute(attr);
301
- if (value != null){
302
- dst.setAttribute(attr, value);
303
- }
304
- }
305
- dst.saveAttributes();
306
-
307
- } catch (Exception ignored) {
308
- Log.e("StreamChatReactNative::copyExif", "EXIF copy failed", ignored);
309
- return false;
310
- }
311
-
312
- return true;
313
- }
314
-
315
163
  /**
316
164
  * Get orientation by reading Image metadata
317
165
  */
@@ -591,4 +439,3 @@ public class StreamChatReactNative {
591
439
  return scaledImage;
592
440
  }
593
441
  }
594
-
@@ -4,8 +4,6 @@ import android.annotation.SuppressLint;
4
4
  import android.graphics.Bitmap;
5
5
  import android.net.Uri;
6
6
  import android.os.AsyncTask;
7
- import android.util.Log;
8
-
9
7
  import androidx.annotation.Nullable;
10
8
  import androidx.annotation.NonNull;
11
9
 
@@ -36,7 +34,7 @@ public class StreamChatReactNativeModule extends StreamChatReactNativeSpec {
36
34
  }
37
35
 
38
36
  @ReactMethod
39
- public void createResizedImage(String uri, double width, double height, String format, double quality, String mode, boolean onlyScaleDown, Double rotation, @Nullable String outputPath, Boolean keepMeta, Promise promise) {
37
+ public void createResizedImage(String uri, double width, double height, String format, double quality, String mode, boolean onlyScaleDown, Double rotation, @Nullable String outputPath, Promise promise) {
40
38
  WritableMap options = Arguments.createMap();
41
39
  options.putString("mode", mode);
42
40
  options.putBoolean("onlyScaleDown", onlyScaleDown);
@@ -46,7 +44,7 @@ public class StreamChatReactNativeModule extends StreamChatReactNativeSpec {
46
44
  @Override
47
45
  protected void doInBackgroundGuarded(Void... params) {
48
46
  try {
49
- Object response = createResizedImageWithExceptions(uri, (int) width, (int) height, format, (int) quality, rotation.intValue(), outputPath, keepMeta, options);
47
+ Object response = createResizedImageWithExceptions(uri, (int) width, (int) height, format, (int) quality, rotation.intValue(), outputPath, options);
50
48
  promise.resolve(response);
51
49
  }
52
50
  catch (IOException e) {
@@ -59,7 +57,6 @@ public class StreamChatReactNativeModule extends StreamChatReactNativeSpec {
59
57
  @SuppressLint("LongLogTag")
60
58
  private Object createResizedImageWithExceptions(String imagePath, int newWidth, int newHeight,
61
59
  String compressFormatString, int quality, int rotation, String outputPath,
62
- final boolean keepMeta,
63
60
  final ReadableMap options) throws IOException {
64
61
 
65
62
  Bitmap.CompressFormat compressFormat = Bitmap.CompressFormat.valueOf(compressFormatString);
@@ -89,16 +86,6 @@ public class StreamChatReactNativeModule extends StreamChatReactNativeSpec {
89
86
  response.putDouble("size", resizedImage.length());
90
87
  response.putDouble("width", scaledImage.getWidth());
91
88
  response.putDouble("height", scaledImage.getHeight());
92
-
93
- // Copy file's metadata/exif info if required
94
- if(keepMeta){
95
- try{
96
- StreamChatReactNative.copyExif(this.getReactApplicationContext(), imageUri, resizedImage.getAbsolutePath());
97
- }
98
- catch(Exception ignored){
99
- Log.e("StreamChatReactNative::createResizedImageWithExceptions", "EXIF copy failed", ignored);
100
- }
101
- }
102
89
  } else {
103
90
  throw new IOException("Error getting resized image path");
104
91
  }
@@ -6,9 +6,11 @@ import com.facebook.react.bridge.ReactApplicationContext;
6
6
  import com.facebook.react.module.model.ReactModuleInfo;
7
7
  import com.facebook.react.module.model.ReactModuleInfoProvider;
8
8
  import com.facebook.react.TurboReactPackage;
9
-
9
+ import com.facebook.react.uimanager.ViewManager;
10
10
 
11
11
  import java.util.HashMap;
12
+ import java.util.Collections;
13
+ import java.util.List;
12
14
  import java.util.Map;
13
15
 
14
16
  public class StreamChatReactNativePackage extends TurboReactPackage {
@@ -42,4 +44,9 @@ public class StreamChatReactNativePackage extends TurboReactPackage {
42
44
  return moduleInfos;
43
45
  };
44
46
  }
47
+
48
+ @Override
49
+ public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
50
+ return Collections.<ViewManager>singletonList(new StreamShimmerViewManager());
51
+ }
45
52
  }
@@ -0,0 +1,260 @@
1
+ package com.streamchatreactnative
2
+
3
+ import android.animation.ValueAnimator
4
+ import android.content.Context
5
+ import android.graphics.Canvas
6
+ import android.graphics.Color
7
+ import android.graphics.LinearGradient
8
+ import android.graphics.Matrix
9
+ import android.graphics.Paint
10
+ import android.graphics.Shader
11
+ import android.util.AttributeSet
12
+ import android.view.View
13
+ import android.view.animation.LinearInterpolator
14
+ import android.widget.FrameLayout
15
+ import kotlin.math.roundToInt
16
+
17
+ /**
18
+ * Native shimmer container used by `StreamShimmerView`.
19
+ *
20
+ * This view draws a base color plus a moving highlight strip directly on canvas and still behaves
21
+ * like a regular container for React children. The animation runs fully on the native side so it
22
+ * does not depend on JS-driven frame updates. It automatically stops animating when the view is
23
+ * detached or not visible, rebuilds its shader when size or colors change, and waits for valid
24
+ * dimensions before starting animation to avoid invalid draw/animation states.
25
+ */
26
+ class StreamShimmerFrameLayout @JvmOverloads constructor(
27
+ context: Context,
28
+ attrs: AttributeSet? = null,
29
+ ) : FrameLayout(context, attrs) {
30
+ private var baseColor: Int = DEFAULT_BASE_COLOR
31
+ private var durationMs: Long = DEFAULT_DURATION_MS
32
+ private var gradientColor: Int = DEFAULT_GRADIENT_COLOR
33
+ private var enabled: Boolean = true
34
+
35
+ private val basePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
36
+ private val shimmerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
37
+ style = Paint.Style.FILL
38
+ isDither = true
39
+ }
40
+ private val shimmerMatrix = Matrix()
41
+
42
+ private var shimmerShader: LinearGradient? = null
43
+ private var shimmerTranslateX: Float = 0f
44
+ private var animatedDurationMs: Long = 0L
45
+ private var animatedViewWidth: Float = 0f
46
+ private var animator: ValueAnimator? = null
47
+
48
+ init {
49
+ setWillNotDraw(false)
50
+ }
51
+
52
+ fun setBaseColor(color: Int) {
53
+ if (baseColor == color) return
54
+ baseColor = color
55
+ rebuildShimmerShader()
56
+ invalidate()
57
+ }
58
+
59
+ fun setGradientColor(color: Int) {
60
+ if (gradientColor == color) return
61
+ gradientColor = color
62
+ rebuildShimmerShader()
63
+ invalidate()
64
+ }
65
+
66
+ fun setDuration(duration: Int) {
67
+ val normalizedDurationMs =
68
+ if (duration > 0) duration.toLong() else DEFAULT_DURATION_MS
69
+ if (durationMs == normalizedDurationMs) return
70
+ durationMs = normalizedDurationMs
71
+ updateAnimatorState()
72
+ }
73
+
74
+ fun setShimmerEnabled(enabled: Boolean) {
75
+ if (this.enabled == enabled) return
76
+ this.enabled = enabled
77
+ updateAnimatorState()
78
+ invalidate()
79
+ }
80
+
81
+ fun updateAnimatorState() {
82
+ // Centralized lifecycle gate for animation start/stop. This keeps shimmer off for detached or
83
+ // hidden views to avoid wasting UI-thread work in long lists.
84
+ if (shouldAnimateShimmer()) {
85
+ startShimmer()
86
+ } else {
87
+ stopShimmer()
88
+ }
89
+ }
90
+
91
+ override fun onAttachedToWindow() {
92
+ super.onAttachedToWindow()
93
+ // Reattachment (including reparenting) should recheck visibility state and restart only if
94
+ // this instance is eligible to animate.
95
+ updateAnimatorState()
96
+ }
97
+
98
+ override fun onDetachedFromWindow() {
99
+ // Detached views are not drawable; stop and clear animator so a future attach starts cleanly.
100
+ stopShimmer()
101
+ super.onDetachedFromWindow()
102
+ }
103
+
104
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
105
+ super.onSizeChanged(w, h, oldw, oldh)
106
+ rebuildShimmerShader()
107
+ updateAnimatorState()
108
+ }
109
+
110
+ override fun onWindowVisibilityChanged(visibility: Int) {
111
+ super.onWindowVisibilityChanged(visibility)
112
+ updateAnimatorState()
113
+ }
114
+
115
+ override fun onVisibilityChanged(changedView: View, visibility: Int) {
116
+ super.onVisibilityChanged(changedView, visibility)
117
+ if (changedView === this) {
118
+ updateAnimatorState()
119
+ }
120
+ }
121
+
122
+ override fun dispatchDraw(canvas: Canvas) {
123
+ val viewWidth = width.toFloat()
124
+ val viewHeight = height.toFloat()
125
+ if (viewWidth <= 0f || viewHeight <= 0f) {
126
+ super.dispatchDraw(canvas)
127
+ return
128
+ }
129
+
130
+ basePaint.color = baseColor
131
+ canvas.drawRect(0f, 0f, viewWidth, viewHeight, basePaint)
132
+
133
+ drawShimmer(canvas, viewWidth, viewHeight)
134
+ super.dispatchDraw(canvas)
135
+ }
136
+
137
+ private fun drawShimmer(canvas: Canvas, viewWidth: Float, viewHeight: Float) {
138
+ if (!enabled) return
139
+
140
+ val shader = shimmerShader ?: return
141
+
142
+ shimmerMatrix.setTranslate(shimmerTranslateX, 0f)
143
+ shader.setLocalMatrix(shimmerMatrix)
144
+ shimmerPaint.shader = shader
145
+ canvas.drawRect(0f, 0f, viewWidth, viewHeight, shimmerPaint)
146
+ shimmerPaint.shader = null
147
+ }
148
+
149
+ private fun rebuildShimmerShader() {
150
+ // Recreates the shimmer gradient for the current width/colors. This allocates shader state,
151
+ // so keep calls tied to real changes (size or color updates), not per frame execution.
152
+ val viewWidth = width.toFloat()
153
+ if (viewWidth <= 0f) {
154
+ shimmerShader = null
155
+ return
156
+ }
157
+
158
+ // Wide multi-stop strip creates a softer "glassy" sweep and avoids the hard thin-line look.
159
+ val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f)
160
+ val transparentHighlight = colorWithAlpha(gradientColor, 0f)
161
+ val edgeBase = colorWithAlpha(gradientColor, EDGE_HIGHLIGHT_ALPHA_FACTOR)
162
+ val softBase = colorWithAlpha(gradientColor, SOFT_HIGHLIGHT_ALPHA_FACTOR)
163
+ val mediumBase = colorWithAlpha(gradientColor, MID_HIGHLIGHT_ALPHA_FACTOR)
164
+ val innerBase = colorWithAlpha(gradientColor, INNER_HIGHLIGHT_ALPHA_FACTOR)
165
+ shimmerShader = LinearGradient(
166
+ 0f,
167
+ 0f,
168
+ shimmerWidth,
169
+ 0f,
170
+ intArrayOf(
171
+ transparentHighlight,
172
+ edgeBase,
173
+ softBase,
174
+ mediumBase,
175
+ innerBase,
176
+ gradientColor,
177
+ innerBase,
178
+ mediumBase,
179
+ softBase,
180
+ edgeBase,
181
+ transparentHighlight,
182
+ ),
183
+ floatArrayOf(
184
+ 0f,
185
+ 0.08f,
186
+ 0.2f,
187
+ 0.32f,
188
+ 0.4f,
189
+ 0.5f,
190
+ 0.6f,
191
+ 0.68f,
192
+ 0.8f,
193
+ 0.92f,
194
+ 1f,
195
+ ),
196
+ Shader.TileMode.CLAMP,
197
+ )
198
+ }
199
+
200
+ private fun startShimmer() {
201
+ val viewWidth = width.toFloat()
202
+ if (viewWidth <= 0f) return
203
+ // Keep the existing animator only when size and duration still match the current request.
204
+ if (animator != null && animatedViewWidth == viewWidth && animatedDurationMs == durationMs) return
205
+
206
+ stopShimmer()
207
+
208
+ // Animate from fully offscreen left to fully offscreen right so the strip enters/exits cleanly.
209
+ val shimmerWidth = (viewWidth * SHIMMER_STRIP_WIDTH_RATIO).coerceAtLeast(1f)
210
+ animatedViewWidth = viewWidth
211
+ animatedDurationMs = durationMs
212
+ animator = ValueAnimator.ofFloat(-shimmerWidth, viewWidth).apply {
213
+ duration = durationMs
214
+ repeatCount = ValueAnimator.INFINITE
215
+ interpolator = LinearInterpolator()
216
+ addUpdateListener {
217
+ shimmerTranslateX = it.animatedValue as Float
218
+ invalidate()
219
+ }
220
+ start()
221
+ }
222
+ }
223
+
224
+ private fun stopShimmer() {
225
+ animator?.cancel()
226
+ animator = null
227
+ animatedDurationMs = 0L
228
+ animatedViewWidth = 0f
229
+ }
230
+
231
+ private fun shouldAnimateShimmer(): Boolean {
232
+ // `isShown` and explicit visibility/window checks cover different hide paths in nested
233
+ // hierarchies. Keeping them all prevents animations running when not visible to the user.
234
+ return enabled &&
235
+ isAttachedToWindow &&
236
+ width > 0 &&
237
+ height > 0 &&
238
+ visibility == View.VISIBLE &&
239
+ windowVisibility == View.VISIBLE &&
240
+ isShown &&
241
+ alpha > 0f
242
+ }
243
+
244
+ private fun colorWithAlpha(color: Int, alphaFactor: Float): Int {
245
+ // Preserve RGB while shaping only alpha; used for symmetric highlight falloff in gradient stops.
246
+ val alpha = (Color.alpha(color) * alphaFactor).roundToInt().coerceIn(0, 255)
247
+ return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))
248
+ }
249
+
250
+ companion object {
251
+ private const val DEFAULT_BASE_COLOR = 0x00FFFFFF
252
+ private const val DEFAULT_DURATION_MS = 1200L
253
+ private const val DEFAULT_GRADIENT_COLOR = 0x59FFFFFF
254
+ private const val SHIMMER_STRIP_WIDTH_RATIO = 1.25f
255
+ private const val EDGE_HIGHLIGHT_ALPHA_FACTOR = 0.1f
256
+ private const val SOFT_HIGHLIGHT_ALPHA_FACTOR = 0.24f
257
+ private const val MID_HIGHLIGHT_ALPHA_FACTOR = 0.48f
258
+ private const val INNER_HIGHLIGHT_ALPHA_FACTOR = 0.72f
259
+ }
260
+ }
@@ -0,0 +1,85 @@
1
+ package com.streamchatreactnative
2
+
3
+ import androidx.annotation.NonNull
4
+ import com.facebook.react.uimanager.ViewManagerDelegate
5
+ import com.facebook.react.uimanager.ThemedReactContext
6
+ import com.facebook.react.uimanager.ViewGroupManager
7
+ import com.facebook.react.viewmanagers.StreamShimmerViewManagerDelegate
8
+ import com.facebook.react.viewmanagers.StreamShimmerViewManagerInterface
9
+
10
+ /**
11
+ * Fabric manager for StreamShimmerView.
12
+ *
13
+ * It creates the native shimmer layout, maps React props to native setters, and exposes child
14
+ * management methods so Fabric can mount and unmount children correctly inside this container.
15
+ * The manager rechecks animation state after prop transactions and disables shimmer when a view
16
+ * instance is dropped as a defensive cleanup step for recycled or unmounted views. Because the
17
+ * shimmer view wraps React children, this must remain a real ViewGroupManager as using a non-group
18
+ * manager can fail in Fabric mounting paths at runtime.
19
+ */
20
+ class StreamShimmerViewManager : ViewGroupManager<StreamShimmerFrameLayout>(),
21
+ StreamShimmerViewManagerInterface<StreamShimmerFrameLayout> {
22
+ private val delegate = StreamShimmerViewManagerDelegate(this)
23
+
24
+ override fun getName(): String = REACT_CLASS
25
+
26
+ @NonNull
27
+ override fun createViewInstance(@NonNull reactContext: ThemedReactContext): StreamShimmerFrameLayout {
28
+ val layout = StreamShimmerFrameLayout(reactContext)
29
+ layout.updateAnimatorState()
30
+ return layout
31
+ }
32
+
33
+ override fun onAfterUpdateTransaction(@NonNull view: StreamShimmerFrameLayout) {
34
+ super.onAfterUpdateTransaction(view)
35
+ // Prop batches can change visibility/enabled/colors together, so we re-evaluate the animator once
36
+ // after every transaction to keep state consistent and avoid duplicate start/stop churn.
37
+ view.updateAnimatorState()
38
+ }
39
+
40
+ override fun addView(parent: StreamShimmerFrameLayout, child: android.view.View, index: Int) {
41
+ parent.addView(child, index)
42
+ }
43
+
44
+ override fun getChildAt(parent: StreamShimmerFrameLayout, index: Int): android.view.View {
45
+ return parent.getChildAt(index)
46
+ }
47
+
48
+ override fun getChildCount(parent: StreamShimmerFrameLayout): Int {
49
+ return parent.childCount
50
+ }
51
+
52
+ override fun removeViewAt(parent: StreamShimmerFrameLayout, index: Int) {
53
+ parent.removeViewAt(index)
54
+ }
55
+
56
+ override fun getDelegate(): ViewManagerDelegate<StreamShimmerFrameLayout> = delegate
57
+
58
+ override fun setEnabled(view: StreamShimmerFrameLayout, enabled: Boolean) {
59
+ view.setShimmerEnabled(enabled)
60
+ }
61
+
62
+ override fun setBaseColor(view: StreamShimmerFrameLayout, color: Int?) {
63
+ view.setBaseColor(color ?: DEFAULT_BASE_COLOR)
64
+ }
65
+
66
+ override fun setDuration(view: StreamShimmerFrameLayout, duration: Int) {
67
+ view.setDuration(duration)
68
+ }
69
+
70
+ override fun setGradientColor(view: StreamShimmerFrameLayout, color: Int?) {
71
+ view.setGradientColor(color ?: DEFAULT_GRADIENT_COLOR)
72
+ }
73
+
74
+ override fun onDropViewInstance(@NonNull view: StreamShimmerFrameLayout) {
75
+ super.onDropViewInstance(view)
76
+ // Defensive shutdown for recycled/unmounted views; avoids animator leaks in list-heavy screens.
77
+ view.setShimmerEnabled(false)
78
+ }
79
+
80
+ companion object {
81
+ const val REACT_CLASS = "StreamShimmerView"
82
+ private const val DEFAULT_BASE_COLOR = 0x00FFFFFF
83
+ private const val DEFAULT_GRADIENT_COLOR = 0x59FFFFFF
84
+ }
85
+ }
@@ -12,5 +12,5 @@ abstract class StreamChatReactNativeSpec extends ReactContextBaseJavaModule {
12
12
  super(context);
13
13
  }
14
14
 
15
- public abstract void createResizedImage(String uri, double width, double height, String format, double quality, String mode, boolean onlyScaleDown, Double rotation, @Nullable String outputPath, Boolean keepMeta, Promise promise);
15
+ public abstract void createResizedImage(String uri, double width, double height, String format, double quality, String mode, boolean onlyScaleDown, Double rotation, @Nullable String outputPath, Promise promise);
16
16
  }