@multiplayer-app/session-recorder-react-native 0.0.1-beta.7 → 0.0.1-beta.8
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/docs/NATIVE_MODULE_SETUP.md +175 -0
- package/ios/SessionRecorderNative.podspec +5 -0
- package/package.json +11 -1
- package/plugin/package.json +20 -0
- package/plugin/src/index.js +42 -0
- package/android/src/main/AndroidManifest.xml +0 -2
- package/android/src/main/java/com/multiplayer/sessionrecorder/ScreenMaskingModule.kt +0 -202
- package/android/src/main/java/com/multiplayer/sessionrecorder/ScreenMaskingPackage.kt +0 -16
- package/android/src/main/java/com/multiplayer/sessionrecorder/SessionRecorderModule.kt +0 -202
- package/android/src/main/java/com/multiplayer/sessionrecorder/SessionRecorderPackage.kt +0 -16
- package/babel.config.js +0 -13
- package/docs/AUTO_METADATA_DETECTION.md +0 -108
- package/docs/TROUBLESHOOTING.md +0 -168
- package/ios/ScreenMasking.m +0 -12
- package/ios/ScreenMasking.podspec +0 -21
- package/ios/ScreenMasking.swift +0 -205
- package/ios/SessionRecorder.podspec +0 -21
- package/scripts/generate-app-metadata.js +0 -173
- package/src/components/GestureCaptureWrapper/GestureCaptureWrapper.tsx +0 -86
- package/src/components/GestureCaptureWrapper/index.ts +0 -1
- package/src/components/ScreenRecorderView/ScreenRecorderView.tsx +0 -72
- package/src/components/ScreenRecorderView/index.ts +0 -1
- package/src/components/SessionRecorderWidget/FinalPopover.tsx +0 -62
- package/src/components/SessionRecorderWidget/FloatingButton.tsx +0 -136
- package/src/components/SessionRecorderWidget/InitialPopover.tsx +0 -89
- package/src/components/SessionRecorderWidget/ModalContainer.tsx +0 -128
- package/src/components/SessionRecorderWidget/ModalHeader.tsx +0 -24
- package/src/components/SessionRecorderWidget/SessionRecorderWidget.tsx +0 -109
- package/src/components/SessionRecorderWidget/icons.tsx +0 -52
- package/src/components/SessionRecorderWidget/index.ts +0 -3
- package/src/components/SessionRecorderWidget/styles.ts +0 -150
- package/src/components/index.ts +0 -3
- package/src/config/constants.ts +0 -60
- package/src/config/defaults.ts +0 -83
- package/src/config/index.ts +0 -6
- package/src/config/masking.ts +0 -28
- package/src/config/session-recorder.ts +0 -55
- package/src/config/validators.ts +0 -31
- package/src/context/SessionRecorderContext.tsx +0 -53
- package/src/index.ts +0 -9
- package/src/native/ScreenMasking.ts +0 -34
- package/src/native/SessionRecorderNative.ts +0 -34
- package/src/otel/helpers.ts +0 -275
- package/src/otel/index.ts +0 -138
- package/src/otel/instrumentations/index.ts +0 -115
- package/src/patch/index.ts +0 -1
- package/src/patch/xhr.ts +0 -141
- package/src/recorder/eventExporter.ts +0 -141
- package/src/recorder/gestureRecorder.ts +0 -498
- package/src/recorder/index.ts +0 -179
- package/src/recorder/navigationTracker.ts +0 -449
- package/src/recorder/screenRecorder.ts +0 -527
- package/src/services/api.service.ts +0 -203
- package/src/services/screenMaskingService.ts +0 -118
- package/src/services/storage.service.ts +0 -199
- package/src/session-recorder.ts +0 -606
- package/src/types/expo.d.ts +0 -23
- package/src/types/index.ts +0 -28
- package/src/types/session-recorder.ts +0 -429
- package/src/types/session.ts +0 -65
- package/src/utils/app-metadata.ts +0 -31
- package/src/utils/index.ts +0 -8
- package/src/utils/logger.ts +0 -225
- package/src/utils/nativeModuleTest.ts +0 -60
- package/src/utils/platform.ts +0 -384
- package/src/utils/request-utils.ts +0 -61
- package/src/utils/rrweb-events.ts +0 -309
- package/src/utils/session.ts +0 -18
- package/src/utils/time.ts +0 -17
- package/src/utils/type-utils.ts +0 -75
- package/src/version.ts +0 -1
- package/tsconfig.json +0 -24
- /package/ios/{SessionRecorder.m → SessionRecorderNative.m} +0 -0
- /package/ios/{SessionRecorder.swift → SessionRecorderNative.swift} +0 -0
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Build script to automatically extract app metadata from configuration files
|
|
5
|
-
* This runs without developer intervention and generates app-metadata.ts
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const fs = require('fs')
|
|
9
|
-
const path = require('path')
|
|
10
|
-
|
|
11
|
-
function findProjectRoot() {
|
|
12
|
-
let currentDir = process.cwd()
|
|
13
|
-
|
|
14
|
-
// Look for package.json going up the directory tree
|
|
15
|
-
while (currentDir !== path.dirname(currentDir)) {
|
|
16
|
-
if (fs.existsSync(path.join(currentDir, 'package.json'))) {
|
|
17
|
-
return currentDir
|
|
18
|
-
}
|
|
19
|
-
currentDir = path.dirname(currentDir)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return process.cwd()
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function extractAppMetadata(projectRoot) {
|
|
26
|
-
const metadata = {
|
|
27
|
-
name: undefined,
|
|
28
|
-
version: undefined,
|
|
29
|
-
bundleId: undefined,
|
|
30
|
-
buildNumber: undefined,
|
|
31
|
-
displayName: undefined,
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
try {
|
|
35
|
-
// Method 1: Try app.json
|
|
36
|
-
const appJsonPath = path.join(projectRoot, 'app.json')
|
|
37
|
-
if (fs.existsSync(appJsonPath)) {
|
|
38
|
-
const appConfig = JSON.parse(fs.readFileSync(appJsonPath, 'utf8'))
|
|
39
|
-
|
|
40
|
-
metadata.name = appConfig.name || appConfig.displayName
|
|
41
|
-
metadata.version = appConfig.version
|
|
42
|
-
metadata.displayName = appConfig.displayName
|
|
43
|
-
|
|
44
|
-
// Extract bundle ID from platform-specific configs
|
|
45
|
-
if (appConfig.ios?.bundleIdentifier) {
|
|
46
|
-
metadata.bundleId = appConfig.ios.bundleIdentifier
|
|
47
|
-
} else if (appConfig.android?.package) {
|
|
48
|
-
metadata.bundleId = appConfig.android.package
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (appConfig.ios?.buildNumber) {
|
|
52
|
-
metadata.buildNumber = appConfig.ios.buildNumber.toString()
|
|
53
|
-
} else if (appConfig.android?.versionCode) {
|
|
54
|
-
metadata.buildNumber = appConfig.android.versionCode.toString()
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
console.log('✅ Extracted metadata from app.json')
|
|
58
|
-
return metadata
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Method 2: Try app.config.js
|
|
62
|
-
const appConfigJsPath = path.join(projectRoot, 'app.config.js')
|
|
63
|
-
if (fs.existsSync(appConfigJsPath)) {
|
|
64
|
-
try {
|
|
65
|
-
// Clear require cache to get fresh config
|
|
66
|
-
delete require.cache[require.resolve(appConfigJsPath)]
|
|
67
|
-
const appConfig = require(appConfigJsPath)
|
|
68
|
-
|
|
69
|
-
metadata.name = appConfig.name || appConfig.displayName
|
|
70
|
-
metadata.version = appConfig.version
|
|
71
|
-
metadata.displayName = appConfig.displayName
|
|
72
|
-
|
|
73
|
-
// Extract bundle ID from platform-specific configs
|
|
74
|
-
if (appConfig.ios?.bundleIdentifier) {
|
|
75
|
-
metadata.bundleId = appConfig.ios.bundleIdentifier
|
|
76
|
-
} else if (appConfig.android?.package) {
|
|
77
|
-
metadata.bundleId = appConfig.android.package
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (appConfig.ios?.buildNumber) {
|
|
81
|
-
metadata.buildNumber = appConfig.ios.buildNumber.toString()
|
|
82
|
-
} else if (appConfig.android?.versionCode) {
|
|
83
|
-
metadata.buildNumber = appConfig.android.versionCode.toString()
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
console.log('✅ Extracted metadata from app.config.js')
|
|
87
|
-
return metadata
|
|
88
|
-
} catch (error) {
|
|
89
|
-
console.warn('⚠️ Could not parse app.config.js:', error.message)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Method 3: Fallback to package.json
|
|
94
|
-
const packageJsonPath = path.join(projectRoot, 'package.json')
|
|
95
|
-
if (fs.existsSync(packageJsonPath)) {
|
|
96
|
-
const packageConfig = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
|
97
|
-
|
|
98
|
-
metadata.name = packageConfig.name
|
|
99
|
-
metadata.version = packageConfig.version
|
|
100
|
-
|
|
101
|
-
console.log('✅ Extracted metadata from package.json')
|
|
102
|
-
return metadata
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
} catch (error) {
|
|
106
|
-
console.warn('⚠️ Error extracting app metadata:', error.message)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return metadata
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function generateAppMetadataFile(metadata, outputPath) {
|
|
113
|
-
const content = `/**
|
|
114
|
-
* Auto-generated app metadata
|
|
115
|
-
* This file is generated at build time to provide app metadata without developer intervention
|
|
116
|
-
*/
|
|
117
|
-
|
|
118
|
-
// This file is automatically generated by the build process
|
|
119
|
-
// It extracts metadata from app.json, app.config.js, or package.json
|
|
120
|
-
|
|
121
|
-
export interface AppMetadata {
|
|
122
|
-
name?: string
|
|
123
|
-
version?: string
|
|
124
|
-
bundleId?: string
|
|
125
|
-
buildNumber?: string
|
|
126
|
-
displayName?: string
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Auto-detected values from project configuration files
|
|
130
|
-
export const APP_METADATA: AppMetadata = {
|
|
131
|
-
name: ${metadata.name ? `'${metadata.name}'` : 'undefined'},
|
|
132
|
-
version: ${metadata.version ? `'${metadata.version}'` : 'undefined'},
|
|
133
|
-
bundleId: ${metadata.bundleId ? `'${metadata.bundleId}'` : 'undefined'},
|
|
134
|
-
buildNumber: ${metadata.buildNumber ? `'${metadata.buildNumber}'` : 'undefined'},
|
|
135
|
-
displayName: ${metadata.displayName ? `'${metadata.displayName}'` : 'undefined'},
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Get auto-detected app metadata
|
|
140
|
-
*/
|
|
141
|
-
export function getAutoDetectedAppMetadata(): AppMetadata {
|
|
142
|
-
return { ...APP_METADATA }
|
|
143
|
-
}
|
|
144
|
-
`
|
|
145
|
-
|
|
146
|
-
fs.writeFileSync(outputPath, content, 'utf8')
|
|
147
|
-
console.log(`✅ Generated app-metadata.ts`)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function main() {
|
|
151
|
-
const projectRoot = findProjectRoot()
|
|
152
|
-
console.log(`🔍 Looking for app metadata in: ${projectRoot}`)
|
|
153
|
-
|
|
154
|
-
const metadata = extractAppMetadata(projectRoot)
|
|
155
|
-
|
|
156
|
-
// Show what was detected
|
|
157
|
-
console.log('📋 Detected metadata:')
|
|
158
|
-
Object.entries(metadata).forEach(([key, value]) => {
|
|
159
|
-
if (value) {
|
|
160
|
-
console.log(` ${key}: ${value}`)
|
|
161
|
-
}
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
// Generate the TypeScript file
|
|
165
|
-
const outputPath = path.join(__dirname, '../src/utils/app-metadata.ts')
|
|
166
|
-
generateAppMetadataFile(metadata, outputPath)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (require.main === module) {
|
|
170
|
-
main()
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
module.exports = { extractAppMetadata, generateAppMetadataFile }
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import React, { ReactNode, useCallback, useMemo } from 'react'
|
|
2
|
-
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'
|
|
3
|
-
|
|
4
|
-
export interface GestureCaptureWrapperProps {
|
|
5
|
-
children: ReactNode
|
|
6
|
-
onGestureRecord: (gestureType: string, data: any) => void
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export const GestureCaptureWrapper: React.FC<GestureCaptureWrapperProps> = ({ children, onGestureRecord }) => {
|
|
10
|
-
const recordGesture = useCallback(
|
|
11
|
-
(gestureType: string, data: any) => {
|
|
12
|
-
// Record with session recorder
|
|
13
|
-
onGestureRecord(gestureType, data)
|
|
14
|
-
},
|
|
15
|
-
[onGestureRecord]
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
// Create tap gesture
|
|
19
|
-
const tapGesture = useMemo(() => {
|
|
20
|
-
return Gesture.Tap()
|
|
21
|
-
.runOnJS(true)
|
|
22
|
-
.onStart((event) => {
|
|
23
|
-
recordGesture('tap', {
|
|
24
|
-
x: event.x,
|
|
25
|
-
y: event.y,
|
|
26
|
-
timestamp: Date.now()
|
|
27
|
-
})
|
|
28
|
-
})
|
|
29
|
-
}, [recordGesture])
|
|
30
|
-
|
|
31
|
-
// Create pan gesture (for swipes and drags)
|
|
32
|
-
const panGesture = useMemo(() => {
|
|
33
|
-
return Gesture.Pan()
|
|
34
|
-
.runOnJS(true)
|
|
35
|
-
.onStart((event) => {
|
|
36
|
-
recordGesture('pan_start', {
|
|
37
|
-
x: event.x,
|
|
38
|
-
y: event.y,
|
|
39
|
-
timestamp: Date.now()
|
|
40
|
-
})
|
|
41
|
-
})
|
|
42
|
-
.onUpdate((event) => {
|
|
43
|
-
recordGesture('pan_update', {
|
|
44
|
-
x: event.x,
|
|
45
|
-
y: event.y,
|
|
46
|
-
translationX: event.translationX,
|
|
47
|
-
translationY: event.translationY,
|
|
48
|
-
velocityX: event.velocityX,
|
|
49
|
-
velocityY: event.velocityY,
|
|
50
|
-
timestamp: Date.now()
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
.onEnd((event) => {
|
|
54
|
-
recordGesture('pan_end', {
|
|
55
|
-
x: event.x,
|
|
56
|
-
y: event.y,
|
|
57
|
-
translationX: event.translationX,
|
|
58
|
-
translationY: event.translationY,
|
|
59
|
-
velocityX: event.velocityX,
|
|
60
|
-
velocityY: event.velocityY,
|
|
61
|
-
timestamp: Date.now()
|
|
62
|
-
})
|
|
63
|
-
})
|
|
64
|
-
}, [recordGesture])
|
|
65
|
-
|
|
66
|
-
// Create long press gesture
|
|
67
|
-
const longPressGesture = useMemo(() => {
|
|
68
|
-
return Gesture.LongPress()
|
|
69
|
-
.runOnJS(true)
|
|
70
|
-
.minDuration(500)
|
|
71
|
-
.onStart((event) => {
|
|
72
|
-
recordGesture('long_press', {
|
|
73
|
-
x: event.x,
|
|
74
|
-
y: event.y,
|
|
75
|
-
duration: 500,
|
|
76
|
-
timestamp: Date.now()
|
|
77
|
-
})
|
|
78
|
-
})
|
|
79
|
-
}, [recordGesture])
|
|
80
|
-
|
|
81
|
-
return (
|
|
82
|
-
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
83
|
-
<GestureDetector gesture={Gesture.Simultaneous(tapGesture, panGesture, longPressGesture)}>{children}</GestureDetector>
|
|
84
|
-
</GestureHandlerRootView>
|
|
85
|
-
)
|
|
86
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./GestureCaptureWrapper";
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import SessionRecorder from '@multiplayer-app/session-recorder-react-native'
|
|
2
|
-
import React, { PropsWithChildren, useCallback } from 'react'
|
|
3
|
-
import { View } from 'react-native'
|
|
4
|
-
import { SessionState } from '../../types'
|
|
5
|
-
import { logger } from '../../utils'
|
|
6
|
-
import { GestureCaptureWrapper } from '../GestureCaptureWrapper'
|
|
7
|
-
|
|
8
|
-
interface ScreenRecorderViewProps extends PropsWithChildren {}
|
|
9
|
-
|
|
10
|
-
export const ScreenRecorderView = ({ children }: ScreenRecorderViewProps) => {
|
|
11
|
-
// Set up gesture recording callback
|
|
12
|
-
const handleGestureRecord = useCallback((gestureType: string, data: any) => {
|
|
13
|
-
if (SessionRecorder.sessionState !== SessionState.started) {
|
|
14
|
-
logger.debug('SessionRecorderContext', 'Gesture recording skipped', {
|
|
15
|
-
client: !!SessionRecorder.sessionState,
|
|
16
|
-
sessionState: SessionRecorder.sessionState
|
|
17
|
-
})
|
|
18
|
-
return
|
|
19
|
-
}
|
|
20
|
-
logger.debug('SessionRecorderContext', 'Gesture recorded', { gestureType, data })
|
|
21
|
-
try {
|
|
22
|
-
// Record gesture as appropriate touch events
|
|
23
|
-
switch (gestureType) {
|
|
24
|
-
case 'tap':
|
|
25
|
-
// For tap, record both touch start and end
|
|
26
|
-
logger.debug('SessionRecorderContext', 'Recording tap as touch start + end')
|
|
27
|
-
SessionRecorder.recordTouchStart?.(data.x, data.y, undefined, 1.0)
|
|
28
|
-
SessionRecorder.recordTouchEnd?.(data.x, data.y, undefined, 1.0)
|
|
29
|
-
break
|
|
30
|
-
|
|
31
|
-
case 'pan_start':
|
|
32
|
-
logger.debug('SessionRecorderContext', 'Recording pan_start as touch start')
|
|
33
|
-
SessionRecorder.recordTouchStart?.(data.x, data.y, undefined, 1.0)
|
|
34
|
-
break
|
|
35
|
-
|
|
36
|
-
case 'pan_update':
|
|
37
|
-
logger.debug('SessionRecorderContext', 'Recording pan_update as touch move')
|
|
38
|
-
SessionRecorder.recordTouchMove?.(data.x, data.y, undefined, 1.0)
|
|
39
|
-
break
|
|
40
|
-
|
|
41
|
-
case 'pan_end':
|
|
42
|
-
logger.debug('SessionRecorderContext', 'Recording pan_end as touch end')
|
|
43
|
-
SessionRecorder.recordTouchEnd?.(data.x, data.y, undefined, 1.0)
|
|
44
|
-
break
|
|
45
|
-
|
|
46
|
-
case 'long_press':
|
|
47
|
-
logger.debug('SessionRecorderContext', 'Recording long_press as touch start + end')
|
|
48
|
-
SessionRecorder.recordTouchStart?.(data.x, data.y, undefined, 1.0)
|
|
49
|
-
SessionRecorder.recordTouchEnd?.(data.x, data.y, undefined, 1.0)
|
|
50
|
-
break
|
|
51
|
-
default:
|
|
52
|
-
}
|
|
53
|
-
} catch (error) {
|
|
54
|
-
logger.error('SessionRecorderContext', 'Failed to record gesture event', error)
|
|
55
|
-
}
|
|
56
|
-
}, [])
|
|
57
|
-
|
|
58
|
-
// Callback ref to set the viewshot ref immediately when available
|
|
59
|
-
const setViewShotRef = (ref: View | null) => {
|
|
60
|
-
if (ref) {
|
|
61
|
-
SessionRecorder.setViewShotRef?.(ref)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return (
|
|
66
|
-
<GestureCaptureWrapper onGestureRecord={handleGestureRecord}>
|
|
67
|
-
<View ref={setViewShotRef} style={{ flex: 1 }}>
|
|
68
|
-
{children}
|
|
69
|
-
</View>
|
|
70
|
-
</GestureCaptureWrapper>
|
|
71
|
-
)
|
|
72
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./ScreenRecorderView";
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
2
|
-
import { View, Text, Pressable, TextInput, Alert } from 'react-native'
|
|
3
|
-
import { WidgetTextOverridesConfig } from '../../types'
|
|
4
|
-
import { sharedStyles } from './styles'
|
|
5
|
-
import ModalHeader from './ModalHeader'
|
|
6
|
-
|
|
7
|
-
interface FinalPopoverProps {
|
|
8
|
-
textOverrides: WidgetTextOverridesConfig
|
|
9
|
-
onStopRecording: (comment: string) => void
|
|
10
|
-
onCancelSession: () => void
|
|
11
|
-
onClose: () => void
|
|
12
|
-
isSubmitting: boolean
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const FinalPopover: React.FC<FinalPopoverProps> = ({ textOverrides, onStopRecording, onCancelSession, isSubmitting }) => {
|
|
16
|
-
const [comment, setComment] = useState('')
|
|
17
|
-
|
|
18
|
-
const handleStopRecording = async () => {
|
|
19
|
-
try {
|
|
20
|
-
await onStopRecording(comment)
|
|
21
|
-
} catch (error) {
|
|
22
|
-
Alert.alert('Error', 'Failed to save session')
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<View style={sharedStyles.popoverContent}>
|
|
28
|
-
<ModalHeader>
|
|
29
|
-
<Pressable onPress={onCancelSession} style={sharedStyles.cancelButton}>
|
|
30
|
-
<Text style={sharedStyles.cancelButtonText}>{textOverrides.cancelButtonText}</Text>
|
|
31
|
-
</Pressable>
|
|
32
|
-
</ModalHeader>
|
|
33
|
-
|
|
34
|
-
<View style={sharedStyles.popoverBody}>
|
|
35
|
-
<Text style={sharedStyles.title}>{textOverrides.finalTitle}</Text>
|
|
36
|
-
<Text style={sharedStyles.description}>{textOverrides.finalDescription}</Text>
|
|
37
|
-
|
|
38
|
-
<TextInput
|
|
39
|
-
style={sharedStyles.commentInput}
|
|
40
|
-
placeholder={textOverrides.commentPlaceholder}
|
|
41
|
-
value={comment}
|
|
42
|
-
onChangeText={setComment}
|
|
43
|
-
multiline
|
|
44
|
-
numberOfLines={3}
|
|
45
|
-
textAlignVertical='top'
|
|
46
|
-
/>
|
|
47
|
-
|
|
48
|
-
<View style={sharedStyles.popoverFooter}>
|
|
49
|
-
<Pressable
|
|
50
|
-
style={[sharedStyles.actionButton, sharedStyles.stopButton]}
|
|
51
|
-
onPress={handleStopRecording}
|
|
52
|
-
disabled={isSubmitting}
|
|
53
|
-
>
|
|
54
|
-
<Text style={sharedStyles.actionButtonText}>{isSubmitting ? 'Saving...' : textOverrides.saveButtonText}</Text>
|
|
55
|
-
</Pressable>
|
|
56
|
-
</View>
|
|
57
|
-
</View>
|
|
58
|
-
</View>
|
|
59
|
-
)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export default FinalPopover
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import React, { useRef, useEffect, useMemo } from 'react'
|
|
2
|
-
import { StyleSheet, Platform, Animated, PanResponder, View, Dimensions } from 'react-native'
|
|
3
|
-
import { SessionState } from '../../types'
|
|
4
|
-
import { StorageService } from '../../services/storage.service'
|
|
5
|
-
import { RecordIcon, CapturingIcon, PausedIcon } from './icons'
|
|
6
|
-
|
|
7
|
-
interface FloatingButtonProps {
|
|
8
|
-
sessionState: SessionState | null
|
|
9
|
-
onPress: () => void
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const buttonSize = 52
|
|
13
|
-
const rightOffset = 20
|
|
14
|
-
const topOffset = Platform.OS === 'ios' ? 60 : 40
|
|
15
|
-
|
|
16
|
-
const FloatingButton: React.FC<FloatingButtonProps> = ({ sessionState, onPress }) => {
|
|
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
|
|
20
|
-
|
|
21
|
-
const screenBounds = useMemo(() => {
|
|
22
|
-
const { width, height } = Dimensions.get('window')
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
minTop: topOffset,
|
|
26
|
-
maxTop: height - buttonSize,
|
|
27
|
-
minRight: 0,
|
|
28
|
-
maxRight: width - buttonSize
|
|
29
|
-
}
|
|
30
|
-
}, [])
|
|
31
|
-
|
|
32
|
-
// Load saved position on component mount
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
const savedPosition = storageService.getFloatingButtonPosition()
|
|
35
|
-
if (savedPosition) {
|
|
36
|
-
const { width } = Dimensions.get('window')
|
|
37
|
-
const top = savedPosition.y
|
|
38
|
-
const right = width - savedPosition.x - buttonSize
|
|
39
|
-
lastPosition.current = { top, right }
|
|
40
|
-
position.setValue({ x: right, y: top })
|
|
41
|
-
} else {
|
|
42
|
-
position.setValue({ x: lastPosition.current.right, y: lastPosition.current.top })
|
|
43
|
-
}
|
|
44
|
-
}, [])
|
|
45
|
-
|
|
46
|
-
const panResponder = useRef(
|
|
47
|
-
PanResponder.create({
|
|
48
|
-
onStartShouldSetPanResponder: () => true,
|
|
49
|
-
onMoveShouldSetPanResponder: (evt, gestureState) => {
|
|
50
|
-
const distance = Math.sqrt(gestureState.dx * gestureState.dx + gestureState.dy * gestureState.dy)
|
|
51
|
-
return distance > 5
|
|
52
|
-
},
|
|
53
|
-
onPanResponderGrant: () => {
|
|
54
|
-
// Set the initial position for this gesture
|
|
55
|
-
position.setValue({ x: lastPosition.current.right, y: lastPosition.current.top })
|
|
56
|
-
},
|
|
57
|
-
onPanResponderMove: (evt, gestureState) => {
|
|
58
|
-
// Calculate new position based on gesture movement
|
|
59
|
-
const newTop = lastPosition.current.top + gestureState.dy
|
|
60
|
-
const newRight = lastPosition.current.right - gestureState.dx
|
|
61
|
-
|
|
62
|
-
// Update position during drag
|
|
63
|
-
position.setValue({ x: newRight, y: newTop })
|
|
64
|
-
},
|
|
65
|
-
onPanResponderRelease: (e, gestureState) => {
|
|
66
|
-
// Check if this was actually a drag (significant movement)
|
|
67
|
-
const distance = Math.sqrt(gestureState.dx * gestureState.dx + gestureState.dy * gestureState.dy)
|
|
68
|
-
|
|
69
|
-
// If it was a tap (no significant movement), trigger onPress
|
|
70
|
-
if (distance <= 5) {
|
|
71
|
-
onPress()
|
|
72
|
-
} else {
|
|
73
|
-
// Calculate new position after dragging
|
|
74
|
-
const newTop = lastPosition.current.top + gestureState.dy
|
|
75
|
-
const newRight = lastPosition.current.right - gestureState.dx
|
|
76
|
-
|
|
77
|
-
// Clamp to screen bounds
|
|
78
|
-
const clampedTop = Math.max(screenBounds.minTop, Math.min(screenBounds.maxTop, newTop))
|
|
79
|
-
const clampedRight = Math.max(screenBounds.minRight, Math.min(screenBounds.maxRight, newRight))
|
|
80
|
-
|
|
81
|
-
// Update position
|
|
82
|
-
lastPosition.current = { top: clampedTop, right: clampedRight }
|
|
83
|
-
position.setValue({ x: clampedRight, y: clampedTop })
|
|
84
|
-
|
|
85
|
-
// Convert back to x,y coordinates for storage
|
|
86
|
-
const { width } = Dimensions.get('window')
|
|
87
|
-
const storagePosition = {
|
|
88
|
-
x: width - clampedRight - buttonSize,
|
|
89
|
-
y: clampedTop
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// Persist position to AsyncStorage (debounced)
|
|
93
|
-
storageService.saveFloatingButtonPosition(storagePosition)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
})
|
|
97
|
-
).current
|
|
98
|
-
|
|
99
|
-
// Memoized button icon and color for performance
|
|
100
|
-
const content = useMemo(() => {
|
|
101
|
-
switch (sessionState) {
|
|
102
|
-
case SessionState.started:
|
|
103
|
-
return { icon: <CapturingIcon size={28} color='white' />, color: '#FF4444' }
|
|
104
|
-
case SessionState.paused:
|
|
105
|
-
return { icon: <PausedIcon size={28} color='white' />, color: '#FFA500' }
|
|
106
|
-
default:
|
|
107
|
-
return { icon: <RecordIcon size={28} color='#718096' />, color: '#ffffff' }
|
|
108
|
-
}
|
|
109
|
-
}, [sessionState])
|
|
110
|
-
|
|
111
|
-
return (
|
|
112
|
-
<Animated.View style={[styles.draggableButton, { top: position.y, right: position.x }]} {...panResponder.panHandlers}>
|
|
113
|
-
<View style={[styles.floatingButton, { backgroundColor: content.color }]}>{content.icon}</View>
|
|
114
|
-
</Animated.View>
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const styles = StyleSheet.create({
|
|
119
|
-
draggableButton: {
|
|
120
|
-
position: 'absolute'
|
|
121
|
-
},
|
|
122
|
-
floatingButton: {
|
|
123
|
-
elevation: 8,
|
|
124
|
-
shadowRadius: 4,
|
|
125
|
-
width: buttonSize,
|
|
126
|
-
shadowColor: '#000',
|
|
127
|
-
height: buttonSize,
|
|
128
|
-
shadowOpacity: 0.25,
|
|
129
|
-
alignItems: 'center',
|
|
130
|
-
justifyContent: 'center',
|
|
131
|
-
borderRadius: buttonSize / 2,
|
|
132
|
-
shadowOffset: { width: 0, height: 2 }
|
|
133
|
-
}
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
export default FloatingButton
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
2
|
-
import { View, Text, Pressable, Alert, Switch } from 'react-native'
|
|
3
|
-
import { SessionType } from '@multiplayer-app/session-recorder-common'
|
|
4
|
-
import { WidgetTextOverridesConfig } from '../../types'
|
|
5
|
-
import { sharedStyles } from './styles'
|
|
6
|
-
import ModalHeader from './ModalHeader'
|
|
7
|
-
|
|
8
|
-
interface InitialPopoverProps {
|
|
9
|
-
textOverrides: WidgetTextOverridesConfig
|
|
10
|
-
showContinuousRecording: boolean
|
|
11
|
-
onStartRecording: (sessionType: SessionType) => void
|
|
12
|
-
onSaveContinuousSession: () => void
|
|
13
|
-
onClose: () => void
|
|
14
|
-
isSubmitting: boolean
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const InitialPopover: React.FC<InitialPopoverProps> = ({
|
|
18
|
-
textOverrides,
|
|
19
|
-
showContinuousRecording,
|
|
20
|
-
onStartRecording,
|
|
21
|
-
onSaveContinuousSession,
|
|
22
|
-
onClose,
|
|
23
|
-
isSubmitting
|
|
24
|
-
}) => {
|
|
25
|
-
const [continuousRecording, setContinuousRecording] = useState(false)
|
|
26
|
-
|
|
27
|
-
const handleStartRecording = async () => {
|
|
28
|
-
try {
|
|
29
|
-
const sessionType = continuousRecording ? SessionType.CONTINUOUS : SessionType.PLAIN
|
|
30
|
-
onStartRecording(sessionType)
|
|
31
|
-
} catch (error) {
|
|
32
|
-
Alert.alert('Error', 'Failed to start recording')
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<View style={sharedStyles.popoverContent}>
|
|
38
|
-
<ModalHeader />
|
|
39
|
-
|
|
40
|
-
<View style={sharedStyles.popoverBody}>
|
|
41
|
-
{showContinuousRecording && (
|
|
42
|
-
<View style={sharedStyles.continuousRecordingSection}>
|
|
43
|
-
<Text style={sharedStyles.continuousRecordingLabel}>{textOverrides.continuousRecordingLabel}</Text>
|
|
44
|
-
<Switch
|
|
45
|
-
value={continuousRecording}
|
|
46
|
-
onValueChange={setContinuousRecording}
|
|
47
|
-
trackColor={{ false: '#767577', true: '#81b0ff' }}
|
|
48
|
-
thumbColor={continuousRecording ? '#007AFF' : '#f4f3f4'}
|
|
49
|
-
/>
|
|
50
|
-
</View>
|
|
51
|
-
)}
|
|
52
|
-
|
|
53
|
-
<Text style={sharedStyles.title}>
|
|
54
|
-
{showContinuousRecording ? textOverrides.initialTitleWithContinuous : textOverrides.initialTitleWithoutContinuous}
|
|
55
|
-
</Text>
|
|
56
|
-
|
|
57
|
-
<Text style={sharedStyles.description}>
|
|
58
|
-
{showContinuousRecording
|
|
59
|
-
? textOverrides.initialDescriptionWithContinuous
|
|
60
|
-
: textOverrides.initialDescriptionWithoutContinuous}
|
|
61
|
-
</Text>
|
|
62
|
-
|
|
63
|
-
<View style={sharedStyles.popoverFooter}>
|
|
64
|
-
<Pressable style={[sharedStyles.actionButton, sharedStyles.startButton]} onPress={handleStartRecording}>
|
|
65
|
-
<Text style={sharedStyles.actionButtonText}>{textOverrides.startRecordingButtonText}</Text>
|
|
66
|
-
</Pressable>
|
|
67
|
-
</View>
|
|
68
|
-
|
|
69
|
-
{showContinuousRecording && continuousRecording && (
|
|
70
|
-
<View style={sharedStyles.continuousOverlay}>
|
|
71
|
-
<View style={sharedStyles.continuousOverlayContent}>
|
|
72
|
-
<Text style={sharedStyles.continuousOverlayTitle}>🔴 {textOverrides.continuousOverlayTitle}</Text>
|
|
73
|
-
<Text style={sharedStyles.continuousOverlayDescription}>{textOverrides.continuousOverlayDescription}</Text>
|
|
74
|
-
</View>
|
|
75
|
-
<Pressable
|
|
76
|
-
style={[sharedStyles.actionButton, sharedStyles.saveButton]}
|
|
77
|
-
onPress={onSaveContinuousSession}
|
|
78
|
-
disabled={isSubmitting}
|
|
79
|
-
>
|
|
80
|
-
<Text style={sharedStyles.actionButtonText}>{textOverrides.saveLastSnapshotButtonText}</Text>
|
|
81
|
-
</Pressable>
|
|
82
|
-
</View>
|
|
83
|
-
)}
|
|
84
|
-
</View>
|
|
85
|
-
</View>
|
|
86
|
-
)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export default InitialPopover
|