Skip to content

Commit 987f3da

Browse files
Merge branch 'dev' into feat/havpe-refresh
2 parents 656fc9e + 75f0d70 commit 987f3da

53 files changed

Lines changed: 3641 additions & 1729 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/app.json

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"expo": {
33
"name": "chronicle",
4-
"slug": "chronicle",
4+
"slug": "friend-lite-app",
55
"version": "1.0.0",
66
"scheme": "chronicle",
77
"orientation": "portrait",
@@ -19,11 +19,13 @@
1919
"supportsTablet": true,
2020
"bundleIdentifier": "com.cupbearer5517.chronicle",
2121
"infoPlist": {
22+
"NSCameraUsageDescription": "Chronicle uses the camera to scan QR codes for backend connection setup.",
2223
"NSMicrophoneUsageDescription": "Chronicle needs access to your microphone to stream audio to the backend for processing.",
2324
"NSAppTransportSecurity": {
2425
"NSAllowsArbitraryLoads": true,
2526
"NSAllowsLocalNetworking": true
26-
}
27+
},
28+
"ITSAppUsesNonExemptEncryption": false
2729
}
2830
},
2931
"android": {
@@ -40,7 +42,8 @@
4042
"android.permission.FOREGROUND_SERVICE",
4143
"android.permission.FOREGROUND_SERVICE_DATA_SYNC",
4244
"android.permission.POST_NOTIFICATIONS",
43-
"android.permission.RECORD_AUDIO"
45+
"android.permission.RECORD_AUDIO",
46+
"android.permission.CAMERA"
4447
],
4548
"usesCleartextTraffic": true
4649
},
@@ -53,7 +56,9 @@
5356
"enableNotifications": true,
5457
"enableBackgroundAudio": true,
5558
"enableDeviceDetection": true,
56-
"iosBackgroundModes": { "useProcessing": true },
59+
"iosBackgroundModes": {
60+
"useProcessing": true
61+
},
5762
"iosConfig": {
5863
"microphoneUsageDescription": "We use the mic for live audio streaming"
5964
}
@@ -96,12 +101,20 @@
96101
}
97102
}
98103
],
104+
[
105+
"expo-camera",
106+
{
107+
"cameraPermission": "Chronicle uses the camera to scan QR codes for backend connection setup."
108+
}
109+
],
110+
"expo-image-picker",
99111
"./plugins/with-ats"
100112
],
113+
"owner": "cupbearer5517",
101114
"extra": {
102115
"eas": {
103116
"projectId": "05d8598e-6fe7-4373-81e4-1654f3d8e181"
104117
}
105118
}
106119
}
107-
}
120+
}

app/app/diagnostics.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const EVENT_BADGE_COLORS: Record<ConnectionEventType, string> = {
1717
error: '#FF3B30',
1818
health_ping: '#34C759',
1919
reconnect_attempt: '#FF9500',
20+
reconnect_backoff: '#FF9500',
2021
bt_state_change: '#5856D6',
2122
};
2223

app/app/index.tsx

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,16 +117,17 @@ export default function App() {
117117

118118
const canScan = React.useMemo(() => (
119119
permissionGranted && bluetoothState === BluetoothState.PoweredOn &&
120-
!autoReconnect.isAttemptingAutoReconnect && !deviceConnection.isConnecting &&
120+
!autoReconnect.isAttemptingAutoReconnect && !autoReconnect.isRetryingConnection &&
121+
!deviceConnection.isConnecting &&
121122
!deviceConnection.connectedDeviceId &&
122123
(autoReconnect.triedAutoReconnectForCurrentId || !autoReconnect.lastKnownDeviceId)
123-
), [permissionGranted, bluetoothState, autoReconnect.isAttemptingAutoReconnect, deviceConnection.isConnecting, deviceConnection.connectedDeviceId, autoReconnect.triedAutoReconnectForCurrentId, autoReconnect.lastKnownDeviceId]);
124+
), [permissionGranted, bluetoothState, autoReconnect.isAttemptingAutoReconnect, autoReconnect.isRetryingConnection, deviceConnection.isConnecting, deviceConnection.connectedDeviceId, autoReconnect.triedAutoReconnectForCurrentId, autoReconnect.lastKnownDeviceId]);
124125

125126
const filteredDevices = React.useMemo(() => {
126127
if (!showOnlyOmi) return scannedDevices;
127128
return scannedDevices.filter(d => {
128129
const name = d.name?.toLowerCase() || '';
129-
return name.includes('omi') || name.includes('friend');
130+
return name.includes('omi') || name.includes('friend') || name.includes('neo');
130131
});
131132
}, [scannedDevices, showOnlyOmi]);
132133

@@ -190,6 +191,21 @@ export default function App() {
190191
<BluetoothStatusBanner bluetoothState={bluetoothState} isPermissionsLoading={isPermissionsLoading} permissionGranted={permissionGranted} onRequestPermission={requestBluetoothPermission} />
191192
<ScanControls scanning={scanning} onScanPress={startScan} onStopScanPress={stopDeviceScanAction} canScan={canScan} />
192193

194+
{autoReconnect.isRetryingConnection && (
195+
<View style={s.retryBanner}>
196+
<ActivityIndicator size="small" color={colors.warning} />
197+
<Text style={s.retryBannerText}>
198+
Reconnecting in {autoReconnect.retryBackoffSeconds}s... (attempt {autoReconnect.connectionRetryCount})
199+
</Text>
200+
<TouchableOpacity
201+
style={[s.button, { backgroundColor: colors.danger, paddingVertical: 6, paddingHorizontal: 10 }]}
202+
onPress={autoReconnect.handleCancelAutoReconnect}
203+
>
204+
<Text style={s.buttonText}>Cancel</Text>
205+
</TouchableOpacity>
206+
</View>
207+
)}
208+
193209
{!settings.isAuthenticated && (
194210
<View style={s.authWarning}>
195211
<Text style={s.authWarningText}>Login is required for advanced backend features. Simple backend can be used without authentication.</Text>
@@ -201,7 +217,7 @@ export default function App() {
201217
<View style={s.sectionHeaderWithFilter}>
202218
<Text style={s.sectionTitle}>Found Devices</Text>
203219
<View style={s.filterContainer}>
204-
<Text style={s.filterText}>Show only OMI/Friend</Text>
220+
<Text style={s.filterText}>Show only OMI/Friend/Neo</Text>
205221
<Switch
206222
trackColor={{ false: colors.disabled, true: colors.primary }}
207223
thumbColor={showOnlyOmi ? colors.warning : colors.card}
@@ -222,7 +238,7 @@ export default function App() {
222238
) : (
223239
<View style={s.noDevicesContainer}>
224240
<Text style={s.noDevicesText}>
225-
{showOnlyOmi ? `No OMI/Friend devices found. ${scannedDevices.length} other device(s) hidden by filter.` : 'No devices found.'}
241+
{showOnlyOmi ? `No OMI/Friend/Neo devices found. ${scannedDevices.length} other device(s) hidden by filter.` : 'No devices found.'}
226242
</Text>
227243
</View>
228244
)}
@@ -408,6 +424,23 @@ const createStyles = (colors: ThemeColors) => StyleSheet.create({
408424
textAlign: 'center',
409425
fontStyle: 'italic',
410426
},
427+
retryBanner: {
428+
flexDirection: 'row',
429+
alignItems: 'center',
430+
padding: 12,
431+
marginBottom: 15,
432+
backgroundColor: colors.card,
433+
borderRadius: 8,
434+
borderWidth: 1,
435+
borderColor: colors.warning,
436+
},
437+
retryBannerText: {
438+
flex: 1,
439+
marginLeft: 10,
440+
fontSize: 14,
441+
color: colors.warning,
442+
fontWeight: '500',
443+
},
411444
authWarning: {
412445
marginBottom: 20,
413446
padding: 15,

app/package-lock.json

Lines changed: 43 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
"setimmediate": "^1.0.5",
3131
"webidl-conversions": "^7.0.0",
3232
"react-native-screens": "~4.11.1",
33-
"react-native-safe-area-context": "5.4.0"
33+
"react-native-safe-area-context": "5.4.0",
34+
"expo-camera": "~16.1.11",
35+
"expo-image-picker": "~16.1.4"
3436
},
3537
"devDependencies": {
3638
"@babel/core": "^7.20.0",

app/src/components/BackendStatus.tsx

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import React, { useState, useEffect } from 'react';
22
import { View, Text, TextInput, TouchableOpacity, StyleSheet, Alert, ActivityIndicator } from 'react-native';
33
import { useTheme, ThemeColors } from '../theme';
4+
import { QRScanner } from './QRScanner';
5+
import { httpUrlToWebSocketUrl } from '../utils/urlConversion';
46

57
interface BackendStatusProps {
68
backendUrl: string;
@@ -26,6 +28,7 @@ export const BackendStatus: React.FC<BackendStatusProps> = ({
2628
status: 'unknown',
2729
message: 'Not checked',
2830
});
31+
const [showQRScanner, setShowQRScanner] = useState(false);
2932

3033
const checkBackendHealth = async (showAlert: boolean = false) => {
3134
if (!backendUrl.trim()) {
@@ -130,6 +133,13 @@ export const BackendStatus: React.FC<BackendStatusProps> = ({
130133
)}
131134
</View>
132135

136+
<TouchableOpacity
137+
style={s.qrButton}
138+
onPress={() => setShowQRScanner(true)}
139+
>
140+
<Text style={s.qrButtonText}>Scan QR Code</Text>
141+
</TouchableOpacity>
142+
133143
<TouchableOpacity
134144
style={[s.button, healthStatus.status === 'checking' ? s.buttonDisabled : null]}
135145
onPress={() => checkBackendHealth(true)}
@@ -139,8 +149,17 @@ export const BackendStatus: React.FC<BackendStatusProps> = ({
139149
</TouchableOpacity>
140150

141151
<Text style={s.helpText}>
142-
Enter the WebSocket URL of your backend server. Status is automatically checked.
152+
Enter the WebSocket URL or scan a QR code from the Chronicle dashboard.
143153
</Text>
154+
155+
<QRScanner
156+
visible={showQRScanner}
157+
onScanned={(httpUrl) => {
158+
const wsUrl = httpUrlToWebSocketUrl(httpUrl);
159+
onBackendUrlChange(wsUrl);
160+
}}
161+
onClose={() => setShowQRScanner(false)}
162+
/>
144163
</View>
145164
);
146165
};
@@ -215,6 +234,21 @@ const createStyles = (colors: ThemeColors) => StyleSheet.create({
215234
textAlign: 'center',
216235
fontStyle: 'italic',
217236
},
237+
qrButton: {
238+
backgroundColor: colors.card,
239+
paddingVertical: 12,
240+
paddingHorizontal: 20,
241+
borderRadius: 8,
242+
alignItems: 'center',
243+
marginBottom: 10,
244+
borderWidth: 1,
245+
borderColor: colors.primary,
246+
},
247+
qrButtonText: {
248+
color: colors.primary,
249+
fontSize: 16,
250+
fontWeight: '600',
251+
},
218252
button: {
219253
backgroundColor: colors.primary,
220254
paddingVertical: 12,

app/src/components/DeviceListItem.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
33
import { OmiDevice } from 'friend-lite-react-native';
44
import { useTheme, ThemeColors } from '../theme';
55
import SignalStrength from './SignalStrength';
6+
import { detectDeviceType } from '../utils/deviceType';
67

78
interface DeviceListItemProps {
89
device: OmiDevice;
@@ -23,12 +24,18 @@ export const DeviceListItem: React.FC<DeviceListItemProps> = ({
2324
const s = createStyles(colors);
2425
const isThisDeviceConnected = connectedDeviceId === device.id;
2526
const isAnotherDeviceConnected = connectedDeviceId !== null && connectedDeviceId !== device.id;
27+
const deviceType = detectDeviceType(device.name);
2628

2729
return (
2830
<View style={s.deviceItem}>
2931
<View style={s.deviceInfoContainer}>
3032
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
3133
<Text style={s.deviceName}>{device.name || 'Unknown Device'}</Text>
34+
{deviceType !== 'unknown' && (
35+
<View style={[s.deviceTypeBadge, { backgroundColor: deviceType === 'neo' ? colors.warning : colors.primary }]}>
36+
<Text style={s.deviceTypeBadgeText}>{deviceType === 'neo' ? 'Neo' : 'OMI'}</Text>
37+
</View>
38+
)}
3239
<SignalStrength rssi={device.rssi} />
3340
</View>
3441
<Text style={s.deviceInfo}>ID: {device.id}</Text>
@@ -85,6 +92,17 @@ const createStyles = (colors: ThemeColors) => StyleSheet.create({
8592
color: colors.textSecondary,
8693
marginTop: 2,
8794
},
95+
deviceTypeBadge: {
96+
marginLeft: 6,
97+
paddingHorizontal: 6,
98+
paddingVertical: 2,
99+
borderRadius: 4,
100+
},
101+
deviceTypeBadgeText: {
102+
color: 'white',
103+
fontSize: 10,
104+
fontWeight: '700',
105+
},
88106
button: {
89107
backgroundColor: colors.primary,
90108
paddingVertical: 12,

0 commit comments

Comments
 (0)