diff --git a/app/src/main/java/to/bitkit/ext/DateTime.kt b/app/src/main/java/to/bitkit/ext/DateTime.kt index 37435fce1..b74076438 100644 --- a/app/src/main/java/to/bitkit/ext/DateTime.kt +++ b/app/src/main/java/to/bitkit/ext/DateTime.kt @@ -17,6 +17,9 @@ fun Instant.formatted(pattern: String = DatePattern.DATE_TIME): String { object DatePattern { const val DATE_TIME = "dd/MM/yyyy, HH:mm" const val INVOICE_EXPIRY = "MMM dd, h:mm a" - const val ACTIVITY_ITEM = "MMMM d yyyy, HH:mm" + const val ACTIVITY_DATE = "MMMM d" + const val ACTIVITY_ROW_DATE = "MMMM d, HH:mm" + const val ACTIVITY_ROW_DATE_YEAR = "MMMM d yyyy, HH:mm" + const val ACTIVITY_TIME = "h:mm" const val LOG_FILE = "yyyy-MM-dd_HH-mm-ss" } diff --git a/app/src/main/java/to/bitkit/ext/Numbers.kt b/app/src/main/java/to/bitkit/ext/Numbers.kt index ffc3f53db..8fec7dafb 100644 --- a/app/src/main/java/to/bitkit/ext/Numbers.kt +++ b/app/src/main/java/to/bitkit/ext/Numbers.kt @@ -7,7 +7,11 @@ import java.time.Instant val ULong.millis: ULong get() = this * 1000u fun ULong.toActivityItemDate(): String { - return Instant.ofEpochSecond(this.toLong()).formatted(DatePattern.ACTIVITY_ITEM) + return Instant.ofEpochSecond(this.toLong()).formatted(DatePattern.ACTIVITY_DATE) +} + +fun ULong.toActivityItemTime(): String { + return Instant.ofEpochSecond(this.toLong()).formatted(DatePattern.ACTIVITY_TIME) } fun Number.formatWithDotSeparator(): String { diff --git a/app/src/main/java/to/bitkit/ui/components/ActivityIcon.kt b/app/src/main/java/to/bitkit/ui/components/ActivityIcon.kt new file mode 100644 index 000000000..0bfde056f --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/components/ActivityIcon.kt @@ -0,0 +1,231 @@ +package to.bitkit.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import to.bitkit.R +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors +import uniffi.bitkitcore.Activity +import uniffi.bitkitcore.LightningActivity +import uniffi.bitkitcore.OnchainActivity +import uniffi.bitkitcore.PaymentState +import uniffi.bitkitcore.PaymentType + +@Composable +fun ActivityIcon( + activity: Activity, + size: Dp = 32.dp, + modifier: Modifier = Modifier, +) { + val isLightning = activity is Activity.Lightning + val status: PaymentState? = when (activity) { + is Activity.Lightning -> activity.v1.status + is Activity.Onchain -> null + } + val txType: PaymentType = when (activity) { + is Activity.Lightning -> activity.v1.txType + is Activity.Onchain -> activity.v1.txType + } + val arrowIcon = painterResource(if (txType == PaymentType.SENT) R.drawable.ic_sent else R.drawable.ic_received) + + if (isLightning) { + when (status) { + PaymentState.FAILED -> { + CircularIcon( + icon = painterResource(R.drawable.ic_x), + iconColor = Colors.Purple, + backgroundColor = Colors.Purple16, + size = size, + modifier = modifier, + ) + } + + PaymentState.PENDING -> { + CircularIcon( + icon = painterResource(R.drawable.ic_hourglass_simple), + iconColor = Colors.Purple, + backgroundColor = Colors.Purple16, + size = size, + modifier = modifier, + ) + } + + else -> { + CircularIcon( + icon = arrowIcon, + iconColor = Colors.Purple, + backgroundColor = Colors.Purple16, + size = size, + modifier = modifier, + ) + } + } + } else { + val isTransfer = (activity as? Activity.Onchain)?.v1?.isTransfer == true + val onChainIcon = if (isTransfer) painterResource(R.drawable.ic_transfer) else arrowIcon + + CircularIcon( + icon = onChainIcon, + iconColor = Colors.Brand, + backgroundColor = Colors.Brand16, + size = size, + modifier = modifier, + ) + } +} + +@Composable +fun CircularIcon( + icon: Painter, + iconColor: Color, + backgroundColor: Color, + size: Dp = 32.dp, + modifier: Modifier = Modifier, +) { + Box( + contentAlignment = Alignment.Companion.Center, + modifier = modifier + .size(size) + .background(backgroundColor, CircleShape) + ) { + Icon( + painter = icon, + contentDescription = null, + tint = iconColor, + modifier = Modifier.size(size * 0.5f), + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + AppThemeSurface { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.padding(16.dp), + ) { + // Lightning Sent Succeeded + ActivityIcon( + activity = Activity.Lightning( + v1 = LightningActivity( + id = "test-lightning-1", + txType = PaymentType.SENT, + status = PaymentState.SUCCEEDED, + value = 50000uL, + fee = 1uL, + invoice = "lnbc...", + message = "", + timestamp = (System.currentTimeMillis() / 1000).toULong(), + preimage = null, + createdAt = null, + updatedAt = null, + ) + ) + ) + + // Lightning Received Failed + ActivityIcon( + activity = Activity.Lightning( + v1 = LightningActivity( + id = "test-lightning-2", + txType = PaymentType.RECEIVED, + status = PaymentState.FAILED, + value = 50000uL, + fee = 1uL, + invoice = "lnbc...", + message = "", + timestamp = (System.currentTimeMillis() / 1000).toULong(), + preimage = null, + createdAt = null, + updatedAt = null, + ) + ) + ) + + // Lightning Pending + ActivityIcon( + activity = Activity.Lightning( + v1 = LightningActivity( + id = "test-lightning-3", + txType = PaymentType.SENT, + status = PaymentState.PENDING, + value = 50000uL, + fee = 1uL, + invoice = "lnbc...", + message = "", + timestamp = (System.currentTimeMillis() / 1000).toULong(), + preimage = null, + createdAt = null, + updatedAt = null, + ) + ) + ) + + // Onchain Received + ActivityIcon( + activity = 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).toULong(), + isBoosted = false, + isTransfer = false, + doesExist = true, + confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(), + channelId = null, + transferTxId = null, + createdAt = null, + updatedAt = null, + ) + ) + ) + + // Onchain Transfer + ActivityIcon( + activity = Activity.Onchain( + v1 = OnchainActivity( + id = "test-onchain-2", + txType = PaymentType.SENT, + txId = "abc123", + value = 100000uL, + fee = 500uL, + feeRate = 8uL, + address = "bc1...", + confirmed = true, + timestamp = (System.currentTimeMillis() / 1000).toULong(), + isBoosted = false, + isTransfer = true, + doesExist = true, + confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(), + channelId = null, + transferTxId = "transferTxId", + createdAt = null, + updatedAt = null, + ) + ) + ) + } + } +} diff --git a/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt b/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt index 1b88d1b52..e2d2d113c 100644 --- a/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt +++ b/app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt @@ -10,8 +10,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import to.bitkit.models.BITCOIN_SYMBOL import to.bitkit.models.ConvertedAmount import to.bitkit.models.PrimaryDisplay import to.bitkit.ui.LocalCurrencies @@ -27,6 +29,21 @@ fun BalanceHeaderView( prefix: String? = null, showBitcoinSymbol: Boolean = true, ) { + val isPreview = LocalInspectionMode.current + if (isPreview) { + BalanceHeader( + modifier = modifier, + smallRowSymbol = "$", + smallRowText = "12.34", + largeRowPrefix = prefix, + largeRowText = "$sats", + largeRowSymbol = BITCOIN_SYMBOL, + showSymbol = showBitcoinSymbol, + onClick = {}, + ) + return + } + val currency = currencyViewModel ?: return val (rates, _, _, _, displayUnit, primaryDisplay) = LocalCurrencies.current val converted: ConvertedAmount? = if (rates.isNotEmpty()) currency.convert(sats = sats) else null @@ -37,7 +54,6 @@ fun BalanceHeaderView( if (primaryDisplay == PrimaryDisplay.BITCOIN) { BalanceHeader( modifier = modifier, - smallRowPrefix = prefix, smallRowSymbol = converted.symbol, smallRowText = converted.formatted, largeRowPrefix = prefix, @@ -49,7 +65,6 @@ fun BalanceHeaderView( } else { BalanceHeader( modifier = modifier, - smallRowPrefix = prefix, smallRowSymbol = btcComponents.symbol, smallRowText = btcComponents.value, largeRowPrefix = prefix, @@ -65,7 +80,6 @@ fun BalanceHeaderView( @Composable fun BalanceHeader( modifier: Modifier = Modifier, - smallRowPrefix: String? = null, smallRowSymbol: String? = null, smallRowText: String, largeRowPrefix: String? = null, @@ -80,7 +94,6 @@ fun BalanceHeader( modifier = modifier.clickableAlpha { onClick() } ) { SmallRow( - prefix = smallRowPrefix, symbol = smallRowSymbol, text = smallRowText ) @@ -120,17 +133,11 @@ fun LargeRow(prefix: String?, text: String, symbol: String, showSymbol: Boolean) } @Composable -private fun SmallRow(prefix: String?, symbol: String?, text: String) { +private fun SmallRow(symbol: String?, text: String) { Row( verticalAlignment = Alignment.Bottom, horizontalArrangement = Arrangement.spacedBy(4.dp), ) { - if (prefix != null) { - Caption13Up( - text = prefix, - color = Colors.White64, - ) - } if (symbol != null) { Caption13Up( text = symbol, @@ -149,12 +156,12 @@ private fun SmallRow(prefix: String?, symbol: String?, text: String) { private fun Preview() { AppThemeSurface { BalanceHeader( - smallRowPrefix = "$", + smallRowSymbol = "$", smallRowText = "27.36", - largeRowPrefix = "₿", + largeRowPrefix = "+", largeRowText = "136 825", - largeRowSymbol = "sats", - showSymbol = false, + largeRowSymbol = "₿", + showSymbol = true, modifier = Modifier.fillMaxWidth(), onClick = {} ) 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 b0c97a96a..609b23957 100644 --- a/app/src/main/java/to/bitkit/ui/components/Button.kt +++ b/app/src/main/java/to/bitkit/ui/components/Button.kt @@ -2,6 +2,7 @@ package to.bitkit.ui.components import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer @@ -20,6 +21,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -74,7 +76,9 @@ fun PrimaryButton( ) } else { if (icon != null) { - icon() + Box(modifier = if (enabled) Modifier else Modifier.alpha(0.5f)) { + icon() + } Spacer(modifier = Modifier.width(8.dp)) } Text( @@ -200,6 +204,13 @@ private fun PrimaryButtonPreview() { PrimaryButton( text = "Primary Disabled", onClick = {}, + icon = { + Icon( + imageVector = Icons.Filled.Favorite, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + }, enabled = false, ) PrimaryButton( diff --git a/app/src/main/java/to/bitkit/ui/components/Tag.kt b/app/src/main/java/to/bitkit/ui/components/Tag.kt index 317016605..821d7f518 100644 --- a/app/src/main/java/to/bitkit/ui/components/Tag.kt +++ b/app/src/main/java/to/bitkit/ui/components/Tag.kt @@ -1,14 +1,13 @@ package to.bitkit.ui.components import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -20,13 +19,15 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R +import to.bitkit.ui.shared.util.clickableAlpha +import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @Composable fun TagButton( text: String, - isSelected: Boolean, + isSelected: Boolean = false, displayIconClose: Boolean = false, onClick: () -> Unit, modifier: Modifier = Modifier, @@ -39,12 +40,8 @@ fun TagButton( horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = modifier .wrapContentWidth() - .border( - width = 1.dp, - color = borderColor, - shape = RoundedCornerShape(8.dp) - ) - .clickable { onClick() } + .border(width = 1.dp, color = borderColor, shape = AppShapes.small) + .clickableAlpha { onClick() } .padding(horizontal = 12.dp, vertical = 8.dp) ) { Text( @@ -57,7 +54,8 @@ fun TagButton( Icon( painter = painterResource(R.drawable.ic_x), contentDescription = null, - tint = Colors.White + tint = Colors.White64, + modifier = Modifier.size(16.dp) ) } } 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/ActivityItemScreen.kt index 274ed5fd4..6e5947261 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/ActivityItemScreen.kt @@ -1,17 +1,18 @@ package to.bitkit.ui.screens.wallets.activity +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +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.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Bolt -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.HourglassEmpty -import androidx.compose.material.icons.outlined.CheckCircle +import androidx.compose.foundation.layout.width import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.runtime.Composable @@ -20,22 +21,32 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.painter.Painter +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 to.bitkit.R import to.bitkit.ext.toActivityItemDate +import to.bitkit.ext.toActivityItemTime import to.bitkit.ui.Routes +import to.bitkit.ui.components.ActivityIcon import to.bitkit.ui.components.BalanceHeaderView import to.bitkit.ui.components.BodySSB -import to.bitkit.ui.components.Text13Up +import to.bitkit.ui.components.ButtonSize +import to.bitkit.ui.components.Caption13Up +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.components.TagButton +import to.bitkit.ui.components.Title import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.shared.util.DarkModePreview import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.viewmodels.ActivityListViewModel import uniffi.bitkitcore.Activity +import uniffi.bitkitcore.LightningActivity +import uniffi.bitkitcore.OnchainActivity import uniffi.bitkitcore.PaymentState import uniffi.bitkitcore.PaymentType @@ -55,125 +66,377 @@ fun ActivityItemScreen( } ?: return ScreenColumn { + // TODO update title based on txType AppTopBar("Activity Details", onBackClick = onBackClick) ActivityItemView(item) } } +@OptIn(ExperimentalLayoutApi::class) @Composable private fun ActivityItemView( item: Activity, ) { + val isLightning = item is Activity.Lightning + val accentColor = if (isLightning) Colors.Purple else Colors.Brand + val isSent = when (item) { + is Activity.Lightning -> item.v1.txType == PaymentType.SENT + is Activity.Onchain -> item.v1.txType == PaymentType.SENT + } + val amountPrefix = if (isSent) "-" else "+" + val timestamp = when (item) { + is Activity.Lightning -> item.v1.timestamp + is Activity.Onchain -> when (item.v1.confirmed) { + true -> item.v1.confirmTimestamp ?: item.v1.timestamp + else -> item.v1.timestamp + } + } + val value = when (item) { + is Activity.Lightning -> item.v1.value + is Activity.Onchain -> when { + isSent -> item.v1.value + item.v1.fee + else -> item.v1.value + } + } + val paymentValue = when (item) { + is Activity.Lightning -> item.v1.value + is Activity.Onchain -> item.v1.value + } + val fee = when (item) { + is Activity.Lightning -> item.v1.fee + is Activity.Onchain -> item.v1.fee + } + Column( - modifier = Modifier.padding(horizontal = 16.dp) + modifier = Modifier.padding(16.dp) ) { + // header section: amount + icon Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Bottom, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), ) { - val amountSats: ULong = when (item) { - is Activity.Lightning -> item.v1.value - is Activity.Onchain -> item.v1.value - } - val amountPrefix = when (item) { - is Activity.Lightning -> if (item.v1.txType == PaymentType.SENT) "-" else "+" - is Activity.Onchain -> if (item.v1.txType == PaymentType.SENT) "-" else "+" - } BalanceHeaderView( - sats = amountSats.toLong(), + sats = value.toLong(), prefix = amountPrefix, showBitcoinSymbol = false, - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f) ) - TransactionIcon(item = item) + ActivityIcon(activity = item, size = 48.dp) } Spacer(modifier = Modifier.height(16.dp)) StatusSection(item) - HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp)) + HorizontalDivider(modifier = Modifier.padding(top = 16.dp)) - Text13Up(text = stringResource(R.string.wallet__activity_date), color = Colors.White64) - Spacer(modifier = Modifier.height(8.dp)) + // Timestamp section: date and time + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + // Date column + Column(modifier = Modifier.weight(1f)) { + Caption13Up( + text = stringResource(R.string.wallet__activity_date), + color = Colors.White64, + modifier = Modifier.padding(top = 16.dp, bottom = 8.dp) + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(R.drawable.ic_calendar), + contentDescription = null, + tint = accentColor, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + BodySSB(text = timestamp.toActivityItemDate()) + } + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider() + } - val timestamp = when (item) { - is Activity.Lightning -> item.v1.timestamp - is Activity.Onchain -> item.v1.timestamp + // Time column + Column(modifier = Modifier.weight(1f)) { + Caption13Up( + text = stringResource(R.string.wallet__activity_time), + color = Colors.White64, + modifier = Modifier.padding(top = 16.dp, bottom = 8.dp) + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(R.drawable.ic_clock), + contentDescription = null, + tint = accentColor, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + BodySSB(text = timestamp.toActivityItemTime()) + } + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider() + } } - BodySSB(text = timestamp.toActivityItemDate()) - HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp)) + // Fee section for sent transactions + if (isSent) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + Column(modifier = Modifier.weight(1f)) { + Caption13Up( + text = stringResource(R.string.wallet__activity_payment), + color = Colors.White64, + modifier = Modifier.padding(top = 16.dp, bottom = 8.dp) + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(R.drawable.ic_user), + contentDescription = null, + tint = accentColor, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + BodySSB(text = "$paymentValue") + } + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider() + } - Spacer(modifier = Modifier.weight(1f)) + // Fee column if fee exists + if (fee != null) { + Column(modifier = Modifier.weight(1f)) { + Caption13Up( + text = stringResource(R.string.wallet__activity_fee), + color = Colors.White64, + modifier = Modifier.padding(top = 16.dp, bottom = 8.dp) + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(R.drawable.ic_speed_normal), + contentDescription = null, + tint = accentColor, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + BodySSB(text = fee.toString()) + } + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider() + } + } + } + } + + // 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 = {}, + ) + } + Spacer(modifier = Modifier.height(16.dp)) + HorizontalDivider() + } + + // Note section for Lightning payments with message + if (item is Activity.Lightning && item.v1.message.isNotEmpty()) { + Column(modifier = Modifier.fillMaxWidth()) { + Caption13Up( + text = stringResource(R.string.wallet__activity_invoice_note), + color = Colors.White64, + modifier = Modifier.padding(top = 16.dp, bottom = 8.dp) + ) + Column( + modifier = Modifier.fillMaxWidth() + ) { + ZigzagDivider() + Column( + modifier = Modifier + .fillMaxWidth() + .background(Colors.White10) + ) { + Title( + text = item.v1.message, + color = Colors.White, + modifier = Modifier.padding(24.dp), + ) + } + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Action buttons + // TODO add buttons action + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + PrimaryButton( + text = stringResource(R.string.wallet__activity_assign), + size = ButtonSize.Small, + onClick = { /* TODO: Implement assign functionality */ }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_user_plus), + contentDescription = null, + tint = accentColor, + modifier = Modifier.size(16.dp) + ) + }, + modifier = Modifier.weight(1f) + ) + PrimaryButton( + text = stringResource(R.string.wallet__activity_tag), + size = ButtonSize.Small, + onClick = { /* TODO: Implement tag functionality */ }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_tag), + contentDescription = null, + tint = accentColor, + modifier = Modifier.size(16.dp) + ) + }, + modifier = Modifier.weight(1f) + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.fillMaxWidth() + ) { + PrimaryButton( + text = stringResource(R.string.wallet__activity_boost), + size = ButtonSize.Small, + onClick = { /* TODO: Implement boost functionality */ }, + enabled = !isLightning, // TODO add logic to enable/disable boost for onchain activities + icon = { + Icon( + painter = painterResource(R.drawable.ic_timer_alt), + contentDescription = null, + tint = accentColor, + modifier = Modifier.size(16.dp) + ) + }, + modifier = Modifier.weight(1f) + ) + PrimaryButton( + text = stringResource(R.string.wallet__activity_explore), + size = ButtonSize.Small, + onClick = { /* TODO: Implement explore functionality */ }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_git_branch), + contentDescription = null, + tint = accentColor, + modifier = Modifier.size(16.dp) + ) + }, + modifier = Modifier.weight(1f) + ) + } + } } } @Composable private fun StatusSection(item: Activity) { Column(modifier = Modifier.fillMaxWidth()) { - Text13Up(text = stringResource(R.string.wallet__activity_status), color = Colors.White64) - Spacer(modifier = Modifier.height(8.dp)) - Row { + Caption13Up( + text = stringResource(R.string.wallet__activity_status), + color = Colors.White64, + modifier = Modifier.padding(bottom = 8.dp) + ) + Row(verticalAlignment = Alignment.CenterVertically) { when (item) { is Activity.Lightning -> { - LightningStatusView(status = item.v1.status) - } + when (item.v1.status) { + PaymentState.PENDING -> { + StatusIcon(painterResource(R.drawable.ic_hourglass_simple), Colors.Purple) + StatusText(stringResource(R.string.wallet__activity_pending), Colors.Purple) + } - is Activity.Onchain -> { - OnchainStatusView(confirmed = item.v1.confirmed) + PaymentState.SUCCEEDED -> { + StatusIcon(painterResource(R.drawable.ic_lightning_alt), Colors.Purple) + StatusText(stringResource(R.string.wallet__activity_successful), Colors.Purple) + } + + PaymentState.FAILED -> { + StatusIcon(painterResource(R.drawable.ic_x), Colors.Purple) + StatusText(stringResource(R.string.wallet__activity_failed), Colors.Purple) + } + } } - } - } - } -} -@Composable -private fun LightningStatusView(status: PaymentState?) { - when (status) { - PaymentState.PENDING -> { - StatusIcon(Icons.Default.HourglassEmpty, Colors.Purple) - StatusText(stringResource(R.string.wallet__activity_pending), Colors.Purple) - } + is Activity.Onchain -> { + // Default status is confirming + var statusIcon = painterResource(R.drawable.ic_hourglass_simple) + var statusColor = Colors.Brand + var statusText = stringResource(R.string.wallet__activity_confirming) - PaymentState.SUCCEEDED -> { - StatusIcon(Icons.Default.Bolt, Colors.Purple) - StatusText(stringResource(R.string.wallet__activity_successful), Colors.Purple) - } + // TODO: handle isTransfer - PaymentState.FAILED -> { - StatusIcon(Icons.Default.Close, Colors.Red) - StatusText(stringResource(R.string.wallet__activity_failed), Colors.Red) - } + if (item.v1.isBoosted) { + statusIcon = painterResource(R.drawable.ic_timer_alt) + statusColor = Colors.Yellow + statusText = stringResource(R.string.wallet__activity_boosting) + } - null -> Unit - } -} + if (item.v1.confirmed) { + statusIcon = painterResource(R.drawable.ic_check_circle) + statusColor = Colors.Green + statusText = stringResource(R.string.wallet__activity_confirmed) + } -@Composable -private fun OnchainStatusView(confirmed: Boolean?) { - when (confirmed) { - true -> { - StatusIcon(Icons.Outlined.CheckCircle, Colors.Green) - StatusText(stringResource(R.string.wallet__activity_confirmed), Colors.Green) - } + if (!item.v1.doesExist) { + statusIcon = painterResource(R.drawable.ic_x) + statusColor = Colors.Red + statusText = stringResource(R.string.wallet__activity_removed) + } - else -> { - StatusIcon(Icons.Default.HourglassEmpty, Colors.Brand) - StatusText(stringResource(R.string.wallet__activity_confirming), Colors.Brand) + StatusIcon(statusIcon, statusColor) + StatusText(statusText, statusColor) + } + } } } } @Composable private fun StatusIcon( - icon: ImageVector, + icon: Painter, tint: Color, - contentDescription: String? = null, ) { Icon( - imageVector = icon, - contentDescription = contentDescription, + painter = icon, + contentDescription = null, tint = tint, - modifier = Modifier.size(20.dp) + modifier = Modifier.size(16.dp) ) } @@ -189,10 +452,92 @@ private fun StatusText( ) } -@DarkModePreview @Composable -private fun PreviewActivityItemView() { +private fun ZigzagDivider() { + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(12.dp) + ) { + val zigzagWidth = 24.dp.toPx() + val amplitude = size.height + val width = size.width + val path = Path() + + path.moveTo(0f, 0f) + var x = 0f + while (x < width) { + path.lineTo(x + zigzagWidth / 2, amplitude) + path.lineTo((x + zigzagWidth).coerceAtMost(width), 0f) + x += zigzagWidth + } + path.lineTo(width, amplitude) + path.lineTo(0f, amplitude) + path.close() + + drawPath( + path = path, + color = Colors.White10, + ) + } +} + +@Preview +@Composable +private fun PreviewLightningSent() { AppThemeSurface { - ActivityItemView(item = testActivityItems[1]) + Column { + // Lightning example + 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, + ) + ) + ) + } + } +} + +@Preview +@Composable +private fun PreviewOnchain() { + AppThemeSurface { + Column { + // Onchain example + 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, + ) + ) + ) + } } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityRow.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityRow.kt index 9144a8aa3..40bdb59d2 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityRow.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityRow.kt @@ -1,45 +1,38 @@ package to.bitkit.ui.screens.wallets.activity -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDownward -import androidx.compose.material.icons.filled.ArrowUpward -import androidx.compose.material.icons.filled.Close -import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import to.bitkit.R -import to.bitkit.ext.toActivityItemDate -import to.bitkit.models.ConvertedAmount +import to.bitkit.ext.DatePattern +import to.bitkit.ext.formatted import to.bitkit.models.PrimaryDisplay import to.bitkit.ui.LocalCurrencies +import to.bitkit.ui.components.ActivityIcon import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.CaptionB import to.bitkit.ui.currencyViewModel -import to.bitkit.ui.shared.util.DarkModePreview import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import uniffi.bitkitcore.Activity import uniffi.bitkitcore.PaymentState import uniffi.bitkitcore.PaymentType +import java.time.Instant +import java.time.LocalDate +import java.time.ZoneId @Composable fun ActivityRow( @@ -50,6 +43,17 @@ fun ActivityRow( is Activity.Onchain -> item.v1.id is Activity.Lightning -> item.v1.id } + ActivityRowContent( + item = item, + onClick = { onClick(id) }, + ) +} + +@Composable +private fun ActivityRowContent( + item: Activity, + onClick: () -> Unit, +) { val status: PaymentState? = when (item) { is Activity.Lightning -> item.v1.status is Activity.Onchain -> null @@ -63,10 +67,8 @@ fun ActivityRow( is Activity.Lightning -> item.v1.txType is Activity.Onchain -> item.v1.txType } - val amountPrefix = when (item) { - is Activity.Lightning -> if (item.v1.txType == PaymentType.SENT) "-" else "+" - is Activity.Onchain -> if (item.v1.txType == PaymentType.SENT) "-" else "+" - } + val isSent = txType == PaymentType.SENT + val amountPrefix = if (isSent) "-" else "+" val confirmed: Boolean? = when (item) { is Activity.Lightning -> null is Activity.Onchain -> item.v1.confirmed @@ -75,43 +77,30 @@ fun ActivityRow( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() - .clickableAlpha { onClick(id) } + .clickableAlpha { onClick() } .padding(vertical = 16.dp) ) { - TransactionIcon(item) - Spacer(modifier = Modifier.width(12.dp)) - - val lightningStatus = when { - txType == PaymentType.SENT -> when (status) { - PaymentState.FAILED -> "Sending Failed" - PaymentState.PENDING -> "Sending..." - PaymentState.SUCCEEDED -> "Sent" - else -> "" - } - - else -> when (status) { - PaymentState.FAILED -> "Receive Failed" - PaymentState.PENDING -> "Receiving..." - PaymentState.SUCCEEDED -> "Received" - else -> "" - } - } - val onchainStatus = when { - txType == PaymentType.SENT -> if (confirmed == true) stringResource(R.string.wallet__activity_sent) else "Sending..." - else -> if (confirmed == true) stringResource(R.string.wallet__activity_received) else "Receiving..." - } - + ActivityIcon(activity = item, size = 32.dp) + Spacer(modifier = Modifier.width(16.dp)) Column( - verticalArrangement = Arrangement.spacedBy(2.dp) + verticalArrangement = Arrangement.spacedBy(4.dp), ) { - BodyMSB(text = if (isLightning) lightningStatus else onchainStatus) - // TODO timestamp: if today - only hour + TransactionStatusText( + txType = txType, + isLightning = isLightning, + status = status, + confirmed = confirmed, + ) val subtitleText = when (item) { - is Activity.Lightning -> { - item.v1.message.ifEmpty { timestamp.toActivityItemDate() } + is Activity.Lightning -> item.v1.message.ifEmpty { formattedTime(timestamp) } + is Activity.Onchain -> { + if (confirmed == true) { + formattedTime(timestamp) + } else { + // TODO: calculate confirmsIn text + stringResource(R.string.wallet__activity_confirms_in).replace("{feeRateDescription}", "± 1h") + } } - - else -> timestamp.toActivityItemDate() } CaptionB( text = subtitleText, @@ -119,118 +108,108 @@ fun ActivityRow( ) } Spacer(modifier = Modifier.weight(1f)) - val amount: ULong = when (item) { - is Activity.Lightning -> item.v1.value - is Activity.Onchain -> item.v1.value - } - amount.let { sats -> - val currency = currencyViewModel ?: return - val (rates, _, _, _, displayUnit, primaryDisplay) = LocalCurrencies.current - val converted: ConvertedAmount? = - if (rates.isNotEmpty()) currency.convert(sats = sats.toLong()) else null + AmountView( + item = item, + prefix = amountPrefix, + ) + } +} - converted?.let { converted -> - Column( - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - if (primaryDisplay == PrimaryDisplay.BITCOIN) { - val btcComponents = converted.bitcoinDisplay(displayUnit) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(1.dp) - ) { - BodyMSB( - text = amountPrefix, - color = Colors.White64, - ) - BodyMSB(text = btcComponents.value) - } - CaptionB( - text = "${converted.symbol} ${converted.formatted}", - color = Colors.White64, - ) - } else { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(1.dp) - ) { - BodyMSB( - text = amountPrefix, - color = Colors.White64, - ) - BodyMSB(text = "${converted.symbol} ${converted.formatted}") - } +@Composable +private fun TransactionStatusText( + txType: PaymentType, + isLightning: Boolean, + status: PaymentState?, + confirmed: Boolean?, +) { + when { + isLightning -> { + when (txType) { + PaymentType.SENT -> when (status) { + PaymentState.FAILED -> BodyMSB(text = stringResource(R.string.wallet__activity_failed)) + PaymentState.PENDING -> BodyMSB(text = stringResource(R.string.wallet__activity_pending)) + PaymentState.SUCCEEDED -> BodyMSB(text = stringResource(R.string.wallet__activity_sent)) + else -> {} + } - val btcComponents = converted.bitcoinDisplay(displayUnit) - CaptionB( - text = btcComponents.value, - color = Colors.White64, - ) - } + else -> when (status) { + PaymentState.FAILED -> BodyMSB(text = stringResource(R.string.wallet__activity_failed)) + PaymentState.PENDING -> BodyMSB(text = stringResource(R.string.wallet__activity_pending)) + PaymentState.SUCCEEDED -> BodyMSB(text = stringResource(R.string.wallet__activity_received)) + else -> {} } } } + + else -> { + when (txType) { + PaymentType.SENT -> BodyMSB(text = stringResource(R.string.wallet__activity_sent)) + else -> BodyMSB(text = stringResource(R.string.wallet__activity_received)) + } + } } } @Composable -fun TransactionIcon(item: Activity) { - val isLightning = item is Activity.Lightning - val status: PaymentState? = when (item) { - is Activity.Lightning -> item.v1.status - is Activity.Onchain -> null - } - val confirmed: Boolean? = when (item) { - is Activity.Lightning -> null - is Activity.Onchain -> item.v1.confirmed - } - val txType: PaymentType = when (item) { - is Activity.Lightning -> item.v1.txType - is Activity.Onchain -> item.v1.txType +private fun AmountView( + item: Activity, + prefix: String, +) { + val currency = currencyViewModel ?: return + val (_, _, _, _, displayUnit, primaryDisplay) = LocalCurrencies.current + val amount = when (item) { + is Activity.Lightning -> item.v1.value + is Activity.Onchain -> when (item.v1.txType) { + PaymentType.SENT -> item.v1.value + item.v1.fee + else -> item.v1.value + } } - - if (isLightning) { - if (status == PaymentState.FAILED) { - IconInCircle( - icon = Icons.Default.Close, - tint = Colors.Red, - ) - } else { - val icon = if (txType == PaymentType.SENT) Icons.Default.ArrowUpward else Icons.Default.ArrowDownward - IconInCircle( - icon = icon, - tint = Colors.Purple, - ) + currency.convert(sats = amount.toLong())?.let { converted -> + val btcComponents = converted.bitcoinDisplay(displayUnit) + Column( + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + if (primaryDisplay == PrimaryDisplay.BITCOIN) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(1.dp), + ) { + BodyMSB(text = prefix, color = Colors.White64) + BodyMSB(text = btcComponents.value) + } + CaptionB( + text = "${converted.symbol} ${converted.formatted}", + color = Colors.White64, + ) + } else { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(1.dp), + ) { + BodyMSB(text = prefix, color = Colors.White64) + BodyMSB(text = "${converted.symbol} ${converted.formatted}") + } + CaptionB( + text = btcComponents.value, + color = Colors.White64, + ) + } } - } else { - val icon = if (txType == PaymentType.SENT) Icons.Default.ArrowUpward else Icons.Default.ArrowDownward - IconInCircle( - icon = icon, - tint = if (confirmed == true) Colors.Brand else Colors.Brand50, - ) } } -@Composable -fun IconInCircle( - icon: ImageVector, - tint: Color, - modifier: Modifier = Modifier, - contentDescription: String? = null, -) { - Box( - contentAlignment = Alignment.Center, - modifier = modifier - .size(32.dp) - .background(color = tint.copy(alpha = 0.16f), shape = CircleShape) - ) { - Icon( - imageVector = icon, - contentDescription = contentDescription, - tint = tint, - modifier = Modifier.size(16.dp) - ) +private fun formattedTime(timestamp: ULong): String { + val instant = Instant.ofEpochSecond(timestamp.toLong()) + val dateTime = instant.atZone(ZoneId.systemDefault()) + val now = LocalDate.now() + + val isToday = dateTime.toLocalDate() == now + val isThisYear = dateTime.year == now.year + return when { + isToday -> instant.formatted(DatePattern.ACTIVITY_TIME) + isThisYear -> instant.formatted(DatePattern.ACTIVITY_ROW_DATE) + else -> instant.formatted(DatePattern.ACTIVITY_ROW_DATE_YEAR) } } @@ -238,10 +217,13 @@ private class ActivityItemsPreviewProvider : PreviewParameterProvider override val values: Sequence get() = testActivityItems.asSequence() } -@DarkModePreview +@Preview(showBackground = true) @Composable -private fun ActivityRowPreview(@PreviewParameter(ActivityItemsPreviewProvider::class) item: Activity) { +private fun Preview(@PreviewParameter(ActivityItemsPreviewProvider::class) item: Activity) { AppThemeSurface { - ActivityRow(item, onClick = { }) + ActivityRowContent( + item = item, + onClick = {}, + ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt index ba825b193..89b9a0ce4 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/AllActivityScreen.kt @@ -36,7 +36,13 @@ import uniffi.bitkitcore.LightningActivity import uniffi.bitkitcore.OnchainActivity import uniffi.bitkitcore.PaymentState import uniffi.bitkitcore.PaymentType +import java.time.Instant +import java.time.ZoneId +import java.time.temporal.ChronoUnit +import java.time.temporal.TemporalAdjusters +import java.time.temporal.WeekFields import java.util.Calendar +import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -166,50 +172,22 @@ fun ActivityList( // region utils private fun groupActivityItems(activityItems: List): List { - val date = Calendar.getInstance() + val now = Instant.now() + val zoneId = ZoneId.systemDefault() + val today = now.atZone(zoneId).truncatedTo(ChronoUnit.DAYS) - val beginningOfDay = date.apply { - set(Calendar.HOUR_OF_DAY, 0) - set(Calendar.MINUTE, 0) - set(Calendar.SECOND, 0) - set(Calendar.MILLISECOND, 0) - }.timeInMillis + val startOfDay = today.toInstant().epochSecond + val startOfYesterday = today.minusDays(1).toInstant().epochSecond + val startOfWeek = today.with(TemporalAdjusters.previousOrSame(WeekFields.of(Locale.getDefault()).firstDayOfWeek)).toInstant().epochSecond + val startOfMonth = today.withDayOfMonth(1).toInstant().epochSecond + val startOfYear = today.withDayOfYear(1).toInstant().epochSecond - val beginningOfYesterday = date.apply { - timeInMillis = beginningOfDay - add(Calendar.DATE, -1) - }.timeInMillis - - val beginningOfWeek = date.apply { - set(Calendar.DAY_OF_WEEK, firstDayOfWeek) - set(Calendar.HOUR_OF_DAY, 0) - set(Calendar.MINUTE, 0) - set(Calendar.SECOND, 0) - set(Calendar.MILLISECOND, 0) - }.timeInMillis - - val beginningOfMonth = date.apply { - set(Calendar.DAY_OF_MONTH, 1) - set(Calendar.HOUR_OF_DAY, 0) - set(Calendar.MINUTE, 0) - set(Calendar.SECOND, 0) - set(Calendar.MILLISECOND, 0) - }.timeInMillis - - val beginningOfYear = date.apply { - set(Calendar.DAY_OF_YEAR, 1) - set(Calendar.HOUR_OF_DAY, 0) - set(Calendar.MINUTE, 0) - set(Calendar.SECOND, 0) - set(Calendar.MILLISECOND, 0) - }.timeInMillis - - val today = mutableListOf() - val yesterday = mutableListOf() - val week = mutableListOf() - val month = mutableListOf() - val year = mutableListOf() - val earlier = mutableListOf() + val todayItems = mutableListOf() + val yesterdayItems = mutableListOf() + val weekItems = mutableListOf() + val monthItems = mutableListOf() + val yearItems = mutableListOf() + val earlierItems = mutableListOf() for (item in activityItems) { val timestamp = when (item) { @@ -217,42 +195,41 @@ private fun groupActivityItems(activityItems: List): List { is Activity.Onchain -> item.v1.timestamp.toLong() } when { - timestamp >= beginningOfDay -> today.add(item) - timestamp >= beginningOfYesterday -> yesterday.add(item) - timestamp >= beginningOfWeek -> week.add(item) - timestamp >= beginningOfMonth -> month.add(item) - timestamp >= beginningOfYear -> year.add(item) - else -> earlier.add(item) + timestamp >= startOfDay -> todayItems.add(item) + timestamp >= startOfYesterday -> yesterdayItems.add(item) + timestamp >= startOfWeek -> weekItems.add(item) + timestamp >= startOfMonth -> monthItems.add(item) + timestamp >= startOfYear -> yearItems.add(item) + else -> earlierItems.add(item) } } - val result = mutableListOf() - if (today.isNotEmpty()) { - result.add("TODAY") - result.addAll(today) - } - if (yesterday.isNotEmpty()) { - result.add("YESTERDAY") - result.addAll(yesterday) - } - if (week.isNotEmpty()) { - result.add("THIS WEEK") - result.addAll(week) - } - if (month.isNotEmpty()) { - result.add("THIS MONTH") - result.addAll(month) - } - if (year.isNotEmpty()) { - result.add("THIS YEAR") - result.addAll(year) - } - if (earlier.isNotEmpty()) { - result.add("EARLIER") - result.addAll(earlier) + return buildList { + if (todayItems.isNotEmpty()) { + add("TODAY") + addAll(todayItems) + } + if (yesterdayItems.isNotEmpty()) { + add("YESTERDAY") + addAll(yesterdayItems) + } + if (weekItems.isNotEmpty()) { + add("THIS WEEK") + addAll(weekItems) + } + if (monthItems.isNotEmpty()) { + add("THIS MONTH") + addAll(monthItems) + } + if (yearItems.isNotEmpty()) { + add("THIS YEAR") + addAll(yearItems) + } + if (earlierItems.isNotEmpty()) { + add("EARLIER") + addAll(earlierItems) + } } - - return result } // endregion @@ -311,6 +288,7 @@ private val today: Calendar = Calendar.getInstance() private val yesterday: Calendar = Calendar.getInstance().apply { add(Calendar.DATE, -1) } private val thisWeek: Calendar = Calendar.getInstance().apply { add(Calendar.DATE, -3) } private val thisMonth: Calendar = Calendar.getInstance().apply { add(Calendar.DATE, -10) } +private val lastYear: Calendar = Calendar.getInstance().apply { add(Calendar.YEAR, -1) } val testActivityItems: List = listOf( // Today @@ -322,7 +300,7 @@ val testActivityItems: List = listOf( value = 42_000_000_u, fee = 200_u, feeRate = 1_u, - address = "bcrt1", + address = "bc1", confirmed = true, timestamp = today.timeInMillis.toULong() / 1000u, isBoosted = false, @@ -340,10 +318,10 @@ val testActivityItems: List = listOf( LightningActivity( id = "2", txType = PaymentType.SENT, - status = PaymentState.SUCCEEDED, + status = PaymentState.PENDING, value = 30_000_u, fee = 15_u, - invoice = "lnbcrt2", + invoice = "lnbc2", message = "Custom message", timestamp = yesterday.timeInMillis.toULong() / 1000u, preimage = "preimage1", @@ -359,7 +337,7 @@ val testActivityItems: List = listOf( status = PaymentState.FAILED, value = 217_000_u, fee = 17_u, - invoice = "lnbcrt3", + invoice = "lnbc3", message = "", timestamp = thisWeek.timeInMillis.toULong() / 1000u, preimage = "preimage2", @@ -376,7 +354,7 @@ val testActivityItems: List = listOf( value = 950_000_u, fee = 110_u, feeRate = 1_u, - address = "bcrt1", + address = "bc1", confirmed = false, timestamp = thisMonth.timeInMillis.toULong() / 1000u, isBoosted = false, @@ -389,5 +367,21 @@ val testActivityItems: List = listOf( updatedAt = thisMonth.timeInMillis.toULong() / 1000u, ) ), + // Last Year + Activity.Lightning( + LightningActivity( + id = "5", + txType = PaymentType.SENT, + status = PaymentState.SUCCEEDED, + value = 200_000_u, + fee = 1_u, + invoice = "lnbc…", + message = "", + timestamp = (lastYear.timeInMillis.toULong() / 1000u), + preimage = null, + createdAt = (lastYear.timeInMillis.toULong() / 1000u), + updatedAt = (lastYear.timeInMillis.toULong() / 1000u), + ) + ), ) // endregion diff --git a/app/src/main/res/drawable/ic_bitcoin.xml b/app/src/main/res/drawable/ic_bitcoin.xml index ded7b3642..d32e9d4b9 100644 --- a/app/src/main/res/drawable/ic_bitcoin.xml +++ b/app/src/main/res/drawable/ic_bitcoin.xml @@ -5,6 +5,5 @@ android:viewportHeight="17"> + android:fillColor="#ffffff"/> diff --git a/app/src/main/res/drawable/ic_calendar.xml b/app/src/main/res/drawable/ic_calendar.xml new file mode 100644 index 000000000..9fe2df2d7 --- /dev/null +++ b/app/src/main/res/drawable/ic_calendar.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_check_circle.xml b/app/src/main/res/drawable/ic_check_circle.xml new file mode 100644 index 000000000..d24f4b8bb --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle.xml @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_git_branch.xml b/app/src/main/res/drawable/ic_git_branch.xml new file mode 100644 index 000000000..2ebce6167 --- /dev/null +++ b/app/src/main/res/drawable/ic_git_branch.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_hourglass_simple.xml b/app/src/main/res/drawable/ic_hourglass_simple.xml new file mode 100644 index 000000000..b51e6bcf9 --- /dev/null +++ b/app/src/main/res/drawable/ic_hourglass_simple.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_lightning_alt.xml b/app/src/main/res/drawable/ic_lightning_alt.xml index 0ab6f0338..baddca9c4 100644 --- a/app/src/main/res/drawable/ic_lightning_alt.xml +++ b/app/src/main/res/drawable/ic_lightning_alt.xml @@ -5,6 +5,5 @@ android:viewportHeight="16"> + android:fillColor="#ffffff"/> diff --git a/app/src/main/res/drawable/ic_received.xml b/app/src/main/res/drawable/ic_received.xml new file mode 100644 index 000000000..ed6a12788 --- /dev/null +++ b/app/src/main/res/drawable/ic_received.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_sent.xml b/app/src/main/res/drawable/ic_sent.xml new file mode 100644 index 000000000..45152a55d --- /dev/null +++ b/app/src/main/res/drawable/ic_sent.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_tag.xml b/app/src/main/res/drawable/ic_tag.xml index cee5db59f..9bf21df5b 100644 --- a/app/src/main/res/drawable/ic_tag.xml +++ b/app/src/main/res/drawable/ic_tag.xml @@ -6,13 +6,13 @@ + android:fillColor="#FFFFFF"/> diff --git a/app/src/main/res/drawable/ic_timer_alt.xml b/app/src/main/res/drawable/ic_timer_alt.xml new file mode 100644 index 000000000..fd04ab152 --- /dev/null +++ b/app/src/main/res/drawable/ic_timer_alt.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_user.xml b/app/src/main/res/drawable/ic_user.xml new file mode 100644 index 000000000..b87221444 --- /dev/null +++ b/app/src/main/res/drawable/ic_user.xml @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_user_plus.xml b/app/src/main/res/drawable/ic_user_plus.xml new file mode 100644 index 000000000..0e0161896 --- /dev/null +++ b/app/src/main/res/drawable/ic_user_plus.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_x.xml b/app/src/main/res/drawable/ic_x.xml index 8227aa433..40c28ca48 100644 --- a/app/src/main/res/drawable/ic_x.xml +++ b/app/src/main/res/drawable/ic_x.xml @@ -1,16 +1,15 @@ - - + android:width="32dp" + android:height="32dp" + android:autoMirrored="true" + android:viewportWidth="32" + android:viewportHeight="32"> + +