@magicred-1/react-native-lxmf 0.2.31 → 0.2.32

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.
@@ -207,6 +207,7 @@ export default function HomeScreen() {
207
207
 
208
208
  const [unpairedRNodes, setUnpairedRNodes] = useState(0);
209
209
  const [liveBleCount, setLiveBleCount] = useState(0);
210
+ const [storedMsgs, setStoredMsgs] = useState<any[]>([]);
210
211
 
211
212
  // Identity hydration: read once from secure store on mount. Until hydrated,
212
213
  // we pass 'new' so Rust generates a fresh identity (which we'll then persist
@@ -218,20 +219,13 @@ export default function HomeScreen() {
218
219
  (async () => {
219
220
  try {
220
221
  const raw = await SecureStore.getItemAsync(IDENTITY_KEY);
221
- // TODO(sentinel): remove debug logs before merge — lengths/booleans only, no key material
222
- console.log('[persist] hydrate', { hasRaw: !!raw, rawLen: raw?.length ?? 0 });
223
222
  if (cancelled) return;
224
223
  if (raw) {
225
224
  const parsed = JSON.parse(raw);
226
- const valid = isValidIdentity(parsed);
227
- console.log('[persist] parsed', { valid, hasIdHex: typeof parsed?.identity_hex === 'string', hasAddrHex: typeof parsed?.address_hex === 'string' });
228
- if (valid) {
229
- setStoredIdentity(parsed);
230
- }
225
+ if (isValidIdentity(parsed)) setStoredIdentity(parsed);
231
226
  }
232
- } catch (e: any) {
233
- console.log('[persist] hydrate FAIL', e?.message ?? 'unknown');
234
- // Corrupt blob or storage error — fall through; we'll generate fresh.
227
+ } catch {
228
+ // Corrupt blob or storage error — fall through; generate fresh identity.
235
229
  } finally {
236
230
  if (!cancelled) setIdentityHydrated(true);
237
231
  }
@@ -240,31 +234,23 @@ export default function HomeScreen() {
240
234
  }, []);
241
235
 
242
236
  const {
243
- isNativeAvailable, isRunning, status, error, events, beacons,
244
- start, stop, send, getStatus, getIdentityHex,
245
- startBLE, stopBLE, bleUnpairedRNodeCount,
237
+ isNativeAvailable, isRunning, status, error, events,
238
+ start, stop, send, broadcast, getStatus, getIdentityHex, fetchMessages,
239
+ bleUnpairedRNodeCount,
246
240
  } = useLxmf({
247
241
  identityHex: storedIdentity?.identity_hex ?? 'new',
248
242
  lxmfAddressHex: storedIdentity?.address_hex ?? 'new',
249
243
  logLevel: 3,
250
244
  });
251
245
 
252
- // Persist identity after node starts successfully (only if not already stored,
253
- // or if the running identity differs — defensive against schema migrations).
246
+ // Persist identity after node starts (only when identity changes from stored copy).
254
247
  useEffect(() => {
255
248
  if (!isRunning) return;
256
249
  const idHex = getIdentityHex();
257
250
  const addrHex = status?.addressHex;
258
- // TODO(sentinel): remove debug logs before merge — lengths/booleans only, no key material
259
- console.log('[persist] save check', {
260
- idHexLen: idHex?.length ?? 0,
261
- addrHexLen: addrHex?.length ?? 0,
262
- alreadyStoredSame: storedIdentity?.identity_hex === idHex && storedIdentity?.address_hex === addrHex,
263
- });
264
251
  if (!idHex || idHex.length !== 128) return;
265
252
  if (!addrHex || !/^[0-9a-fA-F]{32}$/.test(addrHex)) return;
266
253
  if (storedIdentity?.identity_hex === idHex && storedIdentity?.address_hex === addrHex) return;
267
-
268
254
  const blob: StoredIdentity = {
269
255
  version: IDENTITY_SCHEMA_VERSION,
270
256
  identity_hex: idHex,
@@ -272,16 +258,15 @@ export default function HomeScreen() {
272
258
  created_at: new Date().toISOString(),
273
259
  };
274
260
  SecureStore.setItemAsync(IDENTITY_KEY, JSON.stringify(blob))
275
- .then(() => {
276
- console.log('[persist] save OK');
277
- setStoredIdentity(blob);
278
- })
279
- .catch((e: any) => {
280
- console.log('[persist] save FAIL', e?.message ?? 'unknown');
281
- /* persistence failure is non-fatal for the running session */
282
- });
261
+ .then(() => setStoredIdentity(blob))
262
+ .catch(() => { /* non-fatal */ });
283
263
  }, [isRunning, status?.addressHex, storedIdentity, getIdentityHex]);
284
264
 
265
+ // Load persisted messages from SQLite whenever node starts.
266
+ useEffect(() => {
267
+ if (isRunning) setStoredMsgs(fetchMessages(50));
268
+ }, [isRunning, fetchMessages]);
269
+
285
270
  // ── Derived ───────────────────────────────────────────────────────────────
286
271
 
287
272
  const counts = useMemo(() => {
@@ -326,21 +311,18 @@ export default function HomeScreen() {
326
311
  mode: LxmfNodeMode.ReticulumAndBle,
327
312
  tcpInterfaces: [{ host, port }],
328
313
  displayName: displayName.trim() || 'lxmf-mobile',
329
- isBeacon,
330
314
  });
331
315
  if (ok) {
332
316
  setTcpActive(true);
333
- startBLE();
334
317
  setBleActive(true);
335
318
  }
336
- }, [tcpHost, tcpPort, displayName, isBeacon, start, startBLE]);
319
+ }, [tcpHost, tcpPort, displayName, start]);
337
320
 
338
321
  const onStopTcp = useCallback(async () => {
339
- stopBLE();
340
322
  await stop();
341
323
  setTcpActive(false);
342
324
  setBleActive(false);
343
- }, [stop, stopBLE]);
325
+ }, [stop]);
344
326
 
345
327
  const onStartBle = useCallback(async () => {
346
328
  setTransportMsg('');
@@ -353,43 +335,31 @@ export default function HomeScreen() {
353
335
  ]
354
336
  : [PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION];
355
337
  const results = await PermissionsAndroid.requestMultiple(perms);
356
- const denied = Object.values(results).some(r => r !== PermissionsAndroid.RESULTS.GRANTED);
357
- if (denied) {
338
+ if (Object.values(results).some(r => r !== PermissionsAndroid.RESULTS.GRANTED)) {
358
339
  setTransportMsg('BLE permissions denied.');
359
340
  return;
360
341
  }
361
342
  }
362
- // If node already running (started via TCP), just enable BLE hardware.
363
- // Otherwise start mode 4 (TCP+BLE) using the configured TCP host/port.
364
- if (!isRunning) {
365
- const host = tcpHost.trim();
366
- const port = Number(tcpPort);
367
- if (!host) { setTransportMsg('Host required for TCP+BLE mode.'); return; }
368
- if (!Number.isInteger(port) || port < 1 || port > 65535) { setTransportMsg('Port 1–65535.'); return; }
369
- const ok = await start({
370
- mode: LxmfNodeMode.ReticulumAndBle,
371
- tcpInterfaces: [{ host, port }],
372
- displayName: displayName.trim() || 'lxmf-mobile',
373
- isBeacon,
374
- });
375
- if (!ok) {
376
- setTransportMsg('Failed to start node.');
377
- return;
378
- }
379
- setTcpActive(true);
380
- }
381
- startBLE();
343
+ const ok = await start({
344
+ mode: LxmfNodeMode.BleOnly,
345
+ displayName: displayName.trim() || 'lxmf-mobile',
346
+ });
347
+ if (!ok) { setTransportMsg('Failed to start BLE node.'); return; }
382
348
  setBleActive(true);
383
- }, [isRunning, tcpHost, tcpPort, isBeacon, start, startBLE, displayName]);
349
+ }, [start, displayName]);
384
350
 
385
351
  const onStopBle = useCallback(async () => {
386
- stopBLE();
352
+ await stop();
387
353
  setBleActive(false);
388
354
  setUnpairedRNodes(0);
389
- if (isRunning) {
390
- await stop();
391
- }
392
- }, [stopBLE, stop, isRunning]);
355
+ }, [stop]);
356
+
357
+ const onBroadcast = useCallback(async () => {
358
+ if (!knownPeerHashes.length) { setSendResult('No known peers.'); return; }
359
+ const dests = knownPeerHashes.map(p => p.hash);
360
+ const r = await broadcast(dests, utf8ToBase64(msgText));
361
+ setSendResult(r >= 0 ? `Broadcast #${r} → ${dests.length} peers` : 'Broadcast failed.');
362
+ }, [knownPeerHashes, msgText, broadcast]);
393
363
 
394
364
  // Poll for unpaired RNodes while BLE is active
395
365
  useEffect(() => {
@@ -617,13 +587,51 @@ export default function HomeScreen() {
617
587
  />
618
588
  <View style={S.btnRow}>
619
589
  <Btn label="Send" onPress={onSend} disabled={!isRunning} />
590
+ <Btn label="Broadcast" onPress={onBroadcast} disabled={!isRunning || !knownPeerHashes.length} />
620
591
  </View>
621
592
  {sendResult ? <Text style={S.feedback}>{sendResult}</Text> : null}
622
593
  </Accordion>
623
594
 
624
595
  {/* ── Messages ─────────────────────────────────────────────────────── */}
625
- <Accordion title="Messages" badge={counts.messages} defaultOpen>
626
- {msgEvts.length === 0 ? (
596
+ <Accordion title="Messages" badge={counts.messages + storedMsgs.length} defaultOpen>
597
+ {/* Persisted (SQLite) */}
598
+ {storedMsgs.length > 0 && (
599
+ <>
600
+ <Text style={S.sectionLabel}>Persisted ({storedMsgs.length})</Text>
601
+ {storedMsgs.map((m: any, i: number) => {
602
+ const bodyText = base64ToUtf8(m.body ?? '');
603
+ const titleText = m.title ? base64ToUtf8(m.title) : '';
604
+ const sender = m.source ?? m.source_hash ?? '';
605
+ const t = m.timestamp ? new Date(m.timestamp > 10_000_000_000 ? m.timestamp : m.timestamp * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
606
+ return (
607
+ <View key={`stored-${i}-${sender}`} style={[S.itemCard, S.storedCard]}>
608
+ <View style={S.announceHeader}>
609
+ <View style={S.announceInfo}>
610
+ <Text selectable style={S.itemTitle}>From: {shortHex(sender)}</Text>
611
+ {titleText ? <Text selectable style={S.msgTitle}>{titleText}</Text> : null}
612
+ {bodyText ? <Text selectable style={S.itemBody}>{bodyText}</Text> : null}
613
+ {t ? <Text style={S.itemMeta}>{t}</Text> : null}
614
+ </View>
615
+ {sender ? (
616
+ <View style={S.announceActions}>
617
+ <Pressable style={S.copyBtn} onPress={() => copyToClipboard(sender)}>
618
+ <Text style={S.copyBtnText}>⎘</Text>
619
+ </Pressable>
620
+ <Pressable style={S.sendToBtn} onPress={() => { setDest(sender); setSendResult(''); }}>
621
+ <Text style={S.sendToBtnText}>↩ Reply</Text>
622
+ </Pressable>
623
+ </View>
624
+ ) : null}
625
+ </View>
626
+ </View>
627
+ );
628
+ })}
629
+ </>
630
+ )}
631
+
632
+ {/* Live (in-session) */}
633
+ {msgEvts.length > 0 && <Text style={S.sectionLabel}>Live session</Text>}
634
+ {msgEvts.length === 0 && storedMsgs.length === 0 ? (
627
635
  <Text style={S.muted}>No messages yet.</Text>
628
636
  ) : (
629
637
  msgEvts.map((e, i) => {
@@ -885,4 +893,6 @@ const S = StyleSheet.create({
885
893
  // Message card extras
886
894
  msgTitle: { color: C.text, fontSize: 13, fontWeight: '600', fontStyle: 'italic' },
887
895
  mediaBadge: { color: C.accentBright, fontSize: 11, fontFamily: 'monospace', marginTop: 2 },
896
+ sectionLabel: { color: C.textDim, fontSize: 11, fontWeight: '600', textTransform: 'uppercase', letterSpacing: 0.8, marginTop: 4 },
897
+ storedCard: { borderColor: '#253d50', backgroundColor: '#0b1a25' },
888
898
  });
@@ -8,32 +8,32 @@
8
8
  <key>BinaryPath</key>
9
9
  <string>liblxmf_rn.a</string>
10
10
  <key>LibraryIdentifier</key>
11
- <string>ios-arm64</string>
11
+ <string>ios-arm64_x86_64-simulator</string>
12
12
  <key>LibraryPath</key>
13
13
  <string>liblxmf_rn.a</string>
14
14
  <key>SupportedArchitectures</key>
15
15
  <array>
16
16
  <string>arm64</string>
17
+ <string>x86_64</string>
17
18
  </array>
18
19
  <key>SupportedPlatform</key>
19
20
  <string>ios</string>
21
+ <key>SupportedPlatformVariant</key>
22
+ <string>simulator</string>
20
23
  </dict>
21
24
  <dict>
22
25
  <key>BinaryPath</key>
23
26
  <string>liblxmf_rn.a</string>
24
27
  <key>LibraryIdentifier</key>
25
- <string>ios-arm64_x86_64-simulator</string>
28
+ <string>ios-arm64</string>
26
29
  <key>LibraryPath</key>
27
30
  <string>liblxmf_rn.a</string>
28
31
  <key>SupportedArchitectures</key>
29
32
  <array>
30
33
  <string>arm64</string>
31
- <string>x86_64</string>
32
34
  </array>
33
35
  <key>SupportedPlatform</key>
34
36
  <string>ios</string>
35
- <key>SupportedPlatformVariant</key>
36
- <string>simulator</string>
37
37
  </dict>
38
38
  </array>
39
39
  <key>CFBundlePackageType</key>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magicred-1/react-native-lxmf",
3
- "version": "0.2.31",
3
+ "version": "0.2.32",
4
4
  "description": "LXMF Reticulum mesh networking for React Native + Expo",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",