yaver-feedback-react-native 0.5.0 → 0.5.3
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/package.json +5 -4
- package/src/ShakeDetector.ts +95 -35
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yaver-feedback-react-native",
|
|
3
|
-
"version": "0.5.
|
|
4
|
-
"description": "Visual feedback SDK for Yaver —
|
|
3
|
+
"version": "0.5.3",
|
|
4
|
+
"description": "Visual feedback SDK for Yaver — bug reports, screen recording, voice annotations, and local-first developer workflows",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
7
7
|
"files": [
|
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
"feedback",
|
|
51
51
|
"bug-report",
|
|
52
52
|
"screen-recording",
|
|
53
|
-
"
|
|
54
|
-
"
|
|
53
|
+
"react-native",
|
|
54
|
+
"developer-runtime",
|
|
55
|
+
"local-first"
|
|
55
56
|
]
|
|
56
57
|
}
|
package/src/ShakeDetector.ts
CHANGED
|
@@ -1,57 +1,117 @@
|
|
|
1
1
|
import { DeviceEventEmitter, Platform } from 'react-native';
|
|
2
2
|
|
|
3
|
-
const SHAKE_THRESHOLD = 1.5; // g-force threshold
|
|
4
3
|
const SHAKE_TIMEOUT_MS = 1000; // minimum time between shakes
|
|
5
|
-
const
|
|
4
|
+
const ACCEL_THRESHOLD_G = 1.8; // peak g-force that qualifies as a shake event
|
|
5
|
+
const ACCEL_MIN_HITS = 3; // peaks within the window before we fire
|
|
6
|
+
const ACCEL_WINDOW_MS = 800; // rolling window
|
|
7
|
+
const ACCEL_SAMPLE_INTERVAL_MS = 80; // ≈12Hz — cheap but catches shakes
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Detects device shake gestures.
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
+
* Two detection paths run in parallel so shake works in both Debug and
|
|
13
|
+
* Release / TestFlight builds:
|
|
14
|
+
*
|
|
15
|
+
* 1. React Native's built-in `shakeEvent` (iOS) / `ShakeEvent` (Android)
|
|
16
|
+
* on `DeviceEventEmitter`. RN emits `shakeEvent` only in Debug mode
|
|
17
|
+
* on iOS, which is why TestFlight builds never saw a shake prior to
|
|
18
|
+
* SDK 0.5.1.
|
|
19
|
+
* 2. Accelerometer-based fallback via `expo-sensors` (optional peer
|
|
20
|
+
* dep). When the host app has `expo-sensors` installed we subscribe
|
|
21
|
+
* to the accelerometer and fire on a burst of above-threshold peaks.
|
|
22
|
+
* This path works identically in Debug, Release, and TestFlight.
|
|
23
|
+
*
|
|
24
|
+
* Both paths share a 1-second debounce so a single shake never fires the
|
|
25
|
+
* callback twice.
|
|
12
26
|
*/
|
|
13
27
|
export class ShakeDetector {
|
|
14
|
-
private
|
|
28
|
+
private devMenuSub: { remove(): void } | null = null;
|
|
29
|
+
private accelSub: { remove(): void } | null = null;
|
|
15
30
|
private lastShakeTime = 0;
|
|
31
|
+
private peakTimestamps: number[] = [];
|
|
16
32
|
|
|
17
|
-
/**
|
|
18
|
-
* Start listening for shake gestures.
|
|
19
|
-
* @param onShake - Callback invoked when a shake is detected.
|
|
20
|
-
*/
|
|
21
33
|
start(onShake: () => void): void {
|
|
22
34
|
this.stop();
|
|
35
|
+
this.subscribeDevMenu(onShake);
|
|
36
|
+
this.subscribeAccelerometer(onShake);
|
|
37
|
+
}
|
|
23
38
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
this.
|
|
28
|
-
const now = Date.now();
|
|
29
|
-
if (now - this.lastShakeTime > SHAKE_TIMEOUT_MS) {
|
|
30
|
-
this.lastShakeTime = now;
|
|
31
|
-
onShake();
|
|
32
|
-
}
|
|
33
|
-
});
|
|
34
|
-
return;
|
|
39
|
+
stop(): void {
|
|
40
|
+
if (this.devMenuSub) {
|
|
41
|
+
this.devMenuSub.remove();
|
|
42
|
+
this.devMenuSub = null;
|
|
35
43
|
}
|
|
44
|
+
if (this.accelSub) {
|
|
45
|
+
this.accelSub.remove();
|
|
46
|
+
this.accelSub = null;
|
|
47
|
+
}
|
|
48
|
+
this.peakTimestamps = [];
|
|
49
|
+
}
|
|
36
50
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
private fire(onShake: () => void): void {
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
if (now - this.lastShakeTime <= SHAKE_TIMEOUT_MS) return;
|
|
54
|
+
this.lastShakeTime = now;
|
|
55
|
+
this.peakTimestamps = [];
|
|
56
|
+
onShake();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Dev-menu / platform-native event listener. On iOS this is the
|
|
61
|
+
* `shakeEvent` RN posts from RCTDevMenu (Debug only). On Android it is
|
|
62
|
+
* the `ShakeEvent` name a handful of third-party shake libraries emit.
|
|
63
|
+
*/
|
|
64
|
+
private subscribeDevMenu(onShake: () => void): void {
|
|
65
|
+
const eventName = Platform.OS === 'ios' ? 'shakeEvent' : 'ShakeEvent';
|
|
66
|
+
this.devMenuSub = DeviceEventEmitter.addListener(eventName, () => {
|
|
67
|
+
this.fire(onShake);
|
|
47
68
|
});
|
|
48
69
|
}
|
|
49
70
|
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Accelerometer-based detection. Uses `expo-sensors` when available —
|
|
73
|
+
* the host app's own dependency, not the SDK's, so apps that don't
|
|
74
|
+
* want the extra native surface get the Dev-menu path only.
|
|
75
|
+
*/
|
|
76
|
+
private subscribeAccelerometer(onShake: () => void): void {
|
|
77
|
+
let Accelerometer: {
|
|
78
|
+
setUpdateInterval: (ms: number) => void;
|
|
79
|
+
addListener: (cb: (d: { x: number; y: number; z: number }) => void) => {
|
|
80
|
+
remove(): void;
|
|
81
|
+
};
|
|
82
|
+
} | null = null;
|
|
83
|
+
try {
|
|
84
|
+
// Optional peer dep — if it isn't installed, we simply skip this path.
|
|
85
|
+
Accelerometer = require('expo-sensors').Accelerometer;
|
|
86
|
+
} catch {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!Accelerometer) return;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
Accelerometer.setUpdateInterval(ACCEL_SAMPLE_INTERVAL_MS);
|
|
93
|
+
} catch {
|
|
94
|
+
// Some platforms reject zero-value intervals; fall through with defaults
|
|
55
95
|
}
|
|
96
|
+
|
|
97
|
+
this.accelSub = Accelerometer.addListener(({ x, y, z }) => {
|
|
98
|
+
// Magnitude of acceleration vector (in g). Subtract 1 so a stationary
|
|
99
|
+
// device reports ~0 rather than the 1g of gravity.
|
|
100
|
+
const mag = Math.sqrt(x * x + y * y + z * z);
|
|
101
|
+
if (mag - 1 < ACCEL_THRESHOLD_G - 1) return;
|
|
102
|
+
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
this.peakTimestamps.push(now);
|
|
105
|
+
// Drop peaks that fell out of the rolling window.
|
|
106
|
+
while (
|
|
107
|
+
this.peakTimestamps.length > 0 &&
|
|
108
|
+
now - this.peakTimestamps[0] > ACCEL_WINDOW_MS
|
|
109
|
+
) {
|
|
110
|
+
this.peakTimestamps.shift();
|
|
111
|
+
}
|
|
112
|
+
if (this.peakTimestamps.length >= ACCEL_MIN_HITS) {
|
|
113
|
+
this.fire(onShake);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
56
116
|
}
|
|
57
117
|
}
|