Skip to content

Commit 856b39b

Browse files
authored
Merge pull request #135 from synonymdev/feat/activity-detail-ui
feat: Activity detail and list item polish
2 parents b623227 + 78d9c87 commit 856b39b

22 files changed

+1141
-356
lines changed

app/src/main/java/to/bitkit/ext/DateTime.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ fun Instant.formatted(pattern: String = DatePattern.DATE_TIME): String {
1717
object DatePattern {
1818
const val DATE_TIME = "dd/MM/yyyy, HH:mm"
1919
const val INVOICE_EXPIRY = "MMM dd, h:mm a"
20-
const val ACTIVITY_ITEM = "MMMM d yyyy, HH:mm"
20+
const val ACTIVITY_DATE = "MMMM d"
21+
const val ACTIVITY_ROW_DATE = "MMMM d, HH:mm"
22+
const val ACTIVITY_ROW_DATE_YEAR = "MMMM d yyyy, HH:mm"
23+
const val ACTIVITY_TIME = "h:mm"
2124
const val LOG_FILE = "yyyy-MM-dd_HH-mm-ss"
2225
}

app/src/main/java/to/bitkit/ext/Numbers.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import java.time.Instant
77
val ULong.millis: ULong get() = this * 1000u
88

99
fun ULong.toActivityItemDate(): String {
10-
return Instant.ofEpochSecond(this.toLong()).formatted(DatePattern.ACTIVITY_ITEM)
10+
return Instant.ofEpochSecond(this.toLong()).formatted(DatePattern.ACTIVITY_DATE)
11+
}
12+
13+
fun ULong.toActivityItemTime(): String {
14+
return Instant.ofEpochSecond(this.toLong()).formatted(DatePattern.ACTIVITY_TIME)
1115
}
1216

1317
fun Number.formatWithDotSeparator(): String {
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
package to.bitkit.ui.components
2+
3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.Box
6+
import androidx.compose.foundation.layout.Row
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.foundation.layout.size
9+
import androidx.compose.foundation.shape.CircleShape
10+
import androidx.compose.material3.Icon
11+
import androidx.compose.runtime.Composable
12+
import androidx.compose.ui.Alignment
13+
import androidx.compose.ui.Modifier
14+
import androidx.compose.ui.graphics.Color
15+
import androidx.compose.ui.graphics.painter.Painter
16+
import androidx.compose.ui.res.painterResource
17+
import androidx.compose.ui.tooling.preview.Preview
18+
import androidx.compose.ui.unit.Dp
19+
import androidx.compose.ui.unit.dp
20+
import to.bitkit.R
21+
import to.bitkit.ui.theme.AppThemeSurface
22+
import to.bitkit.ui.theme.Colors
23+
import uniffi.bitkitcore.Activity
24+
import uniffi.bitkitcore.LightningActivity
25+
import uniffi.bitkitcore.OnchainActivity
26+
import uniffi.bitkitcore.PaymentState
27+
import uniffi.bitkitcore.PaymentType
28+
29+
@Composable
30+
fun ActivityIcon(
31+
activity: Activity,
32+
size: Dp = 32.dp,
33+
modifier: Modifier = Modifier,
34+
) {
35+
val isLightning = activity is Activity.Lightning
36+
val status: PaymentState? = when (activity) {
37+
is Activity.Lightning -> activity.v1.status
38+
is Activity.Onchain -> null
39+
}
40+
val txType: PaymentType = when (activity) {
41+
is Activity.Lightning -> activity.v1.txType
42+
is Activity.Onchain -> activity.v1.txType
43+
}
44+
val arrowIcon = painterResource(if (txType == PaymentType.SENT) R.drawable.ic_sent else R.drawable.ic_received)
45+
46+
if (isLightning) {
47+
when (status) {
48+
PaymentState.FAILED -> {
49+
CircularIcon(
50+
icon = painterResource(R.drawable.ic_x),
51+
iconColor = Colors.Purple,
52+
backgroundColor = Colors.Purple16,
53+
size = size,
54+
modifier = modifier,
55+
)
56+
}
57+
58+
PaymentState.PENDING -> {
59+
CircularIcon(
60+
icon = painterResource(R.drawable.ic_hourglass_simple),
61+
iconColor = Colors.Purple,
62+
backgroundColor = Colors.Purple16,
63+
size = size,
64+
modifier = modifier,
65+
)
66+
}
67+
68+
else -> {
69+
CircularIcon(
70+
icon = arrowIcon,
71+
iconColor = Colors.Purple,
72+
backgroundColor = Colors.Purple16,
73+
size = size,
74+
modifier = modifier,
75+
)
76+
}
77+
}
78+
} else {
79+
val isTransfer = (activity as? Activity.Onchain)?.v1?.isTransfer == true
80+
val onChainIcon = if (isTransfer) painterResource(R.drawable.ic_transfer) else arrowIcon
81+
82+
CircularIcon(
83+
icon = onChainIcon,
84+
iconColor = Colors.Brand,
85+
backgroundColor = Colors.Brand16,
86+
size = size,
87+
modifier = modifier,
88+
)
89+
}
90+
}
91+
92+
@Composable
93+
fun CircularIcon(
94+
icon: Painter,
95+
iconColor: Color,
96+
backgroundColor: Color,
97+
size: Dp = 32.dp,
98+
modifier: Modifier = Modifier,
99+
) {
100+
Box(
101+
contentAlignment = Alignment.Companion.Center,
102+
modifier = modifier
103+
.size(size)
104+
.background(backgroundColor, CircleShape)
105+
) {
106+
Icon(
107+
painter = icon,
108+
contentDescription = null,
109+
tint = iconColor,
110+
modifier = Modifier.size(size * 0.5f),
111+
)
112+
}
113+
}
114+
115+
@Preview(showBackground = true)
116+
@Composable
117+
private fun Preview() {
118+
AppThemeSurface {
119+
Row(
120+
horizontalArrangement = Arrangement.spacedBy(16.dp),
121+
modifier = Modifier.padding(16.dp),
122+
) {
123+
// Lightning Sent Succeeded
124+
ActivityIcon(
125+
activity = Activity.Lightning(
126+
v1 = LightningActivity(
127+
id = "test-lightning-1",
128+
txType = PaymentType.SENT,
129+
status = PaymentState.SUCCEEDED,
130+
value = 50000uL,
131+
fee = 1uL,
132+
invoice = "lnbc...",
133+
message = "",
134+
timestamp = (System.currentTimeMillis() / 1000).toULong(),
135+
preimage = null,
136+
createdAt = null,
137+
updatedAt = null,
138+
)
139+
)
140+
)
141+
142+
// Lightning Received Failed
143+
ActivityIcon(
144+
activity = Activity.Lightning(
145+
v1 = LightningActivity(
146+
id = "test-lightning-2",
147+
txType = PaymentType.RECEIVED,
148+
status = PaymentState.FAILED,
149+
value = 50000uL,
150+
fee = 1uL,
151+
invoice = "lnbc...",
152+
message = "",
153+
timestamp = (System.currentTimeMillis() / 1000).toULong(),
154+
preimage = null,
155+
createdAt = null,
156+
updatedAt = null,
157+
)
158+
)
159+
)
160+
161+
// Lightning Pending
162+
ActivityIcon(
163+
activity = Activity.Lightning(
164+
v1 = LightningActivity(
165+
id = "test-lightning-3",
166+
txType = PaymentType.SENT,
167+
status = PaymentState.PENDING,
168+
value = 50000uL,
169+
fee = 1uL,
170+
invoice = "lnbc...",
171+
message = "",
172+
timestamp = (System.currentTimeMillis() / 1000).toULong(),
173+
preimage = null,
174+
createdAt = null,
175+
updatedAt = null,
176+
)
177+
)
178+
)
179+
180+
// Onchain Received
181+
ActivityIcon(
182+
activity = Activity.Onchain(
183+
v1 = OnchainActivity(
184+
id = "test-onchain-1",
185+
txType = PaymentType.RECEIVED,
186+
txId = "abc123",
187+
value = 100000uL,
188+
fee = 500uL,
189+
feeRate = 8uL,
190+
address = "bc1...",
191+
confirmed = true,
192+
timestamp = (System.currentTimeMillis() / 1000).toULong(),
193+
isBoosted = false,
194+
isTransfer = false,
195+
doesExist = true,
196+
confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(),
197+
channelId = null,
198+
transferTxId = null,
199+
createdAt = null,
200+
updatedAt = null,
201+
)
202+
)
203+
)
204+
205+
// Onchain Transfer
206+
ActivityIcon(
207+
activity = Activity.Onchain(
208+
v1 = OnchainActivity(
209+
id = "test-onchain-2",
210+
txType = PaymentType.SENT,
211+
txId = "abc123",
212+
value = 100000uL,
213+
fee = 500uL,
214+
feeRate = 8uL,
215+
address = "bc1...",
216+
confirmed = true,
217+
timestamp = (System.currentTimeMillis() / 1000).toULong(),
218+
isBoosted = false,
219+
isTransfer = true,
220+
doesExist = true,
221+
confirmTimestamp = (System.currentTimeMillis() / 1000).toULong(),
222+
channelId = null,
223+
transferTxId = "transferTxId",
224+
createdAt = null,
225+
updatedAt = null,
226+
)
227+
)
228+
)
229+
}
230+
}
231+
}

app/src/main/java/to/bitkit/ui/components/BalanceHeaderView.kt

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import androidx.compose.foundation.layout.padding
1010
import androidx.compose.runtime.Composable
1111
import androidx.compose.ui.Alignment
1212
import androidx.compose.ui.Modifier
13+
import androidx.compose.ui.platform.LocalInspectionMode
1314
import androidx.compose.ui.tooling.preview.Preview
1415
import androidx.compose.ui.unit.dp
16+
import to.bitkit.models.BITCOIN_SYMBOL
1517
import to.bitkit.models.ConvertedAmount
1618
import to.bitkit.models.PrimaryDisplay
1719
import to.bitkit.ui.LocalCurrencies
@@ -27,6 +29,21 @@ fun BalanceHeaderView(
2729
prefix: String? = null,
2830
showBitcoinSymbol: Boolean = true,
2931
) {
32+
val isPreview = LocalInspectionMode.current
33+
if (isPreview) {
34+
BalanceHeader(
35+
modifier = modifier,
36+
smallRowSymbol = "$",
37+
smallRowText = "12.34",
38+
largeRowPrefix = prefix,
39+
largeRowText = "$sats",
40+
largeRowSymbol = BITCOIN_SYMBOL,
41+
showSymbol = showBitcoinSymbol,
42+
onClick = {},
43+
)
44+
return
45+
}
46+
3047
val currency = currencyViewModel ?: return
3148
val (rates, _, _, _, displayUnit, primaryDisplay) = LocalCurrencies.current
3249
val converted: ConvertedAmount? = if (rates.isNotEmpty()) currency.convert(sats = sats) else null
@@ -37,7 +54,6 @@ fun BalanceHeaderView(
3754
if (primaryDisplay == PrimaryDisplay.BITCOIN) {
3855
BalanceHeader(
3956
modifier = modifier,
40-
smallRowPrefix = prefix,
4157
smallRowSymbol = converted.symbol,
4258
smallRowText = converted.formatted,
4359
largeRowPrefix = prefix,
@@ -49,7 +65,6 @@ fun BalanceHeaderView(
4965
} else {
5066
BalanceHeader(
5167
modifier = modifier,
52-
smallRowPrefix = prefix,
5368
smallRowSymbol = btcComponents.symbol,
5469
smallRowText = btcComponents.value,
5570
largeRowPrefix = prefix,
@@ -65,7 +80,6 @@ fun BalanceHeaderView(
6580
@Composable
6681
fun BalanceHeader(
6782
modifier: Modifier = Modifier,
68-
smallRowPrefix: String? = null,
6983
smallRowSymbol: String? = null,
7084
smallRowText: String,
7185
largeRowPrefix: String? = null,
@@ -80,7 +94,6 @@ fun BalanceHeader(
8094
modifier = modifier.clickableAlpha { onClick() }
8195
) {
8296
SmallRow(
83-
prefix = smallRowPrefix,
8497
symbol = smallRowSymbol,
8598
text = smallRowText
8699
)
@@ -120,17 +133,11 @@ fun LargeRow(prefix: String?, text: String, symbol: String, showSymbol: Boolean)
120133
}
121134

122135
@Composable
123-
private fun SmallRow(prefix: String?, symbol: String?, text: String) {
136+
private fun SmallRow(symbol: String?, text: String) {
124137
Row(
125138
verticalAlignment = Alignment.Bottom,
126139
horizontalArrangement = Arrangement.spacedBy(4.dp),
127140
) {
128-
if (prefix != null) {
129-
Caption13Up(
130-
text = prefix,
131-
color = Colors.White64,
132-
)
133-
}
134141
if (symbol != null) {
135142
Caption13Up(
136143
text = symbol,
@@ -149,12 +156,12 @@ private fun SmallRow(prefix: String?, symbol: String?, text: String) {
149156
private fun Preview() {
150157
AppThemeSurface {
151158
BalanceHeader(
152-
smallRowPrefix = "$",
159+
smallRowSymbol = "$",
153160
smallRowText = "27.36",
154-
largeRowPrefix = "",
161+
largeRowPrefix = "+",
155162
largeRowText = "136 825",
156-
largeRowSymbol = "sats",
157-
showSymbol = false,
163+
largeRowSymbol = "",
164+
showSymbol = true,
158165
modifier = Modifier.fillMaxWidth(),
159166
onClick = {}
160167
)

app/src/main/java/to/bitkit/ui/components/Button.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package to.bitkit.ui.components
22

33
import androidx.compose.foundation.BorderStroke
44
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.Box
56
import androidx.compose.foundation.layout.Column
67
import androidx.compose.foundation.layout.PaddingValues
78
import androidx.compose.foundation.layout.Spacer
@@ -20,6 +21,7 @@ import androidx.compose.material3.Text
2021
import androidx.compose.material3.TextButton
2122
import androidx.compose.runtime.Composable
2223
import androidx.compose.ui.Modifier
24+
import androidx.compose.ui.draw.alpha
2325
import androidx.compose.ui.graphics.Color
2426
import androidx.compose.ui.text.style.TextOverflow
2527
import androidx.compose.ui.tooling.preview.Preview
@@ -74,7 +76,9 @@ fun PrimaryButton(
7476
)
7577
} else {
7678
if (icon != null) {
77-
icon()
79+
Box(modifier = if (enabled) Modifier else Modifier.alpha(0.5f)) {
80+
icon()
81+
}
7882
Spacer(modifier = Modifier.width(8.dp))
7983
}
8084
Text(
@@ -200,6 +204,13 @@ private fun PrimaryButtonPreview() {
200204
PrimaryButton(
201205
text = "Primary Disabled",
202206
onClick = {},
207+
icon = {
208+
Icon(
209+
imageVector = Icons.Filled.Favorite,
210+
contentDescription = null,
211+
modifier = Modifier.size(16.dp)
212+
)
213+
},
203214
enabled = false,
204215
)
205216
PrimaryButton(

0 commit comments

Comments
 (0)