Skip to content

Commit f047c93

Browse files
committed
feat(example): wire broadcast button and persisted messages display
1 parent 7a804e4 commit f047c93

1 file changed

Lines changed: 75 additions & 65 deletions

File tree

expo-module/example/app/(tabs)/index.tsx

Lines changed: 75 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ export default function HomeScreen() {
207207

208208
const [unpairedRNodes, setUnpairedRNodes] = useState(0);
209209
const [liveBleCount, setLiveBleCount] = useState(0);
210+
const [storedMsgs, setStoredMsgs] = useState<any[]>([]);
210211

211212
// Identity hydration: read once from secure store on mount. Until hydrated,
212213
// we pass 'new' so Rust generates a fresh identity (which we'll then persist
@@ -218,20 +219,13 @@ export default function HomeScreen() {
218219
(async () => {
219220
try {
220221
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 });
223222
if (cancelled) return;
224223
if (raw) {
225224
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);
231226
}
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.
235229
} finally {
236230
if (!cancelled) setIdentityHydrated(true);
237231
}
@@ -240,48 +234,39 @@ export default function HomeScreen() {
240234
}, []);
241235

242236
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,
246240
} = useLxmf({
247241
identityHex: storedIdentity?.identity_hex ?? 'new',
248242
lxmfAddressHex: storedIdentity?.address_hex ?? 'new',
249243
logLevel: 3,
250244
});
251245

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).
254247
useEffect(() => {
255248
if (!isRunning) return;
256249
const idHex = getIdentityHex();
257250
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-
});
264251
if (!idHex || idHex.length !== 128) return;
265252
if (!addrHex || !/^[0-9a-fA-F]{32}$/.test(addrHex)) return;
266253
if (storedIdentity?.identity_hex === idHex && storedIdentity?.address_hex === addrHex) return;
267-
268254
const blob: StoredIdentity = {
269255
version: IDENTITY_SCHEMA_VERSION,
270256
identity_hex: idHex,
271257
address_hex: addrHex,
272258
created_at: new Date().toISOString(),
273259
};
274260
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 */ });
283263
}, [isRunning, status?.addressHex, storedIdentity, getIdentityHex]);
284264

265+
// Load persisted messages from SQLite whenever node starts.
266+
useEffect(() => {
267+
if (isRunning) setStoredMsgs(fetchMessages(50));
268+
}, [isRunning, fetchMessages]);
269+
285270
// ── Derived ───────────────────────────────────────────────────────────────
286271

287272
const counts = useMemo(() => {
@@ -326,21 +311,18 @@ export default function HomeScreen() {
326311
mode: LxmfNodeMode.ReticulumAndBle,
327312
tcpInterfaces: [{ host, port }],
328313
displayName: displayName.trim() || 'lxmf-mobile',
329-
isBeacon,
330314
});
331315
if (ok) {
332316
setTcpActive(true);
333-
startBLE();
334317
setBleActive(true);
335318
}
336-
}, [tcpHost, tcpPort, displayName, isBeacon, start, startBLE]);
319+
}, [tcpHost, tcpPort, displayName, start]);
337320

338321
const onStopTcp = useCallback(async () => {
339-
stopBLE();
340322
await stop();
341323
setTcpActive(false);
342324
setBleActive(false);
343-
}, [stop, stopBLE]);
325+
}, [stop]);
344326

345327
const onStartBle = useCallback(async () => {
346328
setTransportMsg('');
@@ -353,43 +335,31 @@ export default function HomeScreen() {
353335
]
354336
: [PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION];
355337
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)) {
358339
setTransportMsg('BLE permissions denied.');
359340
return;
360341
}
361342
}
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; }
382348
setBleActive(true);
383-
}, [isRunning, tcpHost, tcpPort, isBeacon, start, startBLE, displayName]);
349+
}, [start, displayName]);
384350

385351
const onStopBle = useCallback(async () => {
386-
stopBLE();
352+
await stop();
387353
setBleActive(false);
388354
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]);
393363

394364
// Poll for unpaired RNodes while BLE is active
395365
useEffect(() => {
@@ -617,13 +587,51 @@ export default function HomeScreen() {
617587
/>
618588
<View style={S.btnRow}>
619589
<Btn label="Send" onPress={onSend} disabled={!isRunning} />
590+
<Btn label="Broadcast" onPress={onBroadcast} disabled={!isRunning || !knownPeerHashes.length} />
620591
</View>
621592
{sendResult ? <Text style={S.feedback}>{sendResult}</Text> : null}
622593
</Accordion>
623594

624595
{/* ── 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 ? (
627635
<Text style={S.muted}>No messages yet.</Text>
628636
) : (
629637
msgEvts.map((e, i) => {
@@ -885,4 +893,6 @@ const S = StyleSheet.create({
885893
// Message card extras
886894
msgTitle: { color: C.text, fontSize: 13, fontWeight: '600', fontStyle: 'italic' },
887895
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' },
888898
});

0 commit comments

Comments
 (0)