11package to.bitkit.ui
22
3+ import androidx.compose.foundation.background
34import androidx.compose.foundation.layout.Arrangement
4- import androidx.compose.foundation.layout.Box
55import androidx.compose.foundation.layout.Column
66import androidx.compose.foundation.layout.Row
77import androidx.compose.foundation.layout.fillMaxWidth
88import androidx.compose.foundation.layout.height
99import androidx.compose.foundation.layout.padding
1010import androidx.compose.foundation.layout.size
1111import androidx.compose.foundation.rememberScrollState
12- import androidx.compose.foundation.shape.CircleShape
1312import androidx.compose.foundation.verticalScroll
1413import androidx.compose.material.icons.Icons
1514import androidx.compose.material.icons.filled.RemoveCircleOutline
15+ import androidx.compose.material.icons.filled.VerifiedUser
1616import androidx.compose.material3.ExperimentalMaterial3Api
1717import androidx.compose.material3.HorizontalDivider
1818import androidx.compose.material3.Icon
19+ import androidx.compose.material3.IconButton
1920import androidx.compose.material3.pulltorefresh.PullToRefreshBox
2021import androidx.compose.runtime.Composable
2122import androidx.compose.runtime.getValue
2223import androidx.compose.ui.Alignment
2324import androidx.compose.ui.Modifier
24- import androidx.compose.ui.draw.clip
2525import androidx.compose.ui.platform.LocalContext
2626import androidx.compose.ui.platform.testTag
2727import androidx.compose.ui.res.stringResource
2828import androidx.compose.ui.text.style.TextOverflow
2929import androidx.compose.ui.tooling.preview.Preview
3030import androidx.compose.ui.unit.dp
31+ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
3132import androidx.lifecycle.compose.collectAsStateWithLifecycle
3233import androidx.navigation.NavController
34+ import com.synonym.bitkitcore.ILspNode
3335import org.lightningdevkit.ldknode.BalanceDetails
3436import org.lightningdevkit.ldknode.BalanceSource
3537import org.lightningdevkit.ldknode.BestBlock
@@ -43,14 +45,18 @@ import to.bitkit.ext.amountSats
4345import to.bitkit.ext.balanceUiText
4446import to.bitkit.ext.channelId
4547import to.bitkit.ext.createChannelDetails
48+ import to.bitkit.ext.ellipsisMiddle
4649import to.bitkit.ext.formatToString
4750import to.bitkit.ext.uri
4851import to.bitkit.models.NodeLifecycleState
49- import to.bitkit.models.Toast
52+ import to.bitkit.models.NodePeer
53+ import to.bitkit.models.alias
5054import to.bitkit.models.formatToModernDisplay
5155import to.bitkit.repositories.LightningState
5256import to.bitkit.ui.components.BodyM
57+ import to.bitkit.ui.components.BodyMSB
5358import to.bitkit.ui.components.Caption
59+ import to.bitkit.ui.components.CaptionB
5460import to.bitkit.ui.components.ChannelStatusUi
5561import to.bitkit.ui.components.HorizontalSpacer
5662import to.bitkit.ui.components.LightningChannel
@@ -65,6 +71,7 @@ import to.bitkit.ui.scaffold.ScreenColumn
6571import to.bitkit.ui.shared.modifiers.clickableAlpha
6672import to.bitkit.ui.theme.AppThemeSurface
6773import to.bitkit.ui.theme.Colors
74+ import to.bitkit.ui.theme.Shapes
6875import to.bitkit.ui.utils.copyToClipboard
6976import to.bitkit.ui.utils.withAccent
7077import kotlin.time.Clock.System.now
@@ -73,30 +80,22 @@ import kotlin.time.ExperimentalTime
7380@Composable
7481fun NodeInfoScreen (
7582 navController : NavController ,
83+ viewModel : NodeInfoViewModel = hiltViewModel(),
7684) {
7785 val wallet = walletViewModel ? : return
78- val app = appViewModel ? : return
79- val settings = settingsViewModel ? : return
80- val context = LocalContext .current
8186
8287 val isRefreshing by wallet.isRefreshing.collectAsStateWithLifecycle()
83- val isDevModeEnabled by settings.isDevModeEnabled.collectAsStateWithLifecycle()
8488 val lightningState by wallet.lightningState.collectAsStateWithLifecycle()
89+ val peers by viewModel.peers.collectAsStateWithLifecycle()
8590
8691 Content (
8792 lightningState = lightningState,
93+ peers = peers,
8894 isRefreshing = isRefreshing,
89- isDevModeEnabled = isDevModeEnabled,
90- onBack = { navController.popBackStack() },
91- onRefresh = { wallet.onPullToRefresh() },
92- onDisconnectPeer = { wallet.disconnectPeer(it) },
93- onCopy = { text ->
94- app.toast(
95- type = Toast .ToastType .SUCCESS ,
96- title = context.getString(R .string.common__copied),
97- description = text
98- )
99- },
95+ onBack = navController::popBackStack,
96+ onRefresh = wallet::onPullToRefresh,
97+ onDisconnectPeer = viewModel::disconnectPeer,
98+ onCopy = viewModel::onCopy,
10099 )
101100}
102101
@@ -105,7 +104,7 @@ fun NodeInfoScreen(
105104private fun Content (
106105 lightningState : LightningState ,
107106 isRefreshing : Boolean = false,
108- isDevModeEnabled : Boolean ,
107+ peers : List < NodePeer > = emptyList() ,
109108 onBack : () -> Unit = {},
110109 onRefresh : () -> Unit = {},
111110 onDisconnectPeer : (PeerDetails ) -> Unit = {},
@@ -130,36 +129,30 @@ private fun Content(
130129 nodeId = lightningState.nodeId,
131130 onCopy = onCopy,
132131 )
132+ NodeStateSection (
133+ nodeLifecycleState = lightningState.nodeLifecycleState,
134+ nodeStatus = lightningState.nodeStatus,
135+ )
136+ lightningState.balances?.let { details ->
137+ WalletBalancesSection (balanceDetails = details)
133138
134- if (isDevModeEnabled) {
135- NodeStateSection (
136- nodeLifecycleState = lightningState.nodeLifecycleState,
137- nodeStatus = lightningState.nodeStatus,
138- )
139-
140- lightningState.balances?.let { details ->
141- WalletBalancesSection (balanceDetails = details)
142-
143- if (details.lightningBalances.isNotEmpty()) {
144- LightningBalancesSection (balances = details.lightningBalances)
145- }
146- }
147-
148- if (lightningState.channels.isNotEmpty()) {
149- ChannelsSection (
150- channels = lightningState.channels,
151- onCopy = onCopy,
152- )
153- }
154-
155- if (lightningState.peers.isNotEmpty()) {
156- PeersSection (
157- peers = lightningState.peers,
158- onDisconnectPeer = onDisconnectPeer,
159- onCopy = onCopy,
160- )
139+ if (details.lightningBalances.isNotEmpty()) {
140+ LightningBalancesSection (balances = details.lightningBalances)
161141 }
162142 }
143+ if (lightningState.channels.isNotEmpty()) {
144+ ChannelsSection (
145+ channels = lightningState.channels,
146+ onCopy = onCopy,
147+ )
148+ }
149+ if (peers.isNotEmpty()) {
150+ PeersSection (
151+ peers = peers,
152+ onDisconnectPeer = onDisconnectPeer,
153+ onCopy = onCopy,
154+ )
155+ }
163156 VerticalSpacer (16 .dp)
164157 }
165158 }
@@ -390,46 +383,67 @@ private fun ChannelsSection(
390383
391384@Composable
392385private fun PeersSection (
393- peers : List <PeerDetails >,
394- onDisconnectPeer : (PeerDetails ) -> Unit ,
386+ peers : List <NodePeer >,
387+ onDisconnectPeer : (PeerDetails ) -> Unit = {} ,
395388 onCopy : (String ) -> Unit = {},
396389) {
397390 Column (modifier = Modifier .fillMaxWidth()) {
398391 SectionHeader (" Peers" )
399- peers.forEach { peer ->
392+ Column (verticalArrangement = Arrangement .spacedBy(8 .dp)) {
393+ peers.forEach { peer ->
394+ PeerCard (
395+ peer = peer,
396+ onCopy = onCopy,
397+ onDisconnectPeer = onDisconnectPeer,
398+ )
399+ }
400+ }
401+ }
402+ }
403+
404+ @Composable
405+ private fun PeerCard (
406+ peer : NodePeer ,
407+ onCopy : (String ) -> Unit ,
408+ onDisconnectPeer : (PeerDetails ) -> Unit ,
409+ ) {
410+ val uri = peer.peerDetails.uri
411+ Row (
412+ verticalAlignment = Alignment .CenterVertically ,
413+ modifier = Modifier
414+ .fillMaxWidth()
415+ .clickableAlpha(onClick = copyToClipboard(uri) { onCopy(it) })
416+ .background(color = Colors .Gray6 , shape = Shapes .medium)
417+ .padding(16 .dp)
418+ ) {
419+ Column (modifier = Modifier .weight(1f )) {
400420 Row (
401- horizontalArrangement = Arrangement .spacedBy(8 .dp),
402421 verticalAlignment = Alignment .CenterVertically ,
403- modifier = Modifier .height(52 .dp)
422+ horizontalArrangement = Arrangement .spacedBy(4 .dp),
423+ modifier = Modifier .fillMaxWidth()
404424 ) {
405- BodyM (
406- text = peer.uri,
407- maxLines = 1 ,
408- overflow = TextOverflow .MiddleEllipsis ,
409- modifier = Modifier
410- .weight(1f )
411- .clickableAlpha(
412- onClick = copyToClipboard(peer.uri) {
413- onCopy(it)
414- }
415- )
416- )
417- Box (
418- contentAlignment = Alignment .Center ,
419- modifier = Modifier
420- .size(16 .dp)
421- .clip(CircleShape )
422- .clickableAlpha(onClick = { onDisconnectPeer(peer) })
423- ) {
425+ BodyMSB (text = peer.alias())
426+ if (peer.lspNode != null ) {
424427 Icon (
425- imageVector = Icons .Default . RemoveCircleOutline ,
426- contentDescription = stringResource( R .string.common__close) ,
427- tint = Colors .Red ,
428- modifier = Modifier .size(16 .dp)
428+ imageVector = Icons .Filled . VerifiedUser ,
429+ contentDescription = null ,
430+ tint = Colors .White32 ,
431+ modifier = Modifier .size(16 .dp),
429432 )
430433 }
431434 }
432- HorizontalDivider ()
435+ CaptionB (
436+ text = peer.peerDetails.nodeId.ellipsisMiddle(@Suppress(" MagicNumber" ) 24 ),
437+ color = Colors .White64 ,
438+ maxLines = 1 ,
439+ )
440+ }
441+ IconButton (onClick = { onDisconnectPeer(peer.peerDetails) }) {
442+ Icon (
443+ imageVector = Icons .Default .RemoveCircleOutline ,
444+ contentDescription = stringResource(R .string.common__close),
445+ tint = Colors .Red ,
446+ )
433447 }
434448 }
435449}
@@ -452,27 +466,47 @@ private fun ChannelDetailRow(
452466 }
453467}
454468
455- @Preview(showSystemUi = true )
469+ private fun previewPeers () = listOf (
470+ NodePeer (
471+ peerDetails = Peers .stag,
472+ lspNode = ILspNode (
473+ alias = " Blocktank-LND1" ,
474+ pubkey = Peers .stag.nodeId,
475+ connectionStrings = listOf (),
476+ readonly = null ,
477+ ),
478+ ),
479+ NodePeer (
480+ peerDetails = PeerDetails (
481+ nodeId = " 0448a2b7c2d3f4e5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9" ,
482+ address = " 192.168.1.1:9735" ,
483+ isConnected = true ,
484+ isPersisted = false ,
485+ ),
486+ lspNode = null ,
487+ ),
488+ )
489+
490+ @Preview
456491@Composable
457- private fun Preview () {
492+ private fun PreviewPeersSection () {
458493 AppThemeSurface {
459- Content (
460- isDevModeEnabled = false ,
461- lightningState = LightningState (
462- nodeId = " 0348a2b7c2d3f4e5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9" ,
463- ),
464- )
494+ Column (modifier = Modifier .padding(16 .dp)) {
495+ PeersSection (
496+ peers = previewPeers(),
497+ )
498+ }
465499 }
466500}
467501
468502@OptIn(ExperimentalTime ::class )
469503@Preview(showSystemUi = true )
470504@Composable
471- private fun PreviewDevMode () {
505+ private fun Preview () {
472506 AppThemeSurface {
473507 val syncTime = now().epochSeconds.toULong()
474508 Content (
475- isDevModeEnabled = true ,
509+ peers = previewPeers() ,
476510 lightningState = LightningState (
477511 nodeLifecycleState = NodeLifecycleState .Running ,
478512 nodeStatus = NodeStatus (
0 commit comments