@multiplayer-app/session-recorder-react-native 0.0.1-beta.4 → 0.0.1-beta.6
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 +32 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/multiplayer/sessionrecorder/ScreenMaskingModule.kt +202 -0
- package/android/src/main/java/com/multiplayer/sessionrecorder/ScreenMaskingPackage.kt +16 -0
- package/android/src/main/java/com/multiplayer/sessionrecorder/SessionRecorderModule.kt +202 -0
- package/android/src/main/java/com/multiplayer/sessionrecorder/SessionRecorderPackage.kt +16 -0
- package/dist/components/MaskableComponent.d.ts +22 -0
- package/dist/components/MaskableComponent.js +1 -0
- package/dist/components/MaskableComponent.js.map +1 -0
- package/dist/components/MaskableTextInput.d.ts +14 -0
- package/dist/components/MaskableTextInput.js +1 -0
- package/dist/components/MaskableTextInput.js.map +1 -0
- package/dist/components/SessionRecorderWidget/FinalPopover.js +1 -1
- package/dist/components/SessionRecorderWidget/FinalPopover.js.map +1 -1
- package/dist/components/SessionRecorderWidget/FloatingButton.js +1 -1
- package/dist/components/SessionRecorderWidget/FloatingButton.js.map +1 -1
- package/dist/components/SessionRecorderWidget/InitialPopover.js +1 -1
- package/dist/components/SessionRecorderWidget/InitialPopover.js.map +1 -1
- package/dist/components/SessionRecorderWidget/ModalContainer.js +1 -1
- package/dist/components/SessionRecorderWidget/ModalContainer.js.map +1 -1
- package/dist/components/SessionRecorderWidget/ModalHeader.d.ts +6 -0
- package/dist/components/SessionRecorderWidget/ModalHeader.js +1 -0
- package/dist/components/SessionRecorderWidget/ModalHeader.js.map +1 -0
- package/dist/components/SessionRecorderWidget/icons.d.ts +1 -0
- package/dist/components/SessionRecorderWidget/icons.js +1 -1
- package/dist/components/SessionRecorderWidget/icons.js.map +1 -1
- package/dist/components/SessionRecorderWidget/styles.d.ts +39 -22
- package/dist/components/SessionRecorderWidget/styles.js +1 -1
- package/dist/components/SessionRecorderWidget/styles.js.map +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +1 -1
- package/dist/components/index.js.map +1 -1
- package/dist/config/defaults.js +1 -1
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/masking.js +1 -1
- package/dist/config/masking.js.map +1 -1
- package/dist/native/ScreenMasking.d.ts +21 -0
- package/dist/native/ScreenMasking.js +1 -0
- package/dist/native/ScreenMasking.js.map +1 -0
- package/dist/native/SessionRecorderNative.d.ts +21 -0
- package/dist/native/SessionRecorderNative.js +1 -0
- package/dist/native/SessionRecorderNative.js.map +1 -0
- package/dist/recorder/screenRecorder.d.ts +1 -0
- package/dist/recorder/screenRecorder.js +1 -1
- package/dist/recorder/screenRecorder.js.map +1 -1
- package/dist/recorder/screenshotManager.d.ts +10 -0
- package/dist/recorder/screenshotManager.js +1 -0
- package/dist/recorder/screenshotManager.js.map +1 -0
- package/dist/services/screenMaskingService.d.ts +39 -0
- package/dist/services/screenMaskingService.js +1 -0
- package/dist/services/screenMaskingService.js.map +1 -0
- package/dist/types/session-recorder.d.ts +6 -0
- package/dist/types/session-recorder.js.map +1 -1
- package/dist/utils/componentRegistry.d.ts +64 -0
- package/dist/utils/componentRegistry.js +1 -0
- package/dist/utils/componentRegistry.js.map +1 -0
- package/dist/utils/nativeModuleTest.d.ts +8 -0
- package/dist/utils/nativeModuleTest.js +1 -0
- package/dist/utils/nativeModuleTest.js.map +1 -0
- package/dist/utils/reactNativeHierarchyExtractor.d.ts +38 -0
- package/dist/utils/reactNativeHierarchyExtractor.js +1 -0
- package/dist/utils/reactNativeHierarchyExtractor.js.map +1 -0
- package/dist/utils/screenshotMasker.d.ts +96 -0
- package/dist/utils/screenshotMasker.js +1 -0
- package/dist/utils/screenshotMasker.js.map +1 -0
- package/dist/utils/viewHierarchyTracker.d.ts +89 -0
- package/dist/utils/viewHierarchyTracker.js +1 -0
- package/dist/utils/viewHierarchyTracker.js.map +1 -0
- package/docs/TROUBLESHOOTING.md +168 -0
- package/ios/ScreenMasking.m +12 -0
- package/ios/ScreenMasking.podspec +21 -0
- package/ios/ScreenMasking.swift +205 -0
- package/ios/SessionRecorder.m +12 -0
- package/ios/SessionRecorder.podspec +21 -0
- package/ios/SessionRecorder.swift +205 -0
- package/ios/SessionRecorderNative.podspec +21 -0
- package/package.json +10 -1
- package/react-native.config.js +15 -0
- package/src/components/SessionRecorderWidget/FinalPopover.tsx +5 -16
- package/src/components/SessionRecorderWidget/FloatingButton.tsx +14 -27
- package/src/components/SessionRecorderWidget/InitialPopover.tsx +3 -9
- package/src/components/SessionRecorderWidget/ModalContainer.tsx +77 -29
- package/src/components/SessionRecorderWidget/ModalHeader.tsx +24 -0
- package/src/components/SessionRecorderWidget/icons.tsx +9 -0
- package/src/components/SessionRecorderWidget/styles.ts +48 -35
- package/src/components/index.ts +3 -1
- package/src/config/defaults.ts +1 -0
- package/src/config/masking.ts +1 -0
- package/src/native/ScreenMasking.ts +34 -0
- package/src/native/SessionRecorderNative.ts +34 -0
- package/src/recorder/screenRecorder.ts +31 -2
- package/src/services/screenMaskingService.ts +118 -0
- package/src/types/session-recorder.ts +7 -1
- package/src/utils/nativeModuleTest.ts +60 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
dependencies: {
|
|
3
|
+
'react-native-session-recorder': {
|
|
4
|
+
platforms: {
|
|
5
|
+
android: {
|
|
6
|
+
sourceDir: '../android',
|
|
7
|
+
packageImportPath: 'import com.multiplayer.sessionrecorder.SessionRecorderPackage;'
|
|
8
|
+
},
|
|
9
|
+
ios: {
|
|
10
|
+
podspecPath: '../ios/SessionRecorderNative.podspec'
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
|
-
import { View, Text, Pressable, TextInput,
|
|
2
|
+
import { View, Text, Pressable, TextInput, Alert } from 'react-native'
|
|
3
3
|
import { WidgetTextOverridesConfig } from '../../types'
|
|
4
4
|
import { sharedStyles } from './styles'
|
|
5
|
+
import ModalHeader from './ModalHeader'
|
|
5
6
|
|
|
6
7
|
interface FinalPopoverProps {
|
|
7
8
|
textOverrides: WidgetTextOverridesConfig
|
|
@@ -11,13 +12,7 @@ interface FinalPopoverProps {
|
|
|
11
12
|
isSubmitting: boolean
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
const FinalPopover: React.FC<FinalPopoverProps> = ({
|
|
15
|
-
textOverrides,
|
|
16
|
-
onStopRecording,
|
|
17
|
-
onCancelSession,
|
|
18
|
-
onClose,
|
|
19
|
-
isSubmitting
|
|
20
|
-
}) => {
|
|
15
|
+
const FinalPopover: React.FC<FinalPopoverProps> = ({ textOverrides, onStopRecording, onCancelSession, isSubmitting }) => {
|
|
21
16
|
const [comment, setComment] = useState('')
|
|
22
17
|
|
|
23
18
|
const handleStopRecording = async () => {
|
|
@@ -30,17 +25,11 @@ const FinalPopover: React.FC<FinalPopoverProps> = ({
|
|
|
30
25
|
|
|
31
26
|
return (
|
|
32
27
|
<View style={sharedStyles.popoverContent}>
|
|
33
|
-
<
|
|
34
|
-
<Pressable onPress={() => Linking.openURL('https://www.multiplayer.app')}>
|
|
35
|
-
<Text style={sharedStyles.logoText}>Multiplayer</Text>
|
|
36
|
-
</Pressable>
|
|
28
|
+
<ModalHeader>
|
|
37
29
|
<Pressable onPress={onCancelSession} style={sharedStyles.cancelButton}>
|
|
38
30
|
<Text style={sharedStyles.cancelButtonText}>{textOverrides.cancelButtonText}</Text>
|
|
39
31
|
</Pressable>
|
|
40
|
-
|
|
41
|
-
<Text style={sharedStyles.closeButtonText}>×</Text>
|
|
42
|
-
</Pressable>
|
|
43
|
-
</View>
|
|
32
|
+
</ModalHeader>
|
|
44
33
|
|
|
45
34
|
<View style={sharedStyles.popoverBody}>
|
|
46
35
|
<Text style={sharedStyles.title}>{textOverrides.finalTitle}</Text>
|
|
@@ -8,20 +8,21 @@ interface FloatingButtonProps {
|
|
|
8
8
|
sessionState: SessionState | null
|
|
9
9
|
onPress: () => void
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
const buttonSize = 52
|
|
12
13
|
const rightOffset = 20
|
|
13
14
|
const topOffset = Platform.OS === 'ios' ? 60 : 40
|
|
14
15
|
|
|
15
16
|
const FloatingButton: React.FC<FloatingButtonProps> = ({ sessionState, onPress }) => {
|
|
16
|
-
const position = useRef(new Animated.ValueXY({ x: 0, y: 0 })).current
|
|
17
|
-
const lastPosition = useRef({ top: topOffset, right: rightOffset })
|
|
18
|
-
const storageService = useRef(StorageService.getInstance()).current
|
|
17
|
+
const position = useRef(new Animated.ValueXY({ x: 0, y: 0 })).current
|
|
18
|
+
const lastPosition = useRef({ top: topOffset, right: rightOffset })
|
|
19
|
+
const storageService = useRef(StorageService.getInstance()).current
|
|
19
20
|
|
|
20
21
|
const screenBounds = useMemo(() => {
|
|
21
22
|
const { width, height } = Dimensions.get('window')
|
|
22
23
|
|
|
23
24
|
return {
|
|
24
|
-
minTop: topOffset,
|
|
25
|
+
minTop: topOffset,
|
|
25
26
|
maxTop: height - buttonSize,
|
|
26
27
|
minRight: 0,
|
|
27
28
|
maxRight: width - buttonSize
|
|
@@ -32,14 +33,12 @@ const FloatingButton: React.FC<FloatingButtonProps> = ({ sessionState, onPress }
|
|
|
32
33
|
useEffect(() => {
|
|
33
34
|
const savedPosition = storageService.getFloatingButtonPosition()
|
|
34
35
|
if (savedPosition) {
|
|
35
|
-
|
|
36
|
-
const { width, height } = Dimensions.get('window')
|
|
36
|
+
const { width } = Dimensions.get('window')
|
|
37
37
|
const top = savedPosition.y
|
|
38
38
|
const right = width - savedPosition.x - buttonSize
|
|
39
39
|
lastPosition.current = { top, right }
|
|
40
40
|
position.setValue({ x: right, y: top })
|
|
41
41
|
} else {
|
|
42
|
-
// Set default position
|
|
43
42
|
position.setValue({ x: lastPosition.current.right, y: lastPosition.current.top })
|
|
44
43
|
}
|
|
45
44
|
}, [])
|
|
@@ -48,7 +47,6 @@ const FloatingButton: React.FC<FloatingButtonProps> = ({ sessionState, onPress }
|
|
|
48
47
|
PanResponder.create({
|
|
49
48
|
onStartShouldSetPanResponder: () => true,
|
|
50
49
|
onMoveShouldSetPanResponder: (evt, gestureState) => {
|
|
51
|
-
// Only start dragging if movement is significant enough
|
|
52
50
|
const distance = Math.sqrt(gestureState.dx * gestureState.dx + gestureState.dy * gestureState.dy)
|
|
53
51
|
return distance > 5
|
|
54
52
|
},
|
|
@@ -59,7 +57,7 @@ const FloatingButton: React.FC<FloatingButtonProps> = ({ sessionState, onPress }
|
|
|
59
57
|
onPanResponderMove: (evt, gestureState) => {
|
|
60
58
|
// Calculate new position based on gesture movement
|
|
61
59
|
const newTop = lastPosition.current.top + gestureState.dy
|
|
62
|
-
const newRight = lastPosition.current.right - gestureState.dx
|
|
60
|
+
const newRight = lastPosition.current.right - gestureState.dx
|
|
63
61
|
|
|
64
62
|
// Update position during drag
|
|
65
63
|
position.setValue({ x: newRight, y: newTop })
|
|
@@ -74,7 +72,7 @@ const FloatingButton: React.FC<FloatingButtonProps> = ({ sessionState, onPress }
|
|
|
74
72
|
} else {
|
|
75
73
|
// Calculate new position after dragging
|
|
76
74
|
const newTop = lastPosition.current.top + gestureState.dy
|
|
77
|
-
const newRight = lastPosition.current.right - gestureState.dx
|
|
75
|
+
const newRight = lastPosition.current.right - gestureState.dx
|
|
78
76
|
|
|
79
77
|
// Clamp to screen bounds
|
|
80
78
|
const clampedTop = Math.max(screenBounds.minTop, Math.min(screenBounds.maxTop, newTop))
|
|
@@ -99,31 +97,20 @@ const FloatingButton: React.FC<FloatingButtonProps> = ({ sessionState, onPress }
|
|
|
99
97
|
).current
|
|
100
98
|
|
|
101
99
|
// Memoized button icon and color for performance
|
|
102
|
-
const
|
|
103
|
-
switch (sessionState) {
|
|
104
|
-
case SessionState.started:
|
|
105
|
-
return <CapturingIcon size={24} color='white' />
|
|
106
|
-
case SessionState.paused:
|
|
107
|
-
return <PausedIcon size={24} color='white' />
|
|
108
|
-
default:
|
|
109
|
-
return <RecordIcon size={19} color='white' />
|
|
110
|
-
}
|
|
111
|
-
}, [sessionState])
|
|
112
|
-
|
|
113
|
-
const buttonColor = useMemo(() => {
|
|
100
|
+
const content = useMemo(() => {
|
|
114
101
|
switch (sessionState) {
|
|
115
102
|
case SessionState.started:
|
|
116
|
-
return '#FF4444'
|
|
103
|
+
return { icon: <CapturingIcon size={28} color='white' />, color: '#FF4444' }
|
|
117
104
|
case SessionState.paused:
|
|
118
|
-
return '#FFA500'
|
|
105
|
+
return { icon: <PausedIcon size={28} color='white' />, color: '#FFA500' }
|
|
119
106
|
default:
|
|
120
|
-
return '#
|
|
107
|
+
return { icon: <RecordIcon size={28} color='#718096' />, color: '#ffffff' }
|
|
121
108
|
}
|
|
122
109
|
}, [sessionState])
|
|
123
110
|
|
|
124
111
|
return (
|
|
125
112
|
<Animated.View style={[styles.draggableButton, { top: position.y, right: position.x }]} {...panResponder.panHandlers}>
|
|
126
|
-
<View style={[styles.floatingButton, { backgroundColor:
|
|
113
|
+
<View style={[styles.floatingButton, { backgroundColor: content.color }]}>{content.icon}</View>
|
|
127
114
|
</Animated.View>
|
|
128
115
|
)
|
|
129
116
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
|
-
import { View, Text, Pressable,
|
|
2
|
+
import { View, Text, Pressable, Alert, Switch } from 'react-native'
|
|
3
3
|
import { SessionType } from '@multiplayer-app/session-recorder-common'
|
|
4
4
|
import { WidgetTextOverridesConfig } from '../../types'
|
|
5
5
|
import { sharedStyles } from './styles'
|
|
6
|
+
import ModalHeader from './ModalHeader'
|
|
6
7
|
|
|
7
8
|
interface InitialPopoverProps {
|
|
8
9
|
textOverrides: WidgetTextOverridesConfig
|
|
@@ -34,14 +35,7 @@ const InitialPopover: React.FC<InitialPopoverProps> = ({
|
|
|
34
35
|
|
|
35
36
|
return (
|
|
36
37
|
<View style={sharedStyles.popoverContent}>
|
|
37
|
-
<
|
|
38
|
-
<Pressable onPress={() => Linking.openURL('https://www.multiplayer.app')}>
|
|
39
|
-
<Text style={sharedStyles.logoText}>Multiplayer</Text>
|
|
40
|
-
</Pressable>
|
|
41
|
-
<Pressable onPress={onClose} style={sharedStyles.closeButton}>
|
|
42
|
-
<Text style={sharedStyles.closeButtonText}>×</Text>
|
|
43
|
-
</Pressable>
|
|
44
|
-
</View>
|
|
38
|
+
<ModalHeader />
|
|
45
39
|
|
|
46
40
|
<View style={sharedStyles.popoverBody}>
|
|
47
41
|
{showContinuousRecording && (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react'
|
|
2
|
-
import { Animated,
|
|
2
|
+
import { Animated, Pressable, StyleSheet, Dimensions, Modal, SafeAreaView } from 'react-native'
|
|
3
|
+
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'
|
|
3
4
|
|
|
4
5
|
const { height: SCREEN_HEIGHT } = Dimensions.get('window')
|
|
5
6
|
const MODAL_HEIGHT = SCREEN_HEIGHT * 0.7
|
|
@@ -12,28 +13,86 @@ interface ModalContainerProps {
|
|
|
12
13
|
|
|
13
14
|
const ModalContainer: React.FC<ModalContainerProps> = ({ isVisible, onClose, children }) => {
|
|
14
15
|
const fadeAnim = useRef(new Animated.Value(0)).current
|
|
16
|
+
const translateY = useRef(new Animated.Value(0)).current
|
|
15
17
|
const [animatedFinished, setAnimatedFinished] = useState(false)
|
|
16
18
|
|
|
19
|
+
const SWIPE_THRESHOLD = 100 // Distance to trigger close
|
|
20
|
+
const MAX_SWIPE_DISTANCE = 200 // Maximum swipe distance
|
|
21
|
+
|
|
22
|
+
const animateClose = () => {
|
|
23
|
+
Animated.parallel([
|
|
24
|
+
Animated.timing(fadeAnim, {
|
|
25
|
+
toValue: 0,
|
|
26
|
+
duration: 250,
|
|
27
|
+
useNativeDriver: true
|
|
28
|
+
}),
|
|
29
|
+
Animated.timing(translateY, {
|
|
30
|
+
toValue: MODAL_HEIGHT,
|
|
31
|
+
duration: 250,
|
|
32
|
+
useNativeDriver: true
|
|
33
|
+
})
|
|
34
|
+
]).start(() => {
|
|
35
|
+
onClose()
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
17
39
|
useEffect(() => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
40
|
+
if (isVisible) {
|
|
41
|
+
// Start from bottom and animate to position
|
|
42
|
+
translateY.setValue(MODAL_HEIGHT)
|
|
43
|
+
Animated.parallel([
|
|
44
|
+
Animated.timing(fadeAnim, {
|
|
45
|
+
toValue: 1,
|
|
46
|
+
duration: 300,
|
|
47
|
+
useNativeDriver: true
|
|
48
|
+
}),
|
|
49
|
+
Animated.timing(translateY, {
|
|
50
|
+
toValue: 0,
|
|
51
|
+
duration: 300,
|
|
52
|
+
useNativeDriver: true
|
|
53
|
+
})
|
|
54
|
+
]).start()
|
|
55
|
+
}
|
|
56
|
+
}, [isVisible, fadeAnim, translateY])
|
|
57
|
+
|
|
58
|
+
const panGesture = Gesture.Pan()
|
|
59
|
+
.onUpdate((event) => {
|
|
60
|
+
translateY.setValue(event.translationY)
|
|
61
|
+
})
|
|
62
|
+
.onEnd((event) => {
|
|
63
|
+
const { translationY, velocityY } = event
|
|
64
|
+
|
|
65
|
+
// If swiped down with sufficient distance or velocity, close modal
|
|
66
|
+
if (translationY > SWIPE_THRESHOLD || velocityY > 500) {
|
|
67
|
+
animateClose()
|
|
68
|
+
} else {
|
|
69
|
+
// Snap back to original position
|
|
70
|
+
Animated.spring(translateY, {
|
|
71
|
+
toValue: 0,
|
|
72
|
+
useNativeDriver: true,
|
|
73
|
+
tension: 100,
|
|
74
|
+
friction: 8
|
|
75
|
+
}).start()
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
.activeOffsetY(10)
|
|
79
|
+
.runOnJS(true)
|
|
24
80
|
|
|
25
81
|
return (
|
|
26
82
|
<>
|
|
27
83
|
{isVisible && (
|
|
28
84
|
<Animated.View style={{ ...styles.backdrop, opacity: fadeAnim }}>
|
|
29
|
-
<Pressable style={styles.backdropPressable} onPress={
|
|
85
|
+
<Pressable style={styles.backdropPressable} onPress={animateClose} />
|
|
30
86
|
</Animated.View>
|
|
31
87
|
)}
|
|
32
|
-
<Modal visible={isVisible} transparent animationType='
|
|
33
|
-
<
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
88
|
+
<Modal visible={isVisible} transparent animationType='none' onRequestClose={onClose}>
|
|
89
|
+
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
90
|
+
<GestureDetector gesture={panGesture}>
|
|
91
|
+
<Animated.View style={[styles.modal, { transform: [{ translateY }] }]}>
|
|
92
|
+
<SafeAreaView style={styles.safeArea}>{children}</SafeAreaView>
|
|
93
|
+
</Animated.View>
|
|
94
|
+
</GestureDetector>
|
|
95
|
+
</GestureHandlerRootView>
|
|
37
96
|
</Modal>
|
|
38
97
|
</>
|
|
39
98
|
)
|
|
@@ -51,29 +110,18 @@ const styles = StyleSheet.create({
|
|
|
51
110
|
backdropPressable: {
|
|
52
111
|
flex: 1
|
|
53
112
|
},
|
|
113
|
+
safeArea: {
|
|
114
|
+
flex: 1
|
|
115
|
+
},
|
|
54
116
|
modal: {
|
|
55
117
|
position: 'absolute',
|
|
56
118
|
bottom: 0,
|
|
57
119
|
left: 0,
|
|
58
120
|
right: 0,
|
|
59
|
-
height:
|
|
121
|
+
height: 'auto',
|
|
60
122
|
backgroundColor: 'white',
|
|
61
123
|
borderTopLeftRadius: 20,
|
|
62
|
-
borderTopRightRadius: 20
|
|
63
|
-
elevation: 10,
|
|
64
|
-
shadowColor: '#000',
|
|
65
|
-
shadowOffset: { width: 0, height: -2 },
|
|
66
|
-
shadowOpacity: 0.25,
|
|
67
|
-
shadowRadius: 4
|
|
68
|
-
},
|
|
69
|
-
modalHandle: {
|
|
70
|
-
width: 40,
|
|
71
|
-
height: 4,
|
|
72
|
-
backgroundColor: '#D1D5DB',
|
|
73
|
-
borderRadius: 2,
|
|
74
|
-
alignSelf: 'center',
|
|
75
|
-
marginTop: 8,
|
|
76
|
-
marginBottom: 8
|
|
124
|
+
borderTopRightRadius: 20
|
|
77
125
|
}
|
|
78
126
|
})
|
|
79
127
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Pressable, Linking } from 'react-native'
|
|
3
|
+
import { sharedStyles } from './styles'
|
|
4
|
+
import { LogoIcon } from './icons'
|
|
5
|
+
|
|
6
|
+
interface ModalHeaderProps {
|
|
7
|
+
children?: React.ReactNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ModalHeader: React.FC<ModalHeaderProps> = ({ children }) => {
|
|
11
|
+
return (
|
|
12
|
+
<View style={sharedStyles.popoverHeader}>
|
|
13
|
+
<View style={sharedStyles.modalHandle} />
|
|
14
|
+
<View style={sharedStyles.popoverHeaderContent}>
|
|
15
|
+
<Pressable onPress={() => Linking.openURL('https://www.multiplayer.app')}>
|
|
16
|
+
<LogoIcon size={42} />
|
|
17
|
+
</Pressable>
|
|
18
|
+
{children}
|
|
19
|
+
</View>
|
|
20
|
+
</View>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default ModalHeader
|
|
@@ -41,3 +41,12 @@ export const CheckmarkIcon: React.FC<IconProps> = ({ size = 24, color = 'white'
|
|
|
41
41
|
/>
|
|
42
42
|
</Svg>
|
|
43
43
|
)
|
|
44
|
+
|
|
45
|
+
export const LogoIcon: React.FC<IconProps> = ({ size = 30, color = '#473CFB' }) => (
|
|
46
|
+
<Svg width={size} height={size * 0.8} viewBox='0 0 30 24' fill='none'>
|
|
47
|
+
<Path
|
|
48
|
+
d='M28.8324 8.83643L23.5495 6.85854L21.4856 6.08553L23.6001 0.4375L15 3.65769L6.39963 0.4375L8.51441 6.08585L6.45046 6.85885L1.16757 8.83674L0.625 9.03974L9.10095 12.0981C10.0891 12.4548 10.9201 13.1265 11.4758 13.9952C11.6632 14.2883 11.8194 14.6036 11.9398 14.9369L15 23.4076L18.0602 14.9369C18.1806 14.6036 18.3368 14.288 18.5242 13.9952C19.0802 13.1265 19.9112 12.4545 20.8991 12.0981L29.375 9.03974L28.8324 8.83674V8.83643ZM19.779 10.6434C18.2872 11.1816 17.1126 12.3563 16.5744 13.848L15.014 18.173L11.5182 8.83643L10.7776 6.85854L10.2456 5.43757L9.57367 3.64272L12.3068 4.66612L18.1631 6.85885L20.8233 7.85481L23.4457 8.83674L24.104 9.08308L19.779 10.6438V10.6434Z'
|
|
49
|
+
fill={color}
|
|
50
|
+
/>
|
|
51
|
+
</Svg>
|
|
52
|
+
)
|
|
@@ -4,31 +4,37 @@ export const sharedStyles = StyleSheet.create({
|
|
|
4
4
|
// Popover styles
|
|
5
5
|
popoverContent: {
|
|
6
6
|
flex: 1,
|
|
7
|
-
paddingHorizontal:
|
|
7
|
+
paddingHorizontal: 0
|
|
8
8
|
},
|
|
9
9
|
popoverHeader: {
|
|
10
|
+
flexDirection: 'column',
|
|
11
|
+
paddingBottom: 8,
|
|
12
|
+
paddingHorizontal: 16,
|
|
13
|
+
borderTopLeftRadius: 20,
|
|
14
|
+
borderTopRightRadius: 20,
|
|
15
|
+
backgroundColor: '#e3ecfd',
|
|
16
|
+
shadowColor: '#e3ecfd',
|
|
17
|
+
shadowOffset: { width: 0, height: 10, },
|
|
18
|
+
shadowOpacity: 1,
|
|
19
|
+
shadowRadius: 5,
|
|
20
|
+
elevation: 3,
|
|
21
|
+
},
|
|
22
|
+
modalHandle: {
|
|
23
|
+
marginTop: 8,
|
|
24
|
+
marginBottom: 16,
|
|
25
|
+
width: 40,
|
|
26
|
+
height: 4,
|
|
27
|
+
backgroundColor: '#D1D5DB',
|
|
28
|
+
borderRadius: 2,
|
|
29
|
+
alignSelf: 'center',
|
|
30
|
+
},
|
|
31
|
+
popoverHeaderContent: {
|
|
10
32
|
flexDirection: 'row',
|
|
11
33
|
justifyContent: 'space-between',
|
|
12
|
-
alignItems: '
|
|
13
|
-
paddingVertical: 16,
|
|
14
|
-
borderBottomWidth: 1,
|
|
15
|
-
borderBottomColor: '#E5E7EB'
|
|
16
|
-
},
|
|
17
|
-
logoText: {
|
|
18
|
-
fontSize: 18,
|
|
19
|
-
fontWeight: 'bold',
|
|
20
|
-
color: '#007AFF'
|
|
21
|
-
},
|
|
22
|
-
closeButton: {
|
|
23
|
-
width: 30,
|
|
24
|
-
height: 30,
|
|
25
|
-
justifyContent: 'center',
|
|
26
|
-
alignItems: 'center'
|
|
27
|
-
},
|
|
28
|
-
closeButtonText: {
|
|
29
|
-
fontSize: 24,
|
|
30
|
-
color: '#6B7280'
|
|
34
|
+
alignItems: 'flex-start',
|
|
31
35
|
},
|
|
36
|
+
|
|
37
|
+
|
|
32
38
|
cancelButton: {
|
|
33
39
|
paddingHorizontal: 12,
|
|
34
40
|
paddingVertical: 6,
|
|
@@ -42,34 +48,34 @@ export const sharedStyles = StyleSheet.create({
|
|
|
42
48
|
},
|
|
43
49
|
popoverBody: {
|
|
44
50
|
flex: 1,
|
|
45
|
-
|
|
51
|
+
padding: 16,
|
|
52
|
+
paddingTop: 32,
|
|
46
53
|
},
|
|
47
54
|
title: {
|
|
48
|
-
fontSize:
|
|
49
|
-
fontWeight: '
|
|
50
|
-
color: '#
|
|
55
|
+
fontSize: 18,
|
|
56
|
+
fontWeight: '600',
|
|
57
|
+
color: '#2d3748',
|
|
51
58
|
marginBottom: 12
|
|
52
59
|
},
|
|
53
60
|
description: {
|
|
54
|
-
fontSize:
|
|
55
|
-
color: '#
|
|
56
|
-
lineHeight: 24,
|
|
61
|
+
fontSize: 14,
|
|
62
|
+
color: '#718096',
|
|
57
63
|
marginBottom: 24
|
|
58
64
|
},
|
|
59
65
|
popoverFooter: {
|
|
60
66
|
marginTop: 'auto',
|
|
61
|
-
paddingTop:
|
|
67
|
+
paddingTop: 48
|
|
62
68
|
},
|
|
63
69
|
actionButton: {
|
|
64
70
|
paddingVertical: 16,
|
|
65
71
|
paddingHorizontal: 24,
|
|
66
|
-
borderRadius:
|
|
72
|
+
borderRadius: 12,
|
|
67
73
|
alignItems: 'center'
|
|
68
74
|
},
|
|
69
75
|
actionButtonText: {
|
|
70
76
|
color: 'white',
|
|
71
77
|
fontSize: 16,
|
|
72
|
-
fontWeight: '
|
|
78
|
+
fontWeight: '500'
|
|
73
79
|
},
|
|
74
80
|
|
|
75
81
|
// Continuous recording styles
|
|
@@ -80,8 +86,15 @@ export const sharedStyles = StyleSheet.create({
|
|
|
80
86
|
marginBottom: 20,
|
|
81
87
|
paddingVertical: 12,
|
|
82
88
|
paddingHorizontal: 16,
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
borderWidth: 1,
|
|
90
|
+
borderColor: '#e1e8f1',
|
|
91
|
+
backgroundColor: '#fff',
|
|
92
|
+
borderRadius: 12,
|
|
93
|
+
shadowColor: '#000',
|
|
94
|
+
shadowOffset: { width: 0, height: 2 },
|
|
95
|
+
shadowOpacity: 0.06,
|
|
96
|
+
shadowRadius: 3,
|
|
97
|
+
elevation: 3,
|
|
85
98
|
},
|
|
86
99
|
continuousRecordingLabel: {
|
|
87
100
|
fontSize: 16,
|
|
@@ -126,12 +139,12 @@ export const sharedStyles = StyleSheet.create({
|
|
|
126
139
|
|
|
127
140
|
// Button color variants
|
|
128
141
|
startButton: {
|
|
129
|
-
backgroundColor: '#
|
|
142
|
+
backgroundColor: '#473cfb'
|
|
130
143
|
},
|
|
131
144
|
stopButton: {
|
|
132
|
-
backgroundColor: '#
|
|
145
|
+
backgroundColor: '#473cfb'
|
|
133
146
|
},
|
|
134
147
|
saveButton: {
|
|
135
|
-
backgroundColor: '#
|
|
148
|
+
backgroundColor: '#473cfb'
|
|
136
149
|
}
|
|
137
150
|
})
|
package/src/components/index.ts
CHANGED
package/src/config/defaults.ts
CHANGED
package/src/config/masking.ts
CHANGED
|
@@ -23,5 +23,6 @@ export const getMaskingConfig = (masking?: MaskingConfig): MaskingConfig => {
|
|
|
23
23
|
isContentMaskingEnabled: isValidBoolean(masking.isContentMaskingEnabled, baseMasking.isContentMaskingEnabled ?? true),
|
|
24
24
|
maskBody: isValidFunction(masking.maskBody, mask(maskBodyFieldsList)),
|
|
25
25
|
maskHeaders: isValidFunction(masking.maskHeaders, mask(maskHeadersList)),
|
|
26
|
+
inputMasking: isValidBoolean(masking.inputMasking, baseMasking.inputMasking ?? true),
|
|
26
27
|
}
|
|
27
28
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NativeModules } from 'react-native'
|
|
2
|
+
|
|
3
|
+
export interface MaskingOptions {
|
|
4
|
+
/** Quality of the captured image (0.1 to 1.0) */
|
|
5
|
+
quality?: number
|
|
6
|
+
/** Whether to mask all input fields automatically */
|
|
7
|
+
inputMasking?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export interface SessionRecorderNativeModule {
|
|
12
|
+
/**
|
|
13
|
+
* Capture the current screen and apply masking to sensitive elements
|
|
14
|
+
* @returns Promise that resolves to base64 encoded image
|
|
15
|
+
*/
|
|
16
|
+
captureAndMask(): Promise<string>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Capture the current screen and apply masking with custom options
|
|
20
|
+
* @param options Custom masking options
|
|
21
|
+
* @returns Promise that resolves to base64 encoded image
|
|
22
|
+
*/
|
|
23
|
+
captureAndMaskWithOptions(options: MaskingOptions): Promise<string>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Get the native module
|
|
27
|
+
const { SessionRecorder } = NativeModules
|
|
28
|
+
|
|
29
|
+
// Validate that the native module is available
|
|
30
|
+
if (!SessionRecorder) {
|
|
31
|
+
console.warn('SessionRecorder native module is not available. Auto-linking may not have completed yet.')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default SessionRecorder as SessionRecorderNativeModule
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NativeModules } from 'react-native'
|
|
2
|
+
|
|
3
|
+
export interface MaskingOptions {
|
|
4
|
+
/** Quality of the captured image (0.1 to 1.0) */
|
|
5
|
+
quality?: number
|
|
6
|
+
/** Whether to mask all input fields automatically */
|
|
7
|
+
inputMasking?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export interface SessionRecorderNativeModule {
|
|
12
|
+
/**
|
|
13
|
+
* Capture the current screen and apply masking to sensitive elements
|
|
14
|
+
* @returns Promise that resolves to base64 encoded image
|
|
15
|
+
*/
|
|
16
|
+
captureAndMask(): Promise<string>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Capture the current screen and apply masking with custom options
|
|
20
|
+
* @param options Custom masking options
|
|
21
|
+
* @returns Promise that resolves to base64 encoded image
|
|
22
|
+
*/
|
|
23
|
+
captureAndMaskWithOptions(options: MaskingOptions): Promise<string>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Get the native module
|
|
27
|
+
const { SessionRecorderNative } = NativeModules
|
|
28
|
+
|
|
29
|
+
// Validate that the native module is available
|
|
30
|
+
if (!SessionRecorderNative) {
|
|
31
|
+
console.warn('SessionRecorderNative module is not available. Auto-linking may not have completed yet.')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default SessionRecorderNative as SessionRecorderNativeModule
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
generateScreenHash,
|
|
11
11
|
logger,
|
|
12
12
|
} from '../utils'
|
|
13
|
+
import { screenMaskingService, ScreenMaskingConfig } from '../services/screenMaskingService'
|
|
13
14
|
|
|
14
15
|
export class ScreenRecorder implements EventRecorder {
|
|
15
16
|
private config?: RecorderConfig
|
|
@@ -18,7 +19,7 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
18
19
|
private captureInterval?: NodeJS.Timeout
|
|
19
20
|
private captureCount: number = 0
|
|
20
21
|
private maxCaptures: number = 100 // Limit captures to prevent memory issues
|
|
21
|
-
private captureQuality: number = 0.
|
|
22
|
+
private captureQuality: number = 0.1
|
|
22
23
|
private captureFormat: 'png' | 'jpg' = 'jpg'
|
|
23
24
|
private screenDimensions: { width: number; height: number } | null = null
|
|
24
25
|
private currentScreen: string | null = null
|
|
@@ -30,11 +31,24 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
30
31
|
private enableChangeDetection: boolean = true
|
|
31
32
|
private hashSampleSize: number = 100
|
|
32
33
|
private currentImageNodeId: number | null = null
|
|
34
|
+
private maskingConfig?: ScreenMaskingConfig
|
|
33
35
|
|
|
34
36
|
init(config: RecorderConfig, eventRecorder?: EventRecorder): void {
|
|
35
37
|
this.config = config
|
|
36
38
|
this.eventRecorder = eventRecorder
|
|
37
39
|
this._getScreenDimensions()
|
|
40
|
+
|
|
41
|
+
// Initialize masking configuration
|
|
42
|
+
this.maskingConfig = {
|
|
43
|
+
enabled: true,
|
|
44
|
+
inputMasking: this.config?.masking?.inputMasking ?? true,
|
|
45
|
+
defaultOptions: {
|
|
46
|
+
quality: this.captureQuality,
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Update the masking service configuration
|
|
51
|
+
screenMaskingService.updateConfig(this.maskingConfig)
|
|
38
52
|
}
|
|
39
53
|
|
|
40
54
|
start(): void {
|
|
@@ -135,6 +149,21 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
135
149
|
|
|
136
150
|
private async _captureScreenBase64(): Promise<string | null> {
|
|
137
151
|
try {
|
|
152
|
+
// Try native masking first if available
|
|
153
|
+
if (screenMaskingService.isScreenMaskingAvailable()) {
|
|
154
|
+
logger.info('ScreenRecorder', 'Using native masking for screen capture')
|
|
155
|
+
const maskedImage = await screenMaskingService.captureMaskedScreen({
|
|
156
|
+
quality: this.captureQuality,
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
if (maskedImage) {
|
|
160
|
+
return maskedImage
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
logger.warn('ScreenRecorder', 'Native masking failed, falling back to view-shot')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Fallback to react-native-view-shot
|
|
138
167
|
if (!this.viewShotRef) {
|
|
139
168
|
logger.warn('ScreenRecorder', 'ViewShot ref not available for screen capture')
|
|
140
169
|
return null
|
|
@@ -451,7 +480,7 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
451
480
|
// Get current configuration
|
|
452
481
|
getConfiguration(): Record<string, any> {
|
|
453
482
|
return {
|
|
454
|
-
captureInterval: this.captureInterval ?
|
|
483
|
+
captureInterval: this.captureInterval ? 2000 : 0, // Default 5 seconds
|
|
455
484
|
captureQuality: this.captureQuality,
|
|
456
485
|
captureFormat: this.captureFormat,
|
|
457
486
|
maxCaptures: this.maxCaptures,
|