@@ -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 - 9 a - f A - 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