@@ -16,7 +16,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
1616import androidx.compose.ui.tooling.preview.PreviewParameterProvider
1717import androidx.compose.ui.unit.dp
1818import to.bitkit.R
19- import to.bitkit.ext.toActivityItemDate
19+ import to.bitkit.ext.DatePattern
20+ import to.bitkit.ext.formatted
2021import to.bitkit.models.PrimaryDisplay
2122import to.bitkit.ui.LocalCurrencies
2223import to.bitkit.ui.components.ActivityIcon
@@ -29,6 +30,9 @@ import to.bitkit.ui.theme.Colors
2930import uniffi.bitkitcore.Activity
3031import uniffi.bitkitcore.PaymentState
3132import uniffi.bitkitcore.PaymentType
33+ import java.time.Instant
34+ import java.time.LocalDate
35+ import java.time.ZoneId
3236
3337@Composable
3438fun ActivityRow (
@@ -39,6 +43,17 @@ fun ActivityRow(
3943 is Activity .Onchain -> item.v1.id
4044 is Activity .Lightning -> item.v1.id
4145 }
46+ ActivityRowContent (
47+ item = item,
48+ onClick = { onClick(id) },
49+ )
50+ }
51+
52+ @Composable
53+ private fun ActivityRowContent (
54+ item : Activity ,
55+ onClick : () -> Unit ,
56+ ) {
4257 val status: PaymentState ? = when (item) {
4358 is Activity .Lightning -> item.v1.status
4459 is Activity .Onchain -> null
@@ -53,8 +68,7 @@ fun ActivityRow(
5368 is Activity .Onchain -> item.v1.txType
5469 }
5570 val isSent = txType == PaymentType .SENT
56-
57- val amountPrefix = if (txType == PaymentType .SENT ) " -" else " +"
71+ val amountPrefix = if (isSent) " -" else " +"
5872 val confirmed: Boolean? = when (item) {
5973 is Activity .Lightning -> null
6074 is Activity .Onchain -> item.v1.confirmed
@@ -63,108 +77,151 @@ fun ActivityRow(
6377 verticalAlignment = Alignment .CenterVertically ,
6478 modifier = Modifier
6579 .fillMaxWidth()
66- .clickableAlpha { onClick(id ) }
80+ .clickableAlpha { onClick() }
6781 .padding(vertical = 16 .dp)
6882 ) {
69- ActivityIcon (item)
70- Spacer (modifier = Modifier .width(12 .dp))
71-
72- // TODO: use localized status texts
73- val lightningStatus = when {
74- txType == PaymentType .SENT -> when (status) {
75- PaymentState .FAILED -> " Sending Failed"
76- PaymentState .PENDING -> " Sending..."
77- PaymentState .SUCCEEDED -> " Sent"
78- else -> " "
79- }
80-
81- else -> when (status) {
82- PaymentState .FAILED -> " Receive Failed"
83- PaymentState .PENDING -> " Receiving..."
84- PaymentState .SUCCEEDED -> " Received"
85- else -> " "
86- }
87- }
88- val onchainStatus = when {
89- txType == PaymentType .SENT -> if (confirmed == true ) stringResource(R .string.wallet__activity_sent) else " Sending..."
90- else -> if (confirmed == true ) stringResource(R .string.wallet__activity_received) else " Receiving..."
91- }
92-
83+ ActivityIcon (activity = item, size = 32 .dp)
84+ Spacer (modifier = Modifier .width(16 .dp))
9385 Column (
94- verticalArrangement = Arrangement .spacedBy(2 .dp)
86+ verticalArrangement = Arrangement .spacedBy(4 .dp),
9587 ) {
96- BodyMSB (text = if (isLightning) lightningStatus else onchainStatus)
97- // TODO timestamp: if today - only hour
88+ TransactionStatusText (
89+ txType = txType,
90+ isLightning = isLightning,
91+ status = status,
92+ confirmed = confirmed,
93+ )
9894 val subtitleText = when (item) {
99- is Activity .Lightning -> {
100- item.v1.message.ifEmpty { timestamp.toActivityItemDate() }
95+ is Activity .Lightning -> item.v1.message.ifEmpty { formattedTime(timestamp) }
96+ is Activity .Onchain -> {
97+ if (confirmed == true ) {
98+ formattedTime(timestamp)
99+ } else {
100+ // TODO: calculate confirmsIn text
101+ stringResource(R .string.wallet__activity_confirms_in).replace(" {feeRateDescription}" , " ???" )
102+ }
101103 }
102-
103- else -> timestamp.toActivityItemDate()
104104 }
105105 CaptionB (
106106 text = subtitleText,
107107 color = Colors .White64 ,
108108 )
109109 }
110110 Spacer (modifier = Modifier .weight(1f ))
111- val amount = when (item) {
112- is Activity .Lightning -> item.v1.value
113- is Activity .Onchain -> when {
114- isSent -> item.v1.value + item.v1.fee
115- else -> item.v1.value
111+ AmountView (
112+ item = item,
113+ prefix = amountPrefix,
114+ )
115+ }
116+ }
117+
118+ @Composable
119+ private fun TransactionStatusText (
120+ txType : PaymentType ,
121+ isLightning : Boolean ,
122+ status : PaymentState ? ,
123+ confirmed : Boolean? ,
124+ ) {
125+ when {
126+ isLightning -> {
127+ when (txType) {
128+ PaymentType .SENT -> when (status) {
129+ PaymentState .FAILED -> BodyMSB (text = stringResource(R .string.wallet__activity_failed))
130+ PaymentState .PENDING -> BodyMSB (text = stringResource(R .string.wallet__activity_pending))
131+ PaymentState .SUCCEEDED -> BodyMSB (text = stringResource(R .string.wallet__activity_sent))
132+ else -> {}
133+ }
134+
135+ else -> when (status) {
136+ PaymentState .FAILED -> BodyMSB (text = stringResource(R .string.wallet__activity_failed))
137+ PaymentState .PENDING -> BodyMSB (text = stringResource(R .string.wallet__activity_pending))
138+ PaymentState .SUCCEEDED -> BodyMSB (text = stringResource(R .string.wallet__activity_received))
139+ else -> {}
140+ }
141+ }
142+ }
143+
144+ else -> {
145+ when (txType) {
146+ PaymentType .SENT -> BodyMSB (text = stringResource(R .string.wallet__activity_sent))
147+ else -> BodyMSB (text = stringResource(R .string.wallet__activity_received))
116148 }
117149 }
118- amount.let { sats ->
119- val currency = currencyViewModel ? : return
120- val (_, _, _, _, displayUnit, primaryDisplay) = LocalCurrencies .current
150+ }
151+ }
121152
122- currency.convert(sats = sats.toLong())?.let { converted ->
123- Column (
124- horizontalAlignment = Alignment .End ,
125- verticalArrangement = Arrangement .spacedBy(2 .dp),
153+ @Composable
154+ private fun AmountView (
155+ item : Activity ,
156+ prefix : String ,
157+ ) {
158+ val currency = currencyViewModel ? : return
159+ val (_, _, _, _, displayUnit, primaryDisplay) = LocalCurrencies .current
160+ val amount = when (item) {
161+ is Activity .Lightning -> item.v1.value
162+ is Activity .Onchain -> when (item.v1.txType) {
163+ PaymentType .SENT -> item.v1.value + item.v1.fee
164+ else -> item.v1.value
165+ }
166+ }
167+ currency.convert(sats = amount.toLong())?.let { converted ->
168+ val btcComponents = converted.bitcoinDisplay(displayUnit)
169+ Column (
170+ horizontalAlignment = Alignment .End ,
171+ verticalArrangement = Arrangement .spacedBy(2 .dp),
172+ ) {
173+ if (primaryDisplay == PrimaryDisplay .BITCOIN ) {
174+ Row (
175+ verticalAlignment = Alignment .CenterVertically ,
176+ horizontalArrangement = Arrangement .spacedBy(1 .dp),
126177 ) {
127- val btcComponents = converted.bitcoinDisplay(displayUnit)
128- if (primaryDisplay == PrimaryDisplay .BITCOIN ) {
129- Row (
130- verticalAlignment = Alignment .CenterVertically ,
131- horizontalArrangement = Arrangement .spacedBy(1 .dp)
132- ) {
133- BodyMSB (text = amountPrefix, color = Colors .White64 )
134- BodyMSB (text = btcComponents.value)
135- }
136- CaptionB (
137- text = " ${converted.symbol} ${converted.formatted} " ,
138- color = Colors .White64 ,
139- )
140- } else {
141- Row (
142- verticalAlignment = Alignment .CenterVertically ,
143- horizontalArrangement = Arrangement .spacedBy(1 .dp)
144- ) {
145- BodyMSB (text = amountPrefix, color = Colors .White64 )
146- BodyMSB (text = " ${converted.symbol} ${converted.formatted} " )
147- }
148- CaptionB (
149- text = btcComponents.value,
150- color = Colors .White64 ,
151- )
152- }
178+ BodyMSB (text = prefix, color = Colors .White64 )
179+ BodyMSB (text = btcComponents.value)
153180 }
181+ CaptionB (
182+ text = " ${converted.symbol} ${converted.formatted} " ,
183+ color = Colors .White64 ,
184+ )
185+ } else {
186+ Row (
187+ verticalAlignment = Alignment .CenterVertically ,
188+ horizontalArrangement = Arrangement .spacedBy(1 .dp),
189+ ) {
190+ BodyMSB (text = prefix, color = Colors .White64 )
191+ BodyMSB (text = " ${converted.symbol} ${converted.formatted} " )
192+ }
193+ CaptionB (
194+ text = btcComponents.value,
195+ color = Colors .White64 ,
196+ )
154197 }
155198 }
156199 }
157200}
158201
202+ private fun formattedTime (timestamp : ULong ): String {
203+ val instant = Instant .ofEpochSecond(timestamp.toLong())
204+ val dateTime = instant.atZone(ZoneId .systemDefault())
205+ val now = LocalDate .now()
206+
207+ val isToday = dateTime.toLocalDate() == now
208+ val isThisYear = dateTime.year == now.year
209+ return when {
210+ isToday -> instant.formatted(DatePattern .ACTIVITY_TIME )
211+ isThisYear -> instant.formatted(DatePattern .ACTIVITY_ROW_DATE )
212+ else -> instant.formatted(DatePattern .ACTIVITY_ROW_DATE_YEAR )
213+ }
214+ }
215+
159216private class ActivityItemsPreviewProvider : PreviewParameterProvider <Activity > {
160217 override val values: Sequence <Activity > get() = testActivityItems.asSequence()
161218}
162219
163220@Preview(showBackground = true )
164221@Composable
165- private fun ActivityRowPreview (@PreviewParameter(ActivityItemsPreviewProvider ::class ) item : Activity ) {
222+ private fun Preview (@PreviewParameter(ActivityItemsPreviewProvider ::class ) item : Activity ) {
166223 AppThemeSurface {
167- ActivityRow (
224+ ActivityRowContent (
168225 item = item,
169226 onClick = {},
170227 )
0 commit comments