Skip to content

Commit 57c355e

Browse files
authored
Merge pull request #800 from synonymdev/release/177-hotfix-update
feat: peer cards with labels + use ldk-node from GH
2 parents c8e6a4f + b5323e9 commit 57c355e

10 files changed

Lines changed: 242 additions & 90 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ suspend fun getData(): Result<Data> = withContext(Dispatchers.IO) {
184184
- NEVER wrap methods returning `Result<T>` in try-catch
185185
- PREFER to use `it` instead of explicit named parameters in lambdas e.g. `fn().onSuccess { log(it) }.onFailure { log(it) }`
186186
- NEVER inject ViewModels as dependencies - Only android activities and composable functions can use viewmodels
187+
- ALWAYS co-locate screen-specific ViewModels in the same package as their screen; only place ViewModels in `viewmodels/` when shared across multiple screens
187188
- NEVER hardcode strings and always preserve string resources
188189
- ALWAYS localize in ViewModels using injected `@ApplicationContext`, e.g. `context.getString()`
189190
- ALWAYS use `remember` for expensive Compose computations

app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ android {
5555
applicationId = "to.bitkit"
5656
minSdk = 28
5757
targetSdk = 36
58-
versionCode = 176
59-
versionName = "2.0.2"
58+
versionCode = 177
59+
versionName = "2.0.3"
6060
testInstrumentationRunner = "to.bitkit.test.HiltTestRunner"
6161
vectorDrawables {
6262
useSupportLibrary = true

app/src/main/java/to/bitkit/env/Env.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import to.bitkit.BuildConfig
88
import to.bitkit.ext.ensureDir
99
import to.bitkit.ext.of
1010
import to.bitkit.models.BlocktankNotificationType
11+
import to.bitkit.models.NodePeer
1112
import to.bitkit.utils.Logger
1213
import java.io.File
1314
import kotlin.io.path.Path
@@ -213,6 +214,21 @@ object Peers {
213214
val lnd1 = PeerDetails.of("039b8b4dd1d88c2c5db374290cda397a8f5d79f312d6ea5d5bfdfc7c6ff363eae3@34.65.111.104:9735")
214215
val lnd3 = PeerDetails.of("03816141f1dce7782ec32b66a300783b1d436b19777e7c686ed00115bd4b88ff4b@34.65.191.64:9735")
215216
val lnd4 = PeerDetails.of("02a371038863605300d0b3fc9de0cf5ccb57728b7f8906535709a831b16e311187@34.65.153.174:9735")
217+
218+
object Known {
219+
val stag = NodePeer(Peers.stag, name = "Synonym-Own-Regtest-0")
220+
val lnd1 = NodePeer(Peers.lnd1, name = "Blocktank-LND1")
221+
val lnd3 = NodePeer(Peers.lnd3, name = "Blocktank-LND3")
222+
val lnd4 = NodePeer(Peers.lnd4, name = "Blocktank-LND4")
223+
224+
fun find(peer: PeerDetails): NodePeer? = when (peer.nodeId) {
225+
stag.peerDetails.nodeId -> stag
226+
lnd1.peerDetails.nodeId -> lnd1
227+
lnd3.peerDetails.nodeId -> lnd3
228+
lnd4.peerDetails.nodeId -> lnd4
229+
else -> null
230+
}
231+
}
216232
}
217233

218234
private object ElectrumServers {
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package to.bitkit.models
2+
3+
import com.synonym.bitkitcore.ILspNode
4+
import org.lightningdevkit.ldknode.PeerDetails
5+
import to.bitkit.ext.ellipsisMiddle
6+
7+
data class NodePeer(
8+
val peerDetails: PeerDetails,
9+
val lspNode: ILspNode? = null,
10+
val name: String? = null,
11+
)
12+
13+
fun NodePeer.alias(): String =
14+
lspNode?.alias
15+
?: name
16+
?: peerDetails.nodeId.ellipsisMiddle(16)

app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt

Lines changed: 120 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,37 @@
11
package to.bitkit.ui
22

3+
import androidx.compose.foundation.background
34
import androidx.compose.foundation.layout.Arrangement
4-
import androidx.compose.foundation.layout.Box
55
import androidx.compose.foundation.layout.Column
66
import androidx.compose.foundation.layout.Row
77
import androidx.compose.foundation.layout.fillMaxWidth
88
import androidx.compose.foundation.layout.height
99
import androidx.compose.foundation.layout.padding
1010
import androidx.compose.foundation.layout.size
1111
import androidx.compose.foundation.rememberScrollState
12-
import androidx.compose.foundation.shape.CircleShape
1312
import androidx.compose.foundation.verticalScroll
1413
import androidx.compose.material.icons.Icons
1514
import androidx.compose.material.icons.filled.RemoveCircleOutline
15+
import androidx.compose.material.icons.filled.VerifiedUser
1616
import androidx.compose.material3.ExperimentalMaterial3Api
1717
import androidx.compose.material3.HorizontalDivider
1818
import androidx.compose.material3.Icon
19+
import androidx.compose.material3.IconButton
1920
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
2021
import androidx.compose.runtime.Composable
2122
import androidx.compose.runtime.getValue
2223
import androidx.compose.ui.Alignment
2324
import androidx.compose.ui.Modifier
24-
import androidx.compose.ui.draw.clip
2525
import androidx.compose.ui.platform.LocalContext
2626
import androidx.compose.ui.platform.testTag
2727
import androidx.compose.ui.res.stringResource
2828
import androidx.compose.ui.text.style.TextOverflow
2929
import androidx.compose.ui.tooling.preview.Preview
3030
import androidx.compose.ui.unit.dp
31+
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
3132
import androidx.lifecycle.compose.collectAsStateWithLifecycle
3233
import androidx.navigation.NavController
34+
import com.synonym.bitkitcore.ILspNode
3335
import org.lightningdevkit.ldknode.BalanceDetails
3436
import org.lightningdevkit.ldknode.BalanceSource
3537
import org.lightningdevkit.ldknode.BestBlock
@@ -43,14 +45,18 @@ import to.bitkit.ext.amountSats
4345
import to.bitkit.ext.balanceUiText
4446
import to.bitkit.ext.channelId
4547
import to.bitkit.ext.createChannelDetails
48+
import to.bitkit.ext.ellipsisMiddle
4649
import to.bitkit.ext.formatToString
4750
import to.bitkit.ext.uri
4851
import to.bitkit.models.NodeLifecycleState
49-
import to.bitkit.models.Toast
52+
import to.bitkit.models.NodePeer
53+
import to.bitkit.models.alias
5054
import to.bitkit.models.formatToModernDisplay
5155
import to.bitkit.repositories.LightningState
5256
import to.bitkit.ui.components.BodyM
57+
import to.bitkit.ui.components.BodyMSB
5358
import to.bitkit.ui.components.Caption
59+
import to.bitkit.ui.components.CaptionB
5460
import to.bitkit.ui.components.ChannelStatusUi
5561
import to.bitkit.ui.components.HorizontalSpacer
5662
import to.bitkit.ui.components.LightningChannel
@@ -65,6 +71,7 @@ import to.bitkit.ui.scaffold.ScreenColumn
6571
import to.bitkit.ui.shared.modifiers.clickableAlpha
6672
import to.bitkit.ui.theme.AppThemeSurface
6773
import to.bitkit.ui.theme.Colors
74+
import to.bitkit.ui.theme.Shapes
6875
import to.bitkit.ui.utils.copyToClipboard
6976
import to.bitkit.ui.utils.withAccent
7077
import kotlin.time.Clock.System.now
@@ -73,30 +80,22 @@ import kotlin.time.ExperimentalTime
7380
@Composable
7481
fun 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(
105104
private 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
392385
private 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

Comments
 (0)