From 929fa54fd074e056f155ce5625bc35494ffc3251 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 20:15:17 +0000 Subject: [PATCH 1/2] fix: resolve channel detail black screen and navigation issues - Change Routes.ChannelDetail to accept channelId parameter - Update ChannelDetailScreen to fetch channel on initialization - Replace early return with loading state when channel is null - Navigate directly to detail screen with channelId - Remove intermediate navigation through list screen - Fix predictive back gesture by eliminating blank composable state Fixes #668 Co-authored-by: Ovi Trif --- app/src/main/java/to/bitkit/ui/ContentView.kt | 4 +- .../settings/lightning/ChannelDetailScreen.kt | 58 +++++++++++++++---- .../lightning/LightningConnectionsScreen.kt | 16 ++--- 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index c89dd918d..5152a7f76 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -1204,9 +1204,11 @@ private fun NavGraphBuilder.lightningConnections( composableWithDefaultTransitions { val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ConnectionsNav) } val viewModel = hiltViewModel(parentEntry) + val route = it.toRoute() ChannelDetailScreen( navController = navController, viewModel = viewModel, + channelId = route.channelId, ) } composableWithDefaultTransitions { @@ -1836,7 +1838,7 @@ sealed interface Routes { data object LightningConnections : Routes @Serializable - data object ChannelDetail : Routes + data class ChannelDetail(val channelId: String) : Routes @Serializable data object CloseConnection : Routes diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index 04795bd4d..7876ebdc1 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -85,19 +85,42 @@ import java.util.Locale fun ChannelDetailScreen( navController: NavController, viewModel: LightningConnectionsViewModel, + channelId: String, ) { val context = LocalContext.current val app = appViewModel ?: return val wallet = walletViewModel ?: return + LaunchedEffect(channelId) { + viewModel.findAndSelectChannel(channelId) + } + val selectedChannel by viewModel.selectedChannel.collectAsStateWithLifecycle() - val channel = selectedChannel ?: return + val channel = selectedChannel val uiState by viewModel.uiState.collectAsStateWithLifecycle() val paidOrders by viewModel.blocktankRepo.blocktankState.collectAsStateWithLifecycle() + val lightningState by wallet.lightningState.collectAsStateWithLifecycle() + + if (channel == null) { + Content( + channel = null, + blocktankOrders = emptyList(), + cjitEntries = emptyList(), + txTime = null, + isRefreshing = false, + isClosedChannel = false, + onBack = { navController.popBackStack() }, + onRefresh = {}, + onCopyText = {}, + onOpenUrl = {}, + onSupport = {}, + onCloseConnection = {}, + ) + return + } val isClosedChannel = uiState.closedChannels.any { it.details.channelId == channel.details.channelId } - val lightningState by wallet.lightningState.collectAsStateWithLifecycle() // Fetch transaction details for funding transaction if available LaunchedEffect(channel.details.fundingTxo?.txid) { @@ -108,8 +131,8 @@ fun ChannelDetailScreen( // Fetch activity timestamp for transfer activity with matching channel ID LaunchedEffect(channel.details.channelId) { - channel.details.channelId?.let { channelId -> - viewModel.fetchActivityTimestamp(channelId) + channel.details.channelId.let { id -> + viewModel.fetchActivityTimestamp(id) } } @@ -148,7 +171,7 @@ fun ChannelDetailScreen( @Suppress("CyclomaticComplexMethod") @Composable private fun Content( - channel: ChannelUi, + channel: ChannelUi?, blocktankOrders: List = emptyList(), cjitEntries: List = emptyList(), txTime: ULong? = null, @@ -161,6 +184,24 @@ private fun Content( onSupport: (Any) -> Unit = {}, onCloseConnection: () -> Unit = {}, ) { + ScreenColumn { + AppTopBar( + titleText = channel?.name ?: "", + onBackClick = onBack, + actions = { DrawerNavIcon() }, + ) + + if (channel == null) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + CaptionB(text = stringResource(R.string.common__loading)) + } + return@ScreenColumn + } // Check if the channel was opened via CJIT val cjitEntry = cjitEntries.find { entry -> entry.channel?.fundingTx?.id == channel.details.fundingTxo?.txid @@ -184,13 +225,6 @@ private fun Content( val remoteBalance = (channel.details.inboundCapacityMsat / 1000u).toLong() val reserveBalance = (channel.details.unspendablePunishmentReserve ?: 0u).toLong() - ScreenColumn { - AppTopBar( - titleText = channel.name, - onBackClick = onBack, - actions = { DrawerNavIcon() }, - ) - PullToRefreshBox( isRefreshing = isRefreshing, onRefresh = onRefresh, diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt index f9611f6cc..857b1ac3c 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsScreen.kt @@ -88,18 +88,13 @@ fun LightningConnectionsScreen( viewModel.refreshObservedState() viewModel.clearSelectedChannel() viewModel.clearTransactionDetails() - } - LaunchedEffect(navController.currentBackStackEntry) { val selectedChannelId = navController.previousBackStackEntry?.savedStateHandle?.get("selectedChannelId") - if (selectedChannelId == null) return@LaunchedEffect - - navController.previousBackStackEntry?.savedStateHandle?.remove("selectedChannelId") - delay(CHANNEL_SELECTION_DELAY_MS) - if (viewModel.findAndSelectChannel(selectedChannelId)) { - navController.navigate(Routes.ChannelDetail) { + if (selectedChannelId != null) { + navController.previousBackStackEntry?.savedStateHandle?.remove("selectedChannelId") + delay(CHANNEL_SELECTION_DELAY_MS) + navController.navigate(Routes.ChannelDetail(selectedChannelId)) { launchSingleTop = true - popUpTo(Routes.ConnectionsNav) { inclusive = false } } } } @@ -112,8 +107,7 @@ fun LightningConnectionsScreen( viewModel.zipLogsForSharing { uri -> context.shareZipFile(uri) } }, onClickChannel = { channelUi -> - viewModel.setSelectedChannel(channelUi) - navController.navigate(Routes.ChannelDetail) + navController.navigate(Routes.ChannelDetail(channelUi.details.channelId)) }, onRefresh = { viewModel.onPullToRefresh() From 691ca22cfd3fbfeced7e506ab7796dbe042ae17f Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 22 Jan 2026 23:47:27 +0100 Subject: [PATCH 2/2] chore: add compose preview for channel loading --- .../settings/lightning/ChannelDetailScreen.kt | 64 +++++++++---------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index 7876ebdc1..02b98fcb2 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.pulltorefresh.PullToRefreshBox @@ -105,17 +106,7 @@ fun ChannelDetailScreen( if (channel == null) { Content( channel = null, - blocktankOrders = emptyList(), - cjitEntries = emptyList(), - txTime = null, - isRefreshing = false, - isClosedChannel = false, onBack = { navController.popBackStack() }, - onRefresh = {}, - onCopyText = {}, - onOpenUrl = {}, - onSupport = {}, - onCloseConnection = {}, ) return } @@ -193,37 +184,36 @@ private fun Content( if (channel == null) { Box( - modifier = Modifier - .fillMaxSize() - .padding(16.dp), - contentAlignment = Alignment.Center + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), ) { - CaptionB(text = stringResource(R.string.common__loading)) + CircularProgressIndicator() } return@ScreenColumn } - // Check if the channel was opened via CJIT - val cjitEntry = cjitEntries.find { entry -> - entry.channel?.fundingTx?.id == channel.details.fundingTxo?.txid - } - // Check if the channel was opened via blocktank order - val blocktankOrder = blocktankOrders.find { order -> - // real channel - if (channel.details.fundingTxo?.txid != null) { - order.channel?.fundingTx?.id == channel.details.fundingTxo?.txid - } else { - // fake channel - order.id == channel.details.channelId + // Check if the channel was opened via CJIT + val cjitEntry = cjitEntries.find { entry -> + entry.channel?.fundingTx?.id == channel.details.fundingTxo?.txid + } + + // Check if the channel was opened via blocktank order + val blocktankOrder = blocktankOrders.find { order -> + // real channel + if (channel.details.fundingTxo?.txid != null) { + order.channel?.fundingTx?.id == channel.details.fundingTxo?.txid + } else { + // fake channel + order.id == channel.details.channelId + } } - } - val order = blocktankOrder ?: cjitEntry + val order = blocktankOrder ?: cjitEntry - val capacity = channel.details.channelValueSats.toLong() - val localBalance = channel.details.amountOnClose.toLong() - val remoteBalance = (channel.details.inboundCapacityMsat / 1000u).toLong() - val reserveBalance = (channel.details.unspendablePunishmentReserve ?: 0u).toLong() + val capacity = channel.details.channelValueSats.toLong() + val localBalance = channel.details.amountOnClose.toLong() + val remoteBalance = (channel.details.inboundCapacityMsat / 1000u).toLong() + val reserveBalance = (channel.details.unspendablePunishmentReserve ?: 0u).toLong() PullToRefreshBox( isRefreshing = isRefreshing, @@ -662,6 +652,14 @@ private fun createSupportEmailIntent( return Intent(Intent.ACTION_SENDTO, uri) } +@Preview +@Composable +private fun PreviewLoadingState() { + AppThemeSurface { + Content(channel = null) + } +} + @Preview @Composable private fun PreviewOpenChannel() {