@datadog/mobile-react-native-session-replay 2.14.1 → 2.14.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.
@@ -0,0 +1,18 @@
1
+ /*
2
+ * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3
+ * This product includes software developed at Datadog (https://www.datadoghq.com/).
4
+ * Copyright 2016-Present Datadog, Inc.
5
+ */
6
+ #import <Foundation/Foundation.h>
7
+ #import "RCTTextPropertiesWrapper.h"
8
+
9
+ @class RCTUIManager;
10
+
11
+ @interface RCTTextExtractor : NSObject
12
+
13
+ - (nullable RCTTextPropertiesWrapper*)tryToExtractTextPropertiesFromView:(UIView* _Nonnull)view
14
+ withUIManager:(RCTUIManager* _Nonnull)uiManager;
15
+
16
+ - (BOOL)isRCTTextView:(UIView* _Nonnull)view;
17
+
18
+ @end
@@ -0,0 +1,127 @@
1
+ /*
2
+ * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
3
+ * This product includes software developed at Datadog (https://www.datadoghq.com/).
4
+ * Copyright 2016-Present Datadog, Inc.
5
+ */
6
+
7
+ #import "RCTTextExtractor.h"
8
+
9
+ #if !RCT_NEW_ARCH_ENABLED
10
+ #import <React/RCTBridge.h>
11
+ #import <React/RCTUIManager.h>
12
+ #import <React/RCTTextView.h>
13
+ #import <React/RCTTextShadowView.h>
14
+ #import <React/RCTRawTextShadowView.h>
15
+ #import <React/RCTVirtualTextShadowView.h>
16
+ #endif
17
+
18
+ @implementation RCTTextExtractor
19
+
20
+ /**
21
+ * Extracts the text properties from the given UIView when using the old Paper architecture.
22
+ * Returns nil when using new architecture or if the view is not a RCTTextView.
23
+ */
24
+ - (nullable RCTTextPropertiesWrapper*)tryToExtractTextPropertiesFromView:(UIView *)view
25
+ withUIManager:(RCTUIManager *)uiManager {
26
+ #if !RCT_NEW_ARCH_ENABLED
27
+ if (![view isKindOfClass:[RCTTextView class]]) {
28
+ return nil;
29
+ }
30
+
31
+ RCTTextView* textView = (RCTTextView*)view;
32
+ NSNumber* tag = textView.reactTag;
33
+
34
+ __block RCTTextShadowView* shadowView = nil;
35
+ NSTimeInterval timeout = 0.2;
36
+ dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
37
+
38
+ // We need to access the shadow view from the UIManager queue, but we're currently on the main thread.
39
+ // Calling `.sync` from the main thread to the UIManager queue is unsafe, because the UIManager queue
40
+ // may already be executing a layout operation that in turn requires the main thread (e.g. measuring a native view).
41
+ // That would create a circular dependency and deadlock the app.
42
+ // To avoid this, we dispatch the work asynchronously to the UIManager queue and wait with a timeout.
43
+ // This ensures we block only if absolutely necessary, and can fail gracefully if the queue is busy.
44
+ dispatch_async(uiManager.methodQueue, ^{
45
+ shadowView = (RCTTextShadowView*)[uiManager shadowViewForReactTag:tag];
46
+ dispatch_semaphore_signal(semaphore);
47
+ });
48
+
49
+ dispatch_time_t waitTimeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC));
50
+ long waitResult = dispatch_semaphore_wait(semaphore, waitTimeout);
51
+
52
+ if (waitResult != 0) { // timeout
53
+ return nil;
54
+ }
55
+
56
+ if (shadowView == nil || ![shadowView isKindOfClass:[RCTTextShadowView class]]) {
57
+ return nil;
58
+ }
59
+
60
+ RCTTextPropertiesWrapper* textProperties = [[RCTTextPropertiesWrapper alloc] init];
61
+
62
+ // Extract text from subviews
63
+ NSString* text = [self tryToExtractTextFromSubViews:shadowView.reactSubviews];
64
+ if (text != nil) {
65
+ textProperties.text = text;
66
+ }
67
+
68
+ // Extract text attributes
69
+ if (shadowView.textAttributes.foregroundColor != nil) {
70
+ textProperties.foregroundColor = shadowView.textAttributes.foregroundColor;
71
+ }
72
+
73
+ textProperties.alignment = shadowView.textAttributes.alignment;
74
+ textProperties.fontSize = shadowView.textAttributes.fontSize;
75
+ textProperties.contentRect = shadowView.layoutMetrics.contentFrame;
76
+
77
+ return textProperties;
78
+ #else
79
+ return nil;
80
+ #endif
81
+ }
82
+
83
+ #if !RCT_NEW_ARCH_ENABLED
84
+ - (nullable NSString*)tryToExtractTextFromSubViews:(NSArray<RCTShadowView*>*)subviews {
85
+ if (subviews == nil) {
86
+ return nil;
87
+ }
88
+
89
+ NSMutableArray<NSString*>* textParts = [NSMutableArray array];
90
+
91
+ for (RCTShadowView* subview in subviews) {
92
+ if ([subview isKindOfClass:[RCTRawTextShadowView class]]) {
93
+ RCTRawTextShadowView* rawTextView = (RCTRawTextShadowView*)subview;
94
+ if (rawTextView.text != nil) {
95
+ [textParts addObject:rawTextView.text];
96
+ }
97
+ } else if ([subview isKindOfClass:[RCTVirtualTextShadowView class]]) {
98
+ // We recursively get all subviews for nested Text components
99
+ RCTVirtualTextShadowView* virtualTextView = (RCTVirtualTextShadowView*)subview;
100
+ NSString* nestedText = [self tryToExtractTextFromSubViews:virtualTextView.reactSubviews];
101
+ if (nestedText != nil) {
102
+ [textParts addObject:nestedText];
103
+ }
104
+ }
105
+ }
106
+
107
+ if (textParts.count == 0) {
108
+ return nil;
109
+ }
110
+
111
+ return [textParts componentsJoinedByString:@""];
112
+ }
113
+ #endif
114
+
115
+ /**
116
+ * Checks if the given view is an RCTTextView.
117
+ * Returns NO when using new architecture or if the view is not a RCTTextView.
118
+ */
119
+ - (BOOL)isRCTTextView:(UIView *)view {
120
+ #if !RCT_NEW_ARCH_ENABLED
121
+ return [view isKindOfClass:[RCTTextView class]];
122
+ #else
123
+ return NO;
124
+ #endif
125
+ }
126
+
127
+ @end
@@ -18,10 +18,12 @@ internal class RCTTextViewRecorder: SessionReplayNodeRecorder {
18
18
 
19
19
  internal let uiManager: RCTUIManager
20
20
  internal let fabricWrapper: RCTFabricWrapper
21
+ private let textExtractor: RCTTextExtractor
21
22
 
22
23
  internal init(uiManager: RCTUIManager, fabricWrapper: RCTFabricWrapper) {
23
24
  self.uiManager = uiManager
24
25
  self.fabricWrapper = fabricWrapper
26
+ self.textExtractor = RCTTextExtractor()
25
27
  }
26
28
 
27
29
  public func semantics(
@@ -30,9 +32,12 @@ internal class RCTTextViewRecorder: SessionReplayNodeRecorder {
30
32
  in context: SessionReplayViewTreeRecordingContext
31
33
  ) -> SessionReplayNodeSemantics? {
32
34
  guard
33
- let textProperties = fabricWrapper.tryToExtractTextProperties(from: view) ?? tryToExtractTextProperties(view: view)
35
+ let textProperties = fabricWrapper.tryToExtractTextProperties(from: view)
36
+ ?? textExtractor.tryToExtractTextProperties(from: view, with: uiManager)
34
37
  else {
35
- return view is RCTTextView ? SessionReplayInvisibleElement.constant : nil
38
+ // Check if this is an RCTTextView that we couldn't extract text from
39
+ // This check is done in Objective-C to avoid compile-time dependency on RCTTextView
40
+ return textExtractor.isRCTTextView(view) ? SessionReplayInvisibleElement.constant : nil
36
41
  }
37
42
 
38
43
  let builder = RCTTextViewWireframesBuilder(
@@ -51,73 +56,6 @@ internal class RCTTextViewRecorder: SessionReplayNodeRecorder {
51
56
  ])
52
57
  }
53
58
 
54
- internal func tryToExtractTextFromSubViews(
55
- subviews: [RCTShadowView]?
56
- ) -> String? {
57
- guard let subviews = subviews else {
58
- return nil
59
- }
60
-
61
- return subviews.compactMap { subview in
62
- if let sub = subview as? RCTRawTextShadowView {
63
- return sub.text
64
- }
65
- if let sub = subview as? RCTVirtualTextShadowView {
66
- // We recursively get all subviews for nested Text components
67
- return tryToExtractTextFromSubViews(subviews: sub.reactSubviews())
68
- }
69
- return nil
70
- }.joined()
71
- }
72
-
73
- private func tryToExtractTextProperties(view: UIView) -> RCTTextPropertiesWrapper? {
74
- guard let textView = view as? RCTTextView else {
75
- return nil
76
- }
77
-
78
- var shadowView: RCTTextShadowView? = nil
79
- let tag = textView.reactTag
80
-
81
- let timeout: TimeInterval = 0.2
82
- let semaphore = DispatchSemaphore(value: 0)
83
-
84
- // We need to access the shadow view from the UIManager queue, but we're currently on the main thread.
85
- // Calling `.sync` from the main thread to the UIManager queue is unsafe, because the UIManager queue
86
- // may already be executing a layout operation that in turn requires the main thread (e.g. measuring a native view).
87
- // That would create a circular dependency and deadlock the app.
88
- // To avoid this, we dispatch the work asynchronously to the UIManager queue and wait with a timeout.
89
- // This ensures we block only if absolutely necessary, and can fail gracefully if the queue is busy.
90
- RCTGetUIManagerQueue().async {
91
- shadowView = self.uiManager.shadowView(forReactTag: tag) as? RCTTextShadowView
92
- semaphore.signal()
93
- }
94
-
95
- let waitResult = semaphore.wait(timeout: .now() + timeout)
96
- if waitResult == .timedOut {
97
- return nil
98
- }
99
-
100
- guard let shadow = shadowView else {
101
- return nil
102
- }
103
-
104
- let textProperties = RCTTextPropertiesWrapper()
105
-
106
- // TODO: RUM-2173 check performance is ok
107
- if let text = tryToExtractTextFromSubViews(subviews: shadow.reactSubviews()) {
108
- textProperties.text = text
109
- }
110
-
111
- if let foregroundColor = shadow.textAttributes.foregroundColor {
112
- textProperties.foregroundColor = foregroundColor
113
- }
114
-
115
- textProperties.alignment = shadow.textAttributes.alignment
116
- textProperties.fontSize = shadow.textAttributes.fontSize
117
- textProperties.contentRect = shadow.contentFrame
118
-
119
- return textProperties
120
- }
121
59
  }
122
60
 
123
61
  internal struct RCTTextViewWireframesBuilder: SessionReplayNodeWireframesBuilder {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datadog/mobile-react-native-session-replay",
3
- "version": "2.14.1",
3
+ "version": "2.14.2",
4
4
  "description": "A client-side React Native module to enable session replay with Datadog",
5
5
  "keywords": [
6
6
  "datadog",
@@ -96,5 +96,5 @@
96
96
  "dependencies": {
97
97
  "chokidar": "^4.0.3"
98
98
  },
99
- "gitHead": "ceddd4c317a90a7b19409146690022cd6889cb26"
99
+ "gitHead": "1f85e56ea633c9cdee4c970a46356f5f8a1dfc67"
100
100
  }