stream-chat-expo 9.9.1 → 9.9.2-beta.2

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.
@@ -3,6 +3,8 @@ import UIKit
3
3
 
4
4
  private protocol StreamShimmerAppLifecycleObserving: AnyObject {
5
5
  func shimmerAppLifecycleDidChange(isActive: Bool)
6
+ /// A scene changed activation state; observers re-evaluate their own.
7
+ func shimmerSceneLifecycleDidChange()
6
8
  }
7
9
 
8
10
  private final class StreamShimmerAppLifecycleCoordinator: NSObject {
@@ -28,6 +30,21 @@ private final class StreamShimmerAppLifecycleCoordinator: NSObject {
28
30
  name: UIApplication.didEnterBackgroundNotification,
29
31
  object: nil
30
32
  )
33
+
34
+ // A scene can change activation state with no app-level transition (Split View).
35
+ for name in [
36
+ UIScene.didActivateNotification,
37
+ UIScene.willDeactivateNotification,
38
+ UIScene.didEnterBackgroundNotification,
39
+ UIScene.willEnterForegroundNotification,
40
+ ] {
41
+ notificationCenter.addObserver(
42
+ self,
43
+ selector: #selector(handleSceneLifecycleChange),
44
+ name: name,
45
+ object: nil
46
+ )
47
+ }
31
48
  }
32
49
 
33
50
  func addObserver(_ observer: StreamShimmerAppLifecycleObserving) {
@@ -49,6 +66,13 @@ private final class StreamShimmerAppLifecycleCoordinator: NSObject {
49
66
  broadcastAppState(isActive: false)
50
67
  }
51
68
 
69
+ @objc
70
+ private func handleSceneLifecycleChange() {
71
+ for case let observer as StreamShimmerAppLifecycleObserving in observers.allObjects {
72
+ observer.shimmerSceneLifecycleDidChange()
73
+ }
74
+ }
75
+
52
76
  private func broadcastAppState(isActive: Bool) {
53
77
  self.isAppActive = isActive
54
78
 
@@ -83,8 +107,35 @@ public final class StreamShimmerView: UIView {
83
107
  private static let shimmerAnimationKey = "stream_shimmer_translate_x"
84
108
  private static let gradientLocations: [NSNumber] = [0.0, 0.35, 0.5, 0.65, 1.0]
85
109
  private static let gradientAlphaFactors: [CGFloat] = [0, softHighlightAlpha, 1, softHighlightAlpha, 0]
86
- private static var animationDistanceTolerance: CGFloat {
87
- 1 / max(UIScreen.main.scale, 1)
110
+ /// Scale of the display this view is on. Not `UIScreen.main`: an iPhone app is resizable from
111
+ /// iOS 27 and may be on a secondary display. Reports 0 before the view has a window.
112
+ private var currentDisplayScale: CGFloat {
113
+ let traitScale = traitCollection.displayScale
114
+ if traitScale > 0 {
115
+ return traitScale
116
+ }
117
+ return window?.windowScene?.screen.scale ?? 1
118
+ }
119
+
120
+ private var animationDistanceTolerance: CGFloat {
121
+ 1 / max(currentDisplayScale, 1)
122
+ }
123
+
124
+ /// Whether this view's scene is on screen. Preferred over the app-wide `applicationState`, which
125
+ /// cannot distinguish scenes in Split View. `foregroundInactive` counts as on screen: that is the
126
+ /// visible-but-unfocused state a side-by-side scene reports.
127
+ private var isSceneOnScreen: Bool {
128
+ guard let activationState = window?.windowScene?.activationState else {
129
+ return isAppActive
130
+ }
131
+ switch activationState {
132
+ case .foregroundActive, .foregroundInactive:
133
+ return true
134
+ case .background, .unattached:
135
+ return false
136
+ @unknown default:
137
+ return true
138
+ }
88
139
  }
89
140
 
90
141
  private let baseLayer = CALayer()
@@ -140,14 +191,17 @@ public final class StreamShimmerView: UIView {
140
191
  // a later reattach starts from a clean state.
141
192
  stopAnimation()
142
193
  } else {
143
- // Reattaching (including reparenting across windows) re-evaluates state and restarts only
144
- // when needed by current bounds/visibility/enablement.
194
+ // The new window may be on a display with a different scale.
195
+ syncContentsScale()
145
196
  updateLayersForCurrentState()
146
197
  }
147
198
  }
148
199
 
149
200
  public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
150
201
  super.traitCollectionDidChange(previousTraitCollection)
202
+ if traitCollection.displayScale != previousTraitCollection?.displayScale {
203
+ syncContentsScale()
204
+ }
151
205
  if let previousTraitCollection,
152
206
  traitCollection.hasDifferentColorAppearance(comparedTo: previousTraitCollection)
153
207
  {
@@ -194,10 +248,16 @@ public final class StreamShimmerView: UIView {
194
248
  lastAnimatedTravelDistance = 0
195
249
  }
196
250
 
251
+ private func syncContentsScale() {
252
+ let scale = currentDisplayScale
253
+ guard shimmerLayer.contentsScale != scale else { return }
254
+ shimmerLayer.contentsScale = scale
255
+ }
256
+
197
257
  private func setupLayers() {
198
258
  isUserInteractionEnabled = false
199
259
 
200
- shimmerLayer.contentsScale = UIScreen.main.scale
260
+ shimmerLayer.contentsScale = currentDisplayScale
201
261
  shimmerLayer.allowsEdgeAntialiasing = true
202
262
  shimmerLayer.startPoint = CGPoint(x: 0, y: 0.5)
203
263
  shimmerLayer.endPoint = CGPoint(x: 1, y: 0.5)
@@ -249,7 +309,7 @@ public final class StreamShimmerView: UIView {
249
309
  private func updateShimmerAnimation(for bounds: CGRect) {
250
310
  guard
251
311
  enabled,
252
- isAppActive,
312
+ isSceneOnScreen,
253
313
  window != nil,
254
314
  !isHidden,
255
315
  alpha > 0.01,
@@ -267,7 +327,7 @@ public final class StreamShimmerView: UIView {
267
327
  // restarting. Fabric can relayout the view for height-only or subpixel changes that do not
268
328
  // require a new horizontal sweep.
269
329
  if shimmerLayer.animation(forKey: Self.shimmerAnimationKey) != nil,
270
- abs(lastAnimatedTravelDistance - animationTravelDistance) <= Self.animationDistanceTolerance,
330
+ abs(lastAnimatedTravelDistance - animationTravelDistance) <= animationDistanceTolerance,
271
331
  lastAnimatedDuration == shimmerDuration
272
332
  {
273
333
  return
@@ -339,14 +399,22 @@ public final class StreamShimmerView: UIView {
339
399
  }
340
400
 
341
401
  extension StreamShimmerView: StreamShimmerAppLifecycleObserving {
342
- func shimmerAppLifecycleDidChange(isActive: Bool) {
343
- // iOS can drop active layer animations while the app is backgrounded. We explicitly rerun
344
- // a state update on foreground so shimmer reliably restarts when returning to the app.
345
- self.isAppActive = isActive
346
- if isActive {
402
+ func shimmerSceneLifecycleDidChange() {
403
+ reevaluateForSceneState()
404
+ }
405
+
406
+ private func reevaluateForSceneState() {
407
+ if isSceneOnScreen {
347
408
  updateLayersForCurrentState()
348
409
  } else {
349
410
  stopAnimation()
350
411
  }
351
412
  }
413
+
414
+ func shimmerAppLifecycleDidChange(isActive: Bool) {
415
+ // iOS can drop active layer animations while the app is backgrounded. We explicitly rerun
416
+ // a state update on foreground so shimmer reliably restarts when returning to the app.
417
+ self.isAppActive = isActive
418
+ reevaluateForSceneState()
419
+ }
352
420
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stream-chat-expo",
3
3
  "description": "The official Expo SDK for Stream Chat, a service for building chat applications",
4
- "version": "9.9.1",
4
+ "version": "9.9.2-beta.2",
5
5
  "author": {
6
6
  "company": "Stream.io Inc",
7
7
  "name": "Stream.io Inc"
@@ -27,7 +27,7 @@
27
27
  "types": "types/index.d.ts",
28
28
  "dependencies": {
29
29
  "mime": "^4.0.7",
30
- "stream-chat-react-native-core": "9.9.1"
30
+ "stream-chat-react-native-core": "9.9.2-beta.2"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "expo": ">=52.0.0",