@magicred-1/react-native-lxmf 0.2.34 → 0.2.36
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/app.plugin.js +8 -1
- package/example/app/(tabs)/_layout.tsx +27 -12
- package/example/app/(tabs)/conversations.tsx +263 -0
- package/example/app/(tabs)/index.tsx +3 -896
- package/example/app/(tabs)/network.tsx +258 -0
- package/example/app/(tabs)/settings.tsx +230 -0
- package/example/app/_layout.tsx +4 -3
- package/example/app/conversation/[address].tsx +329 -0
- package/example/components/ui/icon-symbol.tsx +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
PermissionsAndroid,
|
|
4
|
+
Platform,
|
|
5
|
+
Pressable,
|
|
6
|
+
ScrollView,
|
|
7
|
+
StyleSheet,
|
|
8
|
+
Switch,
|
|
9
|
+
Text,
|
|
10
|
+
TextInput,
|
|
11
|
+
View,
|
|
12
|
+
} from 'react-native';
|
|
13
|
+
import { LxmfModule, LxmfNodeMode } from '@magicred-1/react-native-lxmf';
|
|
14
|
+
import { useLxmfContext } from '@/context/LxmfContext';
|
|
15
|
+
|
|
16
|
+
function shortHex(v: string): string {
|
|
17
|
+
if (!v || v.length <= 12) return v || '—';
|
|
18
|
+
return `${v.slice(0, 6)}…${v.slice(-6)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type TransportTab = 'ble' | 'tcp' | 'both';
|
|
22
|
+
|
|
23
|
+
// ── Stat row ──────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function Row({ label, value }: Readonly<{ label: string; value: string }>) {
|
|
26
|
+
return (
|
|
27
|
+
<View style={S.statRow}>
|
|
28
|
+
<Text style={S.statLabel}>{label}</Text>
|
|
29
|
+
<Text selectable style={S.statValue}>{value}</Text>
|
|
30
|
+
</View>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Main screen ───────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export default function NetworkScreen() {
|
|
37
|
+
const { isRunning, isNativeAvailable, status, error, identityHydrated, displayName, start, stop, getStatus } = useLxmfContext();
|
|
38
|
+
|
|
39
|
+
const [tab, setTab] = useState<TransportTab>('both');
|
|
40
|
+
const [tcpHost, setTcpHost] = useState('192.168.1.135');
|
|
41
|
+
const [tcpPort, setTcpPort] = useState('4243');
|
|
42
|
+
const [localName, setLocalName] = useState(displayName);
|
|
43
|
+
const [isBeacon, setIsBeacon] = useState(false);
|
|
44
|
+
const [msg, setMsg] = useState('');
|
|
45
|
+
const [bleCount, setBleCount] = useState(0);
|
|
46
|
+
const [unpairedCount, setUnpairedCount] = useState(0);
|
|
47
|
+
|
|
48
|
+
// Poll BLE peer count while running
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!isRunning) { setBleCount(0); setUnpairedCount(0); return; }
|
|
51
|
+
const tick = () => {
|
|
52
|
+
try { setBleCount(LxmfModule.blePeerCount()); } catch {}
|
|
53
|
+
try { setUnpairedCount(LxmfModule.bleUnpairedRNodeCount()); } catch {}
|
|
54
|
+
};
|
|
55
|
+
tick();
|
|
56
|
+
const id = setInterval(tick, 2000);
|
|
57
|
+
return () => clearInterval(id);
|
|
58
|
+
}, [isRunning]);
|
|
59
|
+
|
|
60
|
+
// Auto-refresh status
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!isRunning) return;
|
|
63
|
+
const id = setInterval(getStatus, 5000);
|
|
64
|
+
return () => clearInterval(id);
|
|
65
|
+
}, [isRunning, getStatus]);
|
|
66
|
+
|
|
67
|
+
const requestBlePerms = async (): Promise<boolean> => {
|
|
68
|
+
if (Platform.OS !== 'android') return true;
|
|
69
|
+
const perms = Platform.Version >= 31
|
|
70
|
+
? [PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, PermissionsAndroid.PERMISSIONS.BLUETOOTH_ADVERTISE, PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT]
|
|
71
|
+
: [PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION];
|
|
72
|
+
const res = await PermissionsAndroid.requestMultiple(perms);
|
|
73
|
+
if (Object.values(res).some(r => r !== PermissionsAndroid.RESULTS.GRANTED)) {
|
|
74
|
+
setMsg('BLE permissions denied.');
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const onStart = useCallback(async () => {
|
|
81
|
+
setMsg('');
|
|
82
|
+
const name = localName.trim() || 'lxmf-mobile';
|
|
83
|
+
|
|
84
|
+
if (tab === 'ble') {
|
|
85
|
+
if (!await requestBlePerms()) return;
|
|
86
|
+
const ok = await start({ mode: LxmfNodeMode.BleOnly, displayName: name, isBeacon });
|
|
87
|
+
if (!ok) setMsg('Failed to start BLE node.');
|
|
88
|
+
} else {
|
|
89
|
+
const host = tcpHost.trim();
|
|
90
|
+
const port = Number(tcpPort);
|
|
91
|
+
if (!host) { setMsg('Host required.'); return; }
|
|
92
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) { setMsg('Port must be 1–65535.'); return; }
|
|
93
|
+
if (tab === 'both' && !await requestBlePerms()) return;
|
|
94
|
+
const mode = tab === 'tcp' ? LxmfNodeMode.Reticulum : LxmfNodeMode.ReticulumAndBle;
|
|
95
|
+
const ok = await start({ mode, tcpInterfaces: [{ host, port }], displayName: name, isBeacon });
|
|
96
|
+
if (!ok) setMsg('Failed to start node.');
|
|
97
|
+
}
|
|
98
|
+
}, [tab, tcpHost, tcpPort, localName, isBeacon, start]);
|
|
99
|
+
|
|
100
|
+
const onStop = useCallback(async () => {
|
|
101
|
+
await stop();
|
|
102
|
+
setMsg('');
|
|
103
|
+
}, [stop]);
|
|
104
|
+
|
|
105
|
+
const modeLabel = (m: number) => ['BLE', 'TCP', 'TCP Server', 'Reticulum', 'Reticulum+BLE'][m] ?? String(m);
|
|
106
|
+
|
|
107
|
+
const hasTcp = tab !== 'ble';
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<ScrollView style={S.root} contentContainerStyle={S.scroll}>
|
|
111
|
+
<View style={S.header}>
|
|
112
|
+
<Text style={S.headerTitle}>Network</Text>
|
|
113
|
+
</View>
|
|
114
|
+
|
|
115
|
+
{/* Error banner */}
|
|
116
|
+
{error ? <View style={S.errorBanner}><Text style={S.errorText}>{error}</Text></View> : null}
|
|
117
|
+
|
|
118
|
+
{/* ── Transport card ───────────────────────────────────────────── */}
|
|
119
|
+
<View style={S.card}>
|
|
120
|
+
<Text style={S.cardTitle}>Transport</Text>
|
|
121
|
+
|
|
122
|
+
{/* Segment control */}
|
|
123
|
+
<View style={S.segment}>
|
|
124
|
+
{(['ble', 'tcp', 'both'] as TransportTab[]).map(t => (
|
|
125
|
+
<Pressable
|
|
126
|
+
key={t}
|
|
127
|
+
style={[S.segBtn, tab === t && S.segBtnActive]}
|
|
128
|
+
onPress={() => { setTab(t); setMsg(''); }}
|
|
129
|
+
disabled={isRunning}>
|
|
130
|
+
<Text style={[S.segText, tab === t && S.segTextActive]}>
|
|
131
|
+
{t === 'ble' ? 'BLE' : t === 'tcp' ? 'TCP' : 'TCP+BLE'}
|
|
132
|
+
</Text>
|
|
133
|
+
</Pressable>
|
|
134
|
+
))}
|
|
135
|
+
</View>
|
|
136
|
+
|
|
137
|
+
{/* TCP fields */}
|
|
138
|
+
{hasTcp && (
|
|
139
|
+
<>
|
|
140
|
+
<TextInput style={S.input} placeholder="Host" placeholderTextColor="#4a6070"
|
|
141
|
+
value={tcpHost} onChangeText={setTcpHost} autoCapitalize="none" autoCorrect={false} editable={!isRunning} />
|
|
142
|
+
<TextInput style={S.input} placeholder="Port" placeholderTextColor="#4a6070"
|
|
143
|
+
value={tcpPort} onChangeText={setTcpPort} keyboardType="number-pad" editable={!isRunning} />
|
|
144
|
+
</>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{/* Display name */}
|
|
148
|
+
<TextInput style={S.input} placeholder="Display name" placeholderTextColor="#4a6070"
|
|
149
|
+
value={localName} onChangeText={setLocalName} autoCapitalize="none" autoCorrect={false} editable={!isRunning} />
|
|
150
|
+
|
|
151
|
+
{/* Beacon toggle */}
|
|
152
|
+
<View style={S.switchRow}>
|
|
153
|
+
<Text style={S.switchLabel}>Beacon mode</Text>
|
|
154
|
+
<Switch
|
|
155
|
+
value={isBeacon} onValueChange={setIsBeacon} disabled={isRunning}
|
|
156
|
+
trackColor={{ false: '#1e3040', true: '#1a7fc1' }}
|
|
157
|
+
thumbColor={isBeacon ? '#4fb3e8' : '#4a6070'}
|
|
158
|
+
/>
|
|
159
|
+
</View>
|
|
160
|
+
|
|
161
|
+
{msg ? <Text style={S.warn}>{msg}</Text> : null}
|
|
162
|
+
|
|
163
|
+
{unpairedCount > 0 && (
|
|
164
|
+
<Text style={S.warn}>
|
|
165
|
+
{unpairedCount} unpaired RNode{unpairedCount > 1 ? 's' : ''} nearby — pair in Settings > Bluetooth first.
|
|
166
|
+
</Text>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
<View style={S.btnRow}>
|
|
170
|
+
<Pressable
|
|
171
|
+
style={({ pressed }) => [S.btn, (!isNativeAvailable || isRunning || !identityHydrated) && S.btnDisabled, pressed && S.btnPressed]}
|
|
172
|
+
onPress={onStart}
|
|
173
|
+
disabled={!isNativeAvailable || isRunning || !identityHydrated}>
|
|
174
|
+
<Text style={S.btnText}>Start</Text>
|
|
175
|
+
</Pressable>
|
|
176
|
+
<Pressable
|
|
177
|
+
style={({ pressed }) => [S.btn, S.btnDanger, !isRunning && S.btnDisabled, pressed && S.btnPressed]}
|
|
178
|
+
onPress={onStop}
|
|
179
|
+
disabled={!isRunning}>
|
|
180
|
+
<Text style={S.btnText}>Stop</Text>
|
|
181
|
+
</Pressable>
|
|
182
|
+
</View>
|
|
183
|
+
</View>
|
|
184
|
+
|
|
185
|
+
{/* ── Node Status card ─────────────────────────────────────────── */}
|
|
186
|
+
<View style={S.card}>
|
|
187
|
+
<View style={S.cardTitleRow}>
|
|
188
|
+
<Text style={S.cardTitle}>Node Status</Text>
|
|
189
|
+
<Pressable style={({ pressed }) => [S.refreshBtn, pressed && { opacity: 0.7 }]} onPress={getStatus}>
|
|
190
|
+
<Text style={S.refreshText}>↻ Refresh</Text>
|
|
191
|
+
</Pressable>
|
|
192
|
+
</View>
|
|
193
|
+
|
|
194
|
+
<Row label="State" value={isRunning ? '● Running' : '○ Stopped'} />
|
|
195
|
+
<Row label="Mode" value={status ? modeLabel(status.mode) : '—'} />
|
|
196
|
+
<Row label="Address" value={status?.addressHex ? shortHex(status.addressHex) : '—'} />
|
|
197
|
+
<Row label="BLE peers" value={String(bleCount)} />
|
|
198
|
+
<Row label="Pending outbound" value={String(status?.pendingOutbound ?? 0)} />
|
|
199
|
+
<Row label="Messages sent" value={String(status?.outboundSent ?? 0)} />
|
|
200
|
+
<Row label="Messages received" value={String(status?.lxmfMessagesReceived ?? 0)} />
|
|
201
|
+
<Row label="Announces received" value={String(status?.announcesReceived ?? 0)} />
|
|
202
|
+
<Row label="Inbound accepted" value={String(status?.inboundAccepted ?? 0)} />
|
|
203
|
+
</View>
|
|
204
|
+
</ScrollView>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── Styles ────────────────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
const S = StyleSheet.create({
|
|
211
|
+
root: { flex: 1, backgroundColor: '#0c1218' },
|
|
212
|
+
scroll: { paddingBottom: 40, gap: 12 },
|
|
213
|
+
|
|
214
|
+
header: {
|
|
215
|
+
paddingHorizontal: 16, paddingTop: 56, paddingBottom: 14,
|
|
216
|
+
backgroundColor: '#131d26', borderBottomWidth: 1, borderBottomColor: '#1e3040',
|
|
217
|
+
},
|
|
218
|
+
headerTitle: { color: '#d8ecf8', fontSize: 28, fontWeight: '700' },
|
|
219
|
+
|
|
220
|
+
errorBanner: { backgroundColor: '#3a1515', borderWidth: 1, borderColor: '#7a2020', padding: 10, marginHorizontal: 14, borderRadius: 10 },
|
|
221
|
+
errorText: { color: '#ff9a9a', fontSize: 13 },
|
|
222
|
+
|
|
223
|
+
card: {
|
|
224
|
+
backgroundColor: '#131d26', borderRadius: 14, borderWidth: 1,
|
|
225
|
+
borderColor: '#1e3040', padding: 16, gap: 10, marginHorizontal: 14,
|
|
226
|
+
},
|
|
227
|
+
cardTitle: { color: '#d8ecf8', fontSize: 16, fontWeight: '700' },
|
|
228
|
+
cardTitleRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
|
|
229
|
+
refreshBtn: { paddingHorizontal: 10, paddingVertical: 4, borderRadius: 8, backgroundColor: '#0d3550', borderWidth: 1, borderColor: '#1e3040' },
|
|
230
|
+
refreshText: { color: '#4fb3e8', fontSize: 12, fontWeight: '600' },
|
|
231
|
+
|
|
232
|
+
segment: { flexDirection: 'row', borderRadius: 10, borderWidth: 1, borderColor: '#1e3040', overflow: 'hidden' },
|
|
233
|
+
segBtn: { flex: 1, paddingVertical: 9, alignItems: 'center', backgroundColor: '#0e1923' },
|
|
234
|
+
segBtnActive: { backgroundColor: '#0d3550' },
|
|
235
|
+
segText: { color: '#4a6070', fontSize: 13, fontWeight: '600' },
|
|
236
|
+
segTextActive: { color: '#4fb3e8' },
|
|
237
|
+
|
|
238
|
+
input: {
|
|
239
|
+
borderWidth: 1, borderColor: '#2a4050', backgroundColor: '#0b1820', color: '#d8ecf8',
|
|
240
|
+
borderRadius: 10, paddingHorizontal: 12, paddingVertical: 10, fontFamily: 'monospace', fontSize: 13,
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
switchRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingVertical: 2 },
|
|
244
|
+
switchLabel: { color: '#7a9db5', fontSize: 13 },
|
|
245
|
+
|
|
246
|
+
warn: { color: '#f0a500', fontSize: 12, fontFamily: 'monospace' },
|
|
247
|
+
|
|
248
|
+
btnRow: { flexDirection: 'row', gap: 8 },
|
|
249
|
+
btn: { flex: 1, borderRadius: 10, paddingVertical: 11, alignItems: 'center', backgroundColor: '#1a7fc1' },
|
|
250
|
+
btnDanger: { backgroundColor: '#c0392b' },
|
|
251
|
+
btnDisabled: { opacity: 0.4 },
|
|
252
|
+
btnPressed: { opacity: 0.75 },
|
|
253
|
+
btnText: { color: '#e8f6ff', fontSize: 14, fontWeight: '600' },
|
|
254
|
+
|
|
255
|
+
statRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 2 },
|
|
256
|
+
statLabel: { color: '#7a9db5', fontSize: 13 },
|
|
257
|
+
statValue: { color: '#d8ecf8', fontSize: 13, fontFamily: 'monospace' },
|
|
258
|
+
});
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Alert,
|
|
4
|
+
Modal,
|
|
5
|
+
Pressable,
|
|
6
|
+
ScrollView,
|
|
7
|
+
Share,
|
|
8
|
+
StyleSheet,
|
|
9
|
+
Text,
|
|
10
|
+
TextInput,
|
|
11
|
+
View,
|
|
12
|
+
} from 'react-native';
|
|
13
|
+
import { useLxmfContext } from '@/context/LxmfContext';
|
|
14
|
+
|
|
15
|
+
function shortHex(v: string): string {
|
|
16
|
+
if (!v || v.length <= 12) return v || '—';
|
|
17
|
+
return `${v.slice(0, 6)}…${v.slice(-6)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function Row({ label, value, onAction, actionLabel }: Readonly<{
|
|
21
|
+
label: string; value: string; onAction?: () => void; actionLabel?: string;
|
|
22
|
+
}>) {
|
|
23
|
+
return (
|
|
24
|
+
<View style={S.row}>
|
|
25
|
+
<View style={S.rowLeft}>
|
|
26
|
+
<Text style={S.rowLabel}>{label}</Text>
|
|
27
|
+
<Text selectable style={S.rowValue}>{value}</Text>
|
|
28
|
+
</View>
|
|
29
|
+
{onAction && actionLabel && (
|
|
30
|
+
<Pressable style={({ pressed }) => [S.actionBtn, pressed && { opacity: 0.7 }]} onPress={onAction}>
|
|
31
|
+
<Text style={S.actionBtnText}>{actionLabel}</Text>
|
|
32
|
+
</Pressable>
|
|
33
|
+
)}
|
|
34
|
+
</View>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const LOG_LEVELS = ['0 – Off', '1 – Error', '2 – Warn', '3 – Info', '4 – Debug', '5 – Trace'];
|
|
39
|
+
|
|
40
|
+
export default function SettingsScreen() {
|
|
41
|
+
const {
|
|
42
|
+
identity, status, isRunning, displayName, setDisplayName, clearIdentity, setLogLevel,
|
|
43
|
+
} = useLxmfContext();
|
|
44
|
+
|
|
45
|
+
const [nameInput, setNameInput] = useState(displayName);
|
|
46
|
+
const [logLevel, setLogLevelState] = useState(3);
|
|
47
|
+
const [confirmReset, setConfirmReset] = useState(false);
|
|
48
|
+
const [nameSaved, setNameSaved] = useState(false);
|
|
49
|
+
|
|
50
|
+
const saveDisplayName = useCallback(() => {
|
|
51
|
+
setDisplayName(nameInput.trim() || 'lxmf-mobile');
|
|
52
|
+
setNameSaved(true);
|
|
53
|
+
setTimeout(() => setNameSaved(false), 2000);
|
|
54
|
+
}, [nameInput, setDisplayName]);
|
|
55
|
+
|
|
56
|
+
const changeLogLevel = useCallback((delta: number) => {
|
|
57
|
+
const next = Math.max(0, Math.min(5, logLevel + delta));
|
|
58
|
+
setLogLevelState(next);
|
|
59
|
+
setLogLevel(next);
|
|
60
|
+
}, [logLevel, setLogLevel]);
|
|
61
|
+
|
|
62
|
+
const copyAddress = useCallback(() => {
|
|
63
|
+
const addr = status?.addressHex ?? identity?.address_hex;
|
|
64
|
+
if (addr) Share.share({ message: addr }).catch(() => {});
|
|
65
|
+
}, [status?.addressHex, identity?.address_hex]);
|
|
66
|
+
|
|
67
|
+
const doReset = useCallback(async () => {
|
|
68
|
+
setConfirmReset(false);
|
|
69
|
+
await clearIdentity();
|
|
70
|
+
}, [clearIdentity]);
|
|
71
|
+
|
|
72
|
+
const displayAddr = status?.addressHex ?? identity?.address_hex ?? null;
|
|
73
|
+
const createdAt = identity?.created_at
|
|
74
|
+
? new Date(identity.created_at).toLocaleDateString()
|
|
75
|
+
: '—';
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<ScrollView style={S.root} contentContainerStyle={S.scroll}>
|
|
79
|
+
<View style={S.header}>
|
|
80
|
+
<Text style={S.headerTitle}>Settings</Text>
|
|
81
|
+
</View>
|
|
82
|
+
|
|
83
|
+
{/* Display name */}
|
|
84
|
+
<View style={S.section}>
|
|
85
|
+
<Text style={S.sectionTitle}>Display Name</Text>
|
|
86
|
+
<Text style={S.hint}>Shown to peers in LXMF announces.</Text>
|
|
87
|
+
<View style={S.inputRow}>
|
|
88
|
+
<TextInput
|
|
89
|
+
style={[S.input, S.inputFlex]}
|
|
90
|
+
value={nameInput}
|
|
91
|
+
onChangeText={setNameInput}
|
|
92
|
+
placeholder="lxmf-mobile"
|
|
93
|
+
placeholderTextColor="#4a6070"
|
|
94
|
+
autoCapitalize="none"
|
|
95
|
+
autoCorrect={false}
|
|
96
|
+
/>
|
|
97
|
+
<Pressable style={({ pressed }) => [S.saveBtn, pressed && { opacity: 0.75 }]} onPress={saveDisplayName}>
|
|
98
|
+
<Text style={S.saveBtnText}>{nameSaved ? '✓' : 'Save'}</Text>
|
|
99
|
+
</Pressable>
|
|
100
|
+
</View>
|
|
101
|
+
</View>
|
|
102
|
+
|
|
103
|
+
{/* Identity */}
|
|
104
|
+
<View style={S.section}>
|
|
105
|
+
<Text style={S.sectionTitle}>My Identity</Text>
|
|
106
|
+
{displayAddr ? (
|
|
107
|
+
<>
|
|
108
|
+
<Row
|
|
109
|
+
label="Address"
|
|
110
|
+
value={shortHex(displayAddr)}
|
|
111
|
+
onAction={copyAddress}
|
|
112
|
+
actionLabel="⎘ Copy"
|
|
113
|
+
/>
|
|
114
|
+
<Row label="Full address" value={displayAddr} />
|
|
115
|
+
<Row label="Created" value={createdAt} />
|
|
116
|
+
</>
|
|
117
|
+
) : (
|
|
118
|
+
<Text style={S.hint}>No identity yet — start the node in the Network tab.</Text>
|
|
119
|
+
)}
|
|
120
|
+
</View>
|
|
121
|
+
|
|
122
|
+
{/* Log level */}
|
|
123
|
+
<View style={S.section}>
|
|
124
|
+
<Text style={S.sectionTitle}>Log Level</Text>
|
|
125
|
+
<View style={S.logRow}>
|
|
126
|
+
<Pressable style={({ pressed }) => [S.stepBtn, pressed && { opacity: 0.7 }]} onPress={() => changeLogLevel(-1)}>
|
|
127
|
+
<Text style={S.stepBtnText}>−</Text>
|
|
128
|
+
</Pressable>
|
|
129
|
+
<Text style={S.logLabel}>{LOG_LEVELS[logLevel] ?? String(logLevel)}</Text>
|
|
130
|
+
<Pressable style={({ pressed }) => [S.stepBtn, pressed && { opacity: 0.7 }]} onPress={() => changeLogLevel(1)}>
|
|
131
|
+
<Text style={S.stepBtnText}>+</Text>
|
|
132
|
+
</Pressable>
|
|
133
|
+
</View>
|
|
134
|
+
</View>
|
|
135
|
+
|
|
136
|
+
{/* Danger zone */}
|
|
137
|
+
<View style={[S.section, S.dangerSection]}>
|
|
138
|
+
<Text style={[S.sectionTitle, S.dangerTitle]}>Danger Zone</Text>
|
|
139
|
+
<Text style={S.hint}>
|
|
140
|
+
Resetting your identity permanently removes your private key. You will lose access to your LXMF address and any pending messages.
|
|
141
|
+
</Text>
|
|
142
|
+
<Pressable
|
|
143
|
+
style={({ pressed }) => [S.dangerBtn, isRunning && S.btnDisabled, pressed && { opacity: 0.8 }]}
|
|
144
|
+
onPress={() => setConfirmReset(true)}
|
|
145
|
+
disabled={isRunning}>
|
|
146
|
+
<Text style={S.dangerBtnText}>{isRunning ? 'Stop node before resetting' : 'Reset Identity'}</Text>
|
|
147
|
+
</Pressable>
|
|
148
|
+
</View>
|
|
149
|
+
|
|
150
|
+
{/* Confirm reset modal */}
|
|
151
|
+
<Modal visible={confirmReset} transparent animationType="fade" onRequestClose={() => setConfirmReset(false)}>
|
|
152
|
+
<Pressable style={S.overlay} onPress={() => setConfirmReset(false)}>
|
|
153
|
+
<Pressable style={S.modal}>
|
|
154
|
+
<Text style={S.modalTitle}>Reset Identity?</Text>
|
|
155
|
+
<Text style={S.modalBody}>
|
|
156
|
+
This permanently deletes your identity and address.{'\n\n'}
|
|
157
|
+
You will lose access to all pending messages. This cannot be undone.
|
|
158
|
+
</Text>
|
|
159
|
+
<View style={S.modalBtns}>
|
|
160
|
+
<Pressable style={[S.modalBtn, S.modalBtnCancel]} onPress={() => setConfirmReset(false)}>
|
|
161
|
+
<Text style={S.modalBtnText}>Cancel</Text>
|
|
162
|
+
</Pressable>
|
|
163
|
+
<Pressable style={[S.modalBtn, S.modalBtnDanger]} onPress={doReset}>
|
|
164
|
+
<Text style={S.modalBtnText}>Delete</Text>
|
|
165
|
+
</Pressable>
|
|
166
|
+
</View>
|
|
167
|
+
</Pressable>
|
|
168
|
+
</Pressable>
|
|
169
|
+
</Modal>
|
|
170
|
+
</ScrollView>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Styles ────────────────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
const S = StyleSheet.create({
|
|
177
|
+
root: { flex: 1, backgroundColor: '#0c1218' },
|
|
178
|
+
scroll: { paddingBottom: 60, gap: 12 },
|
|
179
|
+
|
|
180
|
+
header: {
|
|
181
|
+
paddingHorizontal: 16, paddingTop: 56, paddingBottom: 14,
|
|
182
|
+
backgroundColor: '#131d26', borderBottomWidth: 1, borderBottomColor: '#1e3040',
|
|
183
|
+
},
|
|
184
|
+
headerTitle: { color: '#d8ecf8', fontSize: 28, fontWeight: '700' },
|
|
185
|
+
|
|
186
|
+
section: {
|
|
187
|
+
backgroundColor: '#131d26', borderRadius: 14, borderWidth: 1,
|
|
188
|
+
borderColor: '#1e3040', padding: 16, gap: 10, marginHorizontal: 14,
|
|
189
|
+
},
|
|
190
|
+
sectionTitle: { color: '#d8ecf8', fontSize: 15, fontWeight: '700' },
|
|
191
|
+
hint: { color: '#7a9db5', fontSize: 13, lineHeight: 19 },
|
|
192
|
+
|
|
193
|
+
row: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 10 },
|
|
194
|
+
rowLeft: { flex: 1, gap: 2 },
|
|
195
|
+
rowLabel: { color: '#7a9db5', fontSize: 12 },
|
|
196
|
+
rowValue: { color: '#d8ecf8', fontSize: 13, fontFamily: 'monospace' },
|
|
197
|
+
|
|
198
|
+
actionBtn: { paddingHorizontal: 10, paddingVertical: 5, borderRadius: 8, backgroundColor: '#0d3550', borderWidth: 1, borderColor: '#1e3040' },
|
|
199
|
+
actionBtnText: { color: '#4fb3e8', fontSize: 12, fontWeight: '600' },
|
|
200
|
+
|
|
201
|
+
inputRow: { flexDirection: 'row', gap: 8, alignItems: 'center' },
|
|
202
|
+
inputFlex: { flex: 1 },
|
|
203
|
+
input: {
|
|
204
|
+
borderWidth: 1, borderColor: '#2a4050', backgroundColor: '#0b1820', color: '#d8ecf8',
|
|
205
|
+
borderRadius: 10, paddingHorizontal: 12, paddingVertical: 10, fontSize: 14,
|
|
206
|
+
},
|
|
207
|
+
saveBtn: { paddingHorizontal: 14, paddingVertical: 10, borderRadius: 10, backgroundColor: '#1a7fc1' },
|
|
208
|
+
saveBtnText: { color: '#e8f6ff', fontSize: 14, fontWeight: '600' },
|
|
209
|
+
|
|
210
|
+
logRow: { flexDirection: 'row', alignItems: 'center', gap: 14 },
|
|
211
|
+
stepBtn: { width: 36, height: 36, borderRadius: 18, backgroundColor: '#0d3550', borderWidth: 1, borderColor: '#1e3040', alignItems: 'center', justifyContent: 'center' },
|
|
212
|
+
stepBtnText: { color: '#4fb3e8', fontSize: 20, lineHeight: 24, fontWeight: '600' },
|
|
213
|
+
logLabel: { flex: 1, color: '#d8ecf8', fontSize: 14, fontFamily: 'monospace' },
|
|
214
|
+
|
|
215
|
+
dangerSection: { borderColor: '#4a1515' },
|
|
216
|
+
dangerTitle: { color: '#ff7070' },
|
|
217
|
+
dangerBtn: { backgroundColor: '#7a1515', borderRadius: 10, paddingVertical: 12, alignItems: 'center' },
|
|
218
|
+
dangerBtnText: { color: '#ffcccc', fontSize: 14, fontWeight: '600' },
|
|
219
|
+
btnDisabled: { opacity: 0.4 },
|
|
220
|
+
|
|
221
|
+
overlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.75)', justifyContent: 'center', alignItems: 'center', padding: 24 },
|
|
222
|
+
modal: { width: '100%', backgroundColor: '#131d26', borderRadius: 16, borderWidth: 1, borderColor: '#4a1515', padding: 20, gap: 14 },
|
|
223
|
+
modalTitle: { color: '#ff7070', fontSize: 18, fontWeight: '700' },
|
|
224
|
+
modalBody: { color: '#d8ecf8', fontSize: 14, lineHeight: 22 },
|
|
225
|
+
modalBtns: { flexDirection: 'row', gap: 10 },
|
|
226
|
+
modalBtn: { flex: 1, borderRadius: 10, paddingVertical: 11, alignItems: 'center' },
|
|
227
|
+
modalBtnCancel: { backgroundColor: '#1a2e40' },
|
|
228
|
+
modalBtnDanger: { backgroundColor: '#7a1515' },
|
|
229
|
+
modalBtnText: { color: '#e8f6ff', fontSize: 14, fontWeight: '600' },
|
|
230
|
+
});
|
package/example/app/_layout.tsx
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { Stack } from 'expo-router';
|
|
2
2
|
import { StatusBar } from 'expo-status-bar';
|
|
3
3
|
import { LogBox } from 'react-native';
|
|
4
|
+
import { LxmfProvider } from '@/context/LxmfContext';
|
|
4
5
|
|
|
5
6
|
LogBox.ignoreLogs(['Unable to determine event arguments for "onModeChange"']);
|
|
6
7
|
|
|
7
8
|
export default function RootLayout() {
|
|
8
9
|
return (
|
|
9
|
-
|
|
10
|
+
<LxmfProvider>
|
|
10
11
|
<Stack screenOptions={{ headerShown: false }} />
|
|
11
|
-
<StatusBar style="
|
|
12
|
-
|
|
12
|
+
<StatusBar style="light" />
|
|
13
|
+
</LxmfProvider>
|
|
13
14
|
);
|
|
14
15
|
}
|