diff --git a/app/src/main/java/to/bitkit/ext/Activities.kt b/app/src/main/java/to/bitkit/ext/Activities.kt index 814a7fb3f..d011c2a9c 100644 --- a/app/src/main/java/to/bitkit/ext/Activities.kt +++ b/app/src/main/java/to/bitkit/ext/Activities.kt @@ -2,8 +2,7 @@ package to.bitkit.ext import uniffi.bitkitcore.Activity -val Activity.idValue: String - get() = when (this) { - is Activity.Lightning -> v1.id - is Activity.Onchain -> v1.txId - } +fun Activity.rawId(): String = when (this) { + is Activity.Lightning -> v1.id + is Activity.Onchain -> v1.id +} diff --git a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt index 323a3fd45..7b5b701e7 100644 --- a/app/src/main/java/to/bitkit/repositories/WalletRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/WalletRepo.kt @@ -21,7 +21,6 @@ import to.bitkit.data.entities.InvoiceTagEntity import to.bitkit.data.keychain.Keychain import to.bitkit.di.BgDispatcher import to.bitkit.env.Env -import to.bitkit.ext.idValue import to.bitkit.ext.toHex import to.bitkit.models.BalanceState import to.bitkit.models.NodeLifecycleState @@ -530,6 +529,12 @@ class WalletRepo @Inject constructor( is Activity.Onchain -> paymentHashOrTxId == v1.txId } + private val Activity.idValue: String + get() = when (this) { + is Activity.Lightning -> v1.id + is Activity.Onchain -> v1.txId + } + private suspend fun Scanner.OnChain.extractLightningHashOrAddress(): String { val address = this.invoice.address val lightningInvoice: String = this.invoice.params?.get("lightning") ?: address diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index eac7977f9..9239cf527 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -58,7 +58,7 @@ import to.bitkit.ui.screens.transfer.external.ExternalConnectionScreen import to.bitkit.ui.screens.transfer.external.ExternalFeeCustomScreen import to.bitkit.ui.screens.transfer.external.ExternalSuccessScreen import to.bitkit.ui.screens.wallets.HomeScreen -import to.bitkit.ui.screens.wallets.activity.ActivityItemScreen +import to.bitkit.ui.screens.wallets.activity.ActivityDetailScreen import to.bitkit.ui.screens.wallets.activity.ActivityExploreScreen import to.bitkit.ui.settings.BackupSettingsScreen import to.bitkit.ui.settings.BlocktankRegtestScreen @@ -148,7 +148,7 @@ fun ContentView( LaunchedEffect(appViewModel) { appViewModel.mainScreenEffect.collect { when (it) { - is MainScreenEffect.NavigateActivityDetail -> navController.navigate(Routes.ActivityItem(it.activityId)) + is MainScreenEffect.NavigateActivityDetail -> navController.navigate(Routes.ActivityDetail(it.activityId)) else -> Unit } } @@ -639,10 +639,10 @@ private fun NavGraphBuilder.activityItem( activityListViewModel: ActivityListViewModel, navController: NavHostController, ) { - composableWithDefaultTransitions { navBackEntry -> - ActivityItemScreen( - viewModel = activityListViewModel, - activityItem = navBackEntry.toRoute(), + composableWithDefaultTransitions { navBackEntry -> + ActivityDetailScreen( + listViewModel = activityListViewModel, + route = navBackEntry.toRoute(), onExploreClick = { id -> navController.navigateToActivityExplore(id) }, onBackClick = { navController.popBackStack() }, onCloseClick = { navController.navigateToHome() }, @@ -650,7 +650,7 @@ private fun NavGraphBuilder.activityItem( } composableWithDefaultTransitions { navBackEntry -> ActivityExploreScreen( - viewModel = activityListViewModel, + listViewModel = activityListViewModel, route = navBackEntry.toRoute(), onBackClick = { navController.popBackStack() }, onCloseClick = { navController.navigateToHome() }, @@ -827,7 +827,7 @@ fun NavController.navigateToTransferFunding() = navigate( ) fun NavController.navigateToActivityItem(id: String) = navigate( - route = Routes.ActivityItem(id), + route = Routes.ActivityDetail(id), ) fun NavController.navigateToActivityExplore(id: String) = navigate( @@ -1003,7 +1003,7 @@ object Routes { data object ExternalFeeCustom @Serializable - data class ActivityItem(val id: String) + data class ActivityDetail(val id: String) @Serializable data class ActivityExplore(val id: String) diff --git a/app/src/main/java/to/bitkit/ui/components/Button.kt b/app/src/main/java/to/bitkit/ui/components/Button.kt index 609b23957..90e88cd7e 100644 --- a/app/src/main/java/to/bitkit/ui/components/Button.kt +++ b/app/src/main/java/to/bitkit/ui/components/Button.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons @@ -65,7 +66,7 @@ fun PrimaryButton( contentPadding = PaddingValues(horizontal = size.horizontalPadding), modifier = Modifier .then(if (fullWidth) Modifier.fillMaxWidth() else Modifier) - .height(size.height) + .requiredHeight(size.height) .then(modifier) ) { if (isLoading) { diff --git a/app/src/main/java/to/bitkit/ui/components/ForgotPinSheet.kt b/app/src/main/java/to/bitkit/ui/components/ForgotPinSheet.kt index 802e056ce..81a9c8e80 100644 --- a/app/src/main/java/to/bitkit/ui/components/ForgotPinSheet.kt +++ b/app/src/main/java/to/bitkit/ui/components/ForgotPinSheet.kt @@ -1,8 +1,6 @@ package to.bitkit.ui.components import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize @@ -10,7 +8,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.material3.BottomSheetDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.rememberModalBottomSheetState @@ -42,16 +39,7 @@ fun ForgotPinSheet( sheetState = sheetState, shape = AppShapes.sheet, containerColor = Colors.Black, - dragHandle = { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .fillMaxWidth() - .background(color = Colors.Gray6) - ) { - BottomSheetDefaults.DragHandle() - } - }, + dragHandle = { ModalBottomSheetHandle() }, modifier = Modifier .fillMaxSize() .padding(top = ModalSheetTopPadding) diff --git a/app/src/main/java/to/bitkit/ui/components/ModalBottomSheetHandle.kt b/app/src/main/java/to/bitkit/ui/components/ModalBottomSheetHandle.kt new file mode 100644 index 000000000..7ce58962c --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/components/ModalBottomSheetHandle.kt @@ -0,0 +1,26 @@ +package to.bitkit.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import to.bitkit.ui.theme.Colors + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun ModalBottomSheetHandle( + modifier: Modifier = Modifier, +) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .fillMaxWidth() + .background(color = Colors.Gray6) + ) { + BottomSheetDefaults.DragHandle() + } +} diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityItemScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt similarity index 80% rename from app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityItemScreen.kt rename to app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt index a94e530b6..be576c714 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityItemScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -16,7 +17,11 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -26,10 +31,11 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.ext.ellipsisMiddle -import to.bitkit.ext.idValue +import to.bitkit.ext.rawId import to.bitkit.ext.toActivityItemDate import to.bitkit.ext.toActivityItemTime import to.bitkit.models.Toast @@ -45,12 +51,14 @@ import to.bitkit.ui.components.Title import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.CloseNavIcon import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.screens.wallets.activity.components.ActivityAddTagSheet import to.bitkit.ui.screens.wallets.activity.components.ActivityIcon import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.copyToClipboard import to.bitkit.ui.utils.getScreenTitleRes +import to.bitkit.viewmodels.ActivityDetailViewModel import to.bitkit.viewmodels.ActivityListViewModel import uniffi.bitkitcore.Activity import uniffi.bitkitcore.LightningActivity @@ -59,28 +67,39 @@ import uniffi.bitkitcore.PaymentState import uniffi.bitkitcore.PaymentType @Composable -fun ActivityItemScreen( - viewModel: ActivityListViewModel, - activityItem: Routes.ActivityItem, +fun ActivityDetailScreen( + listViewModel: ActivityListViewModel, + detailViewModel: ActivityDetailViewModel = hiltViewModel(), + route: Routes.ActivityDetail, onExploreClick: (String) -> Unit, onBackClick: () -> Unit, onCloseClick: () -> Unit, ) { - val activities by viewModel.filteredActivities.collectAsStateWithLifecycle() - val item = activities?.find { it.idValue == activityItem.id } + val activities by listViewModel.filteredActivities.collectAsStateWithLifecycle() + val item = activities?.find { it.rawId() == route.id } ?: return val app = appViewModel ?: return val copyToastTitle = stringResource(R.string.common__copied) + val tags by detailViewModel.tags.collectAsStateWithLifecycle() + var showAddTagSheet by remember { mutableStateOf(false) } + + LaunchedEffect(item) { + detailViewModel.setActivity(item) + } + ScreenColumn { AppTopBar( titleText = stringResource(item.getScreenTitleRes()), onBackClick = onBackClick, actions = { CloseNavIcon(onClick = onCloseClick) }, ) - ActivityItemView( + ActivityDetailContent( item = item, + tags = tags, + onRemoveTag = { detailViewModel.removeTag(it) }, + onAddTagClick = { showAddTagSheet = true }, onExploreClick = onExploreClick, onCopy = { text -> app.toast( @@ -88,15 +107,24 @@ fun ActivityItemScreen( title = copyToastTitle, description = text.ellipsisMiddle(40) ) - } + }, ) + if (showAddTagSheet) { + ActivityAddTagSheet( + activityViewModel = detailViewModel, + onDismiss = { showAddTagSheet = false }, + ) + } } } @OptIn(ExperimentalLayoutApi::class) @Composable -private fun ActivityItemView( +private fun ActivityDetailContent( item: Activity, + tags: List, + onRemoveTag: (String) -> Unit, + onAddTagClick: () -> Unit, onExploreClick: (String) -> Unit, onCopy: (String) -> Unit, ) { @@ -131,7 +159,9 @@ private fun ActivityItemView( } Column( - modifier = Modifier.padding(16.dp) + modifier = Modifier + .fillMaxSize() + .padding(16.dp) ) { // header section: amount + icon Row( @@ -253,32 +283,28 @@ private fun ActivityItemView( } // Tags section - Column(modifier = Modifier.fillMaxWidth()) { - Caption13Up( - text = stringResource(R.string.wallet__tags), - color = Colors.White64, - modifier = Modifier.padding(top = 16.dp, bottom = 8.dp) - ) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - // TODO: Get actual tags - TagButton( - text = "test1", - onClick = {}, - ) - TagButton( - text = "test2", - onClick = {}, - ) - TagButton( - text = "test3", - onClick = {}, + if (tags.isNotEmpty()) { + Column(modifier = Modifier.fillMaxWidth()) { + Caption13Up( + text = stringResource(R.string.wallet__tags), + color = Colors.White64, + modifier = Modifier.padding(top = 16.dp, bottom = 8.dp) ) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + tags.forEach { tag -> + TagButton( + text = tag, + displayIconClose = true, + onClick = { onRemoveTag(tag) } + ) + } + } + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider() } - Spacer(modifier = Modifier.height(16.dp)) - HorizontalDivider() } // Note section for Lightning payments with message @@ -318,7 +344,6 @@ private fun ActivityItemView( Spacer(modifier = Modifier.height(16.dp)) // Action buttons - // TODO add buttons action Column( verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth() @@ -344,7 +369,7 @@ private fun ActivityItemView( PrimaryButton( text = stringResource(R.string.wallet__activity_tag), size = ButtonSize.Small, - onClick = { /* TODO: Implement tag functionality */ }, + onClick = onAddTagClick, icon = { Icon( painter = painterResource(R.drawable.ic_tag), @@ -378,7 +403,7 @@ private fun ActivityItemView( PrimaryButton( text = stringResource(R.string.wallet__activity_explore), size = ButtonSize.Small, - onClick = { onExploreClick(item.idValue) }, + onClick = { onExploreClick(item.rawId()) }, icon = { Icon( painter = painterResource(R.drawable.ic_git_branch), @@ -512,64 +537,66 @@ private fun ZigzagDivider() { } } -@Preview +@Preview(showSystemUi = true) @Composable private fun PreviewLightningSent() { AppThemeSurface { - Column { - ActivityItemView( - item = Activity.Lightning( - v1 = LightningActivity( - id = "test-lightning-1", - txType = PaymentType.SENT, - status = PaymentState.SUCCEEDED, - value = 50000UL, - fee = 1UL, - invoice = "lnbc...", - message = "Thanks for paying at the bar. Here's my share.", - timestamp = (System.currentTimeMillis() / 1000).toULong(), - preimage = null, - createdAt = null, - updatedAt = null, - ) - ), - onExploreClick = {}, - onCopy = {}, - ) - } + ActivityDetailContent( + item = Activity.Lightning( + v1 = LightningActivity( + id = "test-lightning-1", + txType = PaymentType.SENT, + status = PaymentState.SUCCEEDED, + value = 50000UL, + fee = 1UL, + invoice = "lnbc...", + message = "Thanks for paying at the bar. Here's my share.", + timestamp = (System.currentTimeMillis() / 1000).toULong(), + preimage = null, + createdAt = null, + updatedAt = null, + ) + ), + tags = listOf("Lunch", "Drinks"), + onRemoveTag = {}, + onAddTagClick = {}, + onExploreClick = {}, + onCopy = {}, + ) } } -@Preview +@Preview(showSystemUi = true, showBackground = true) @Composable private fun PreviewOnchain() { AppThemeSurface { - Column { - ActivityItemView( - item = Activity.Onchain( - v1 = OnchainActivity( - id = "test-onchain-1", - txType = PaymentType.RECEIVED, - txId = "abc123", - value = 100000UL, - fee = 500UL, - feeRate = 8UL, - address = "bc1...", - confirmed = true, - timestamp = (System.currentTimeMillis() / 1000 - 3600).toULong(), - isBoosted = false, - isTransfer = false, - doesExist = true, - confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(), - channelId = null, - transferTxId = null, - createdAt = null, - updatedAt = null, - ) - ), - onExploreClick = {}, - onCopy = {}, - ) - } + ActivityDetailContent( + item = Activity.Onchain( + v1 = OnchainActivity( + id = "test-onchain-1", + txType = PaymentType.RECEIVED, + txId = "abc123", + value = 100000UL, + fee = 500UL, + feeRate = 8UL, + address = "bc1...", + confirmed = true, + timestamp = (System.currentTimeMillis() / 1000 - 3600).toULong(), + isBoosted = false, + isTransfer = false, + doesExist = true, + confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(), + channelId = null, + transferTxId = null, + createdAt = null, + updatedAt = null, + ) + ), + tags = emptyList(), + onRemoveTag = {}, + onAddTagClick = {}, + onExploreClick = {}, + onCopy = {}, + ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt index ba5c7d3f0..e6fe414ac 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt @@ -33,7 +33,7 @@ import org.lightningdevkit.ldknode.Network import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ext.ellipsisMiddle -import to.bitkit.ext.idValue +import to.bitkit.ext.rawId import to.bitkit.models.Toast import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel @@ -62,17 +62,17 @@ import uniffi.bitkitcore.PaymentType @Composable fun ActivityExploreScreen( - viewModel: ActivityListViewModel, + listViewModel: ActivityListViewModel, + detailViewModel: ActivityDetailViewModel = hiltViewModel(), route: Routes.ActivityExplore, onBackClick: () -> Unit, onCloseClick: () -> Unit, ) { - val activities by viewModel.filteredActivities.collectAsStateWithLifecycle() - val item = activities?.find { it.idValue == route.id } + val activities by listViewModel.filteredActivities.collectAsStateWithLifecycle() + val item = activities?.find { it.rawId() == route.id } ?: return val app = appViewModel ?: return - val detailViewModel: ActivityDetailViewModel = hiltViewModel() val txDetails by detailViewModel.txDetails.collectAsStateWithLifecycle() val copyToastTitle = stringResource(R.string.common__copied) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityAddTagSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityAddTagSheet.kt new file mode 100644 index 000000000..330263d6c --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityAddTagSheet.kt @@ -0,0 +1,74 @@ +package to.bitkit.ui.screens.wallets.activity.components + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import to.bitkit.ui.components.ModalBottomSheetHandle +import to.bitkit.ui.screens.wallets.send.AddTagContent +import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.theme.AppShapes +import to.bitkit.ui.theme.Colors +import to.bitkit.viewmodels.ActivityDetailViewModel +import to.bitkit.viewmodels.TagsViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ActivityAddTagSheet( + activityViewModel: ActivityDetailViewModel, + tagsViewModel: TagsViewModel = hiltViewModel(), + onDismiss: () -> Unit, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val uiState by tagsViewModel.uiState.collectAsState() + + LaunchedEffect(Unit) { + tagsViewModel.loadTagSuggestions() + } + + DisposableEffect(Unit) { + onDispose { + tagsViewModel.onInputUpdated("") + } + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + shape = AppShapes.sheet, + containerColor = Colors.Black, + dragHandle = { ModalBottomSheetHandle() }, + modifier = Modifier + .fillMaxWidth() + .imePadding() + ) { + AddTagContent( + uiState = uiState, + onTagSelected = { tag -> + activityViewModel.addTag(tag) + onDismiss() + }, + onTagConfirmed = { tag -> + if (tag.isNotBlank()) { + activityViewModel.addTag(tag) + onDismiss() + } + }, + onInputUpdated = { newText -> tagsViewModel.onInputUpdated(newText) }, + onBack = onDismiss, + modifier = Modifier + .height(400.dp) + .gradientBackground() + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt index 7072a7163..b96447c5d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/components/ActivityRow.kt @@ -18,7 +18,7 @@ import androidx.compose.ui.unit.dp import to.bitkit.R import to.bitkit.ext.DatePattern import to.bitkit.ext.formatted -import to.bitkit.ext.idValue +import to.bitkit.ext.rawId import to.bitkit.models.PrimaryDisplay import to.bitkit.ui.LocalCurrencies import to.bitkit.ui.components.BodyMSB @@ -63,7 +63,7 @@ fun ActivityRow( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .clickableAlpha { onClick(item.idValue) } + .clickableAlpha { onClick(item.rawId()) } .padding(vertical = 16.dp) ) { ActivityIcon(activity = item, size = 32.dp) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt index 4c8d64d3c..646f39efd 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/AddTagScreen.kt @@ -8,13 +8,14 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier @@ -25,8 +26,10 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import to.bitkit.R import to.bitkit.ui.components.Caption13Up +import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.TagButton import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.AppTextFieldDefaults import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -42,6 +45,10 @@ fun AddTagScreen( ) { val uiState: AddTagUiState by viewModel.uiState.collectAsState() + LaunchedEffect(Unit) { + viewModel.loadTagSuggestions() + } + AddTagContent( uiState = uiState, onTagSelected = onTagSelected, @@ -49,7 +56,8 @@ fun AddTagScreen( onTagSelected(tag) }, onInputUpdated = { newText -> viewModel.onInputUpdated(newText) }, - onBack = onBack + onBack = onBack, + modifier = Modifier.fillMaxSize() ) } @@ -61,16 +69,16 @@ fun AddTagContent( onTagConfirmed: (String) -> Unit, onInputUpdated: (String) -> Unit, onBack: () -> Unit, + modifier: Modifier = Modifier, ) { Column( - modifier = Modifier.fillMaxSize() + modifier = modifier.navigationBarsPadding() ) { SheetTopBar(stringResource(R.string.wallet__tags_add)) { onBack() } - - Spacer(Modifier.height(32.dp)) + Spacer(Modifier.height(16.dp)) Column( modifier = Modifier @@ -106,8 +114,8 @@ fun AddTagContent( onValueChange = onInputUpdated, maxLines = 1, singleLine = true, - colors = AppTextFieldDefaults.noIndicatorColors, - shape = MaterialTheme.shapes.small, + colors = AppTextFieldDefaults.semiTransparent, + shape = AppShapes.small, keyboardOptions = KeyboardOptions( imeAction = ImeAction.Done ), @@ -116,6 +124,15 @@ fun AddTagContent( }), modifier = Modifier.fillMaxWidth() ) + + Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.weight(1f)) + PrimaryButton( + text = stringResource(R.string.wallet__tags_add_button), + onClick = { onTagConfirmed(uiState.tagInput) }, + enabled = uiState.tagInput.isNotBlank(), + ) + Spacer(modifier = Modifier.height(16.dp)) } } } @@ -141,7 +158,7 @@ private fun Preview() { private fun Preview2() { AppThemeSurface { AddTagContent( - uiState = AddTagUiState(), + uiState = AddTagUiState(tagInput = "Lunch"), onTagSelected = {}, onInputUpdated = {}, onTagConfirmed = {}, diff --git a/app/src/main/java/to/bitkit/viewmodels/ActivityDetailViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/ActivityDetailViewModel.kt index af185ae03..a2d60e6c0 100644 --- a/app/src/main/java/to/bitkit/viewmodels/ActivityDetailViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/ActivityDetailViewModel.kt @@ -6,18 +6,71 @@ import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import to.bitkit.ext.rawId +import to.bitkit.services.CoreService import to.bitkit.utils.AddressChecker import to.bitkit.utils.Logger import to.bitkit.utils.TxDetails +import uniffi.bitkitcore.Activity import javax.inject.Inject @HiltViewModel class ActivityDetailViewModel @Inject constructor( private val addressChecker: AddressChecker, + private val coreService: CoreService, ) : ViewModel() { private val _txDetails = MutableStateFlow(null) val txDetails = _txDetails.asStateFlow() + private val _tags = MutableStateFlow>(emptyList()) + val tags = _tags.asStateFlow() + + private var activity: Activity? = null + + fun setActivity(activity: Activity) { + this.activity = activity + loadTags() + } + + fun loadTags() { + val id = activity?.rawId() ?: return + viewModelScope.launch { + try { + val activityTags = coreService.activity.tags(forActivityId = id) + _tags.value = activityTags + } catch (e: Exception) { + Logger.error("Failed to load tags for activity $id", e, TAG) + _tags.value = emptyList() + } + } + } + + fun removeTag(tag: String) { + val id = activity?.rawId() ?: return + viewModelScope.launch { + try { + coreService.activity.dropTags(fromActivityId = id, tags = listOf(tag)) + loadTags() + } catch (e: Exception) { + Logger.error("Failed to remove tag $tag from activity $id", e, TAG) + } + } + } + + fun addTag(tag: String) { + val id = activity?.rawId() ?: return + viewModelScope.launch { + try { + val result = coreService.activity.appendTags(toActivityId = id, tags = listOf(tag)) + if (result.isSuccess) { + loadTags() + } + } catch (e: Exception) { + Logger.error("Failed to add tag $tag to activity $id", e, TAG) + } + } + } + fun fetchTransactionDetails(txid: String) { viewModelScope.launch { try { diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 7f0642579..811705477 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -29,7 +29,7 @@ import to.bitkit.data.keychain.Keychain import to.bitkit.di.BgDispatcher import to.bitkit.env.Env import to.bitkit.ext.WatchResult -import to.bitkit.ext.idValue +import to.bitkit.ext.rawId import to.bitkit.ext.removeSpaces import to.bitkit.ext.watchUntil import to.bitkit.models.NewTransactionSheetDetails @@ -51,7 +51,6 @@ import to.bitkit.ui.screens.wallets.send.SendRoute import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.utils.Logger import to.bitkit.utils.ResourceProvider -import uniffi.bitkitcore.Activity import uniffi.bitkitcore.ActivityFilter import uniffi.bitkitcore.LightningInvoice import uniffi.bitkitcore.OnChainInvoice @@ -636,7 +635,7 @@ class AppViewModel @Inject constructor( return@launch } - mainScreenEffect(MainScreenEffect.NavigateActivityDetail(activity.idValue)) + mainScreenEffect(MainScreenEffect.NavigateActivityDetail(activity.rawId())) } } diff --git a/app/src/main/java/to/bitkit/viewmodels/TagsViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TagsViewModel.kt index 1d5994a55..71946a3fa 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TagsViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TagsViewModel.kt @@ -21,7 +21,7 @@ class TagsViewModel @Inject constructor( val uiState = _uiState.asStateFlow() init { - getPossibleTags() + loadTagSuggestions() } fun addTags(activityId: String, tags: List) { @@ -42,7 +42,7 @@ class TagsViewModel @Inject constructor( _uiState.update { it.copy(tagInput = input) } } - private fun getPossibleTags() { + fun loadTagSuggestions() { viewModelScope.launch(Dispatchers.IO) { val tags = coreService.activity.allPossibleTags() _uiState.update { it.copy(tagsSuggestions = tags) }