stream-chat-react-native 8.13.7 → 9.0.0-beta.1

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()
@@ -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
+ }
@@ -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": "8.13.7",
4
+ "version": "9.0.0-beta.1",
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": "8.13.7"
32
+ "stream-chat-react-native-core": "9.0.0-beta.1"
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
- "prepack": " cp ../../README.md .",
82
- "postpack": "rm README.md"
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": "modules",
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,
@@ -0,0 +1,15 @@
1
+ import type { ColorValue, HostComponent, ViewProps } from 'react-native';
2
+
3
+ import type { Int32, WithDefault } from 'react-native/Libraries/Types/CodegenTypes';
4
+ import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
5
+
6
+ export interface NativeProps extends ViewProps {
7
+ baseColor?: ColorValue;
8
+ duration?: WithDefault<Int32, 1200>;
9
+ enabled?: WithDefault<boolean, true>;
10
+ gradientColor?: ColorValue;
11
+ }
12
+
13
+ export default codegenNativeComponent<NativeProps>(
14
+ 'StreamShimmerView',
15
+ ) as HostComponent<NativeProps>;
@@ -0,0 +1,3 @@
1
+ import StreamShimmerViewNativeComponent from '../native/StreamShimmerViewNativeComponent';
2
+
3
+ export const NativeShimmerView = StreamShimmerViewNativeComponent;
@@ -2,7 +2,19 @@ import React from 'react';
2
2
  import AudioVideoPlayer from './AudioVideo';
3
3
 
4
4
  export const Video = AudioVideoPlayer
5
- ? ({ onBuffer, onEnd, onLoad, onProgress, paused, repeat, resizeMode, style, uri, videoRef }) => (
5
+ ? ({
6
+ onBuffer,
7
+ onEnd,
8
+ onLoad,
9
+ onProgress,
10
+ paused,
11
+ repeat,
12
+ resizeMode,
13
+ style,
14
+ uri,
15
+ videoRef,
16
+ rate,
17
+ }) => (
6
18
  <AudioVideoPlayer
7
19
  ignoreSilentSwitch={'ignore'}
8
20
  onBuffer={onBuffer}
@@ -20,6 +32,7 @@ export const Video = AudioVideoPlayer
20
32
  uri,
21
33
  }}
22
34
  style={style}
35
+ rate={rate}
23
36
  />
24
37
  )
25
38
  : null;
@@ -4,6 +4,7 @@ export * from './FlatList';
4
4
  export * from './getLocalAssetUri';
5
5
  export * from './getPhotos';
6
6
  export * from './iOS14RefreshGallerySelection';
7
+ export * from './NativeShimmerView';
7
8
  export * from './oniOS14GalleryLibrarySelectionChange';
8
9
  export * from './pickDocument';
9
10
  export * from './pickImage';
@@ -1,7 +1,6 @@
1
1
  require "json"
2
2
 
3
3
  package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
- folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
5
4
 
6
5
  Pod::Spec.new do |s|
7
6
  s.name = "stream-chat-react-native"
@@ -11,32 +10,16 @@ Pod::Spec.new do |s|
11
10
  s.license = package["license"]
12
11
  s.authors = package["author"]
13
12
 
14
- s.platforms = { :ios => "10.0" }
13
+ s.platforms = { :ios => min_ios_version_supported }
15
14
  s.source = { :git => "./ios", :tag => "#{s.version}" }
15
+ s.prepare_command = <<-CMD
16
+ if [ -d ../shared-native/ios ]; then
17
+ mkdir -p ios/shared
18
+ rsync -a --delete ../shared-native/ios/ ios/shared/
19
+ fi
20
+ CMD
21
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
22
+ s.private_header_files = "ios/**/*.h"
16
23
 
17
- s.source_files = "ios/**/*.{h,m,mm}"
18
-
19
- s.dependency "React-Core"
20
- s.ios.framework = 'AssetsLibrary', 'MobileCoreServices'
21
-
22
- # Don't install the dependencies when we run `pod install` in the old architecture.
23
- if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
24
- s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
25
- s.pod_target_xcconfig = {
26
- "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
27
- "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
28
- "CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
29
- }
30
- s.dependency "React-Codegen"
31
- # Prebuilt RN modes already handle Folly (or don't expose RCT-Folly podspec).
32
- use_prebuilt_core = ENV['RCT_USE_PREBUILT_RNCORE'] == '1'
33
- use_prebuilt_deps = ENV['RCT_USE_RN_DEP'] == '1'
34
- unless use_prebuilt_core || use_prebuilt_deps
35
- s.dependency "RCT-Folly"
36
- end
37
- s.dependency "RCTRequired"
38
- s.dependency "RCTTypeSafety"
39
- s.dependency "ReactCommon/turbomodule/core"
40
- install_modules_dependencies(s)
41
- end
24
+ install_modules_dependencies(s)
42
25
  end