Skip to content

Commit 73c8d8f

Browse files
authored
Merge branch 'master' into fix/migrate-unsupported-addresses-activities
2 parents ee7c812 + f78d819 commit 73c8d8f

61 files changed

Lines changed: 1642 additions & 1401 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ class LightningNodeService : Service() {
100100
}
101101

102102
private fun createNotification(
103-
contentText: String = getString(R.string.notification_running_in_background),
103+
contentText: String = getString(R.string.notification__service__body),
104104
): Notification {
105105
val notificationIntent = Intent(this, MainActivity::class.java).apply {
106106
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
@@ -120,7 +120,7 @@ class LightningNodeService : Service() {
120120
.setContentIntent(pendingIntent)
121121
.addAction(
122122
R.drawable.ic_x,
123-
getString(R.string.notification_stop_app),
123+
getString(R.string.notification__service__stop),
124124
stopPendingIntent
125125
)
126126
.build()

app/src/main/java/to/bitkit/async/ServiceQueue.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ enum class ServiceQueue {
2626
): T {
2727
return runBlocking(coroutineContext) {
2828
try {
29-
measured(functionName) {
29+
measured(label = functionName, context = TAG) {
3030
block()
3131
}
3232
} catch (e: Exception) {
@@ -43,7 +43,7 @@ enum class ServiceQueue {
4343
): T {
4444
return withContext(coroutineContext) {
4545
try {
46-
measured(functionName) {
46+
measured(label = functionName, context = TAG) {
4747
block()
4848
}
4949
} catch (e: Exception) {
@@ -52,6 +52,10 @@ enum class ServiceQueue {
5252
}
5353
}
5454
}
55+
56+
companion object {
57+
private const val TAG = "ServiceQueue"
58+
}
5559
}
5660

5761
fun newSingleThreadDispatcher(id: String): ExecutorCoroutineDispatcher {

app/src/main/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandler.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,11 @@ class NotifyPaymentReceivedHandler @Inject constructor(
9696

9797
private suspend fun buildNotificationContent(sats: Long): NotificationDetails {
9898
val settings = settingsStore.data.first()
99-
val title = context.getString(R.string.notification_received_title)
99+
val title = context.getString(R.string.notification__received__title)
100100
val body = if (settings.showNotificationDetails) {
101101
formatNotificationAmount(sats, settings)
102102
} else {
103-
context.getString(R.string.notification_received_body_hidden)
103+
context.getString(R.string.notification__received__body_hidden)
104104
}
105105
return NotificationDetails(title, body)
106106
}
@@ -117,7 +117,7 @@ class NotifyPaymentReceivedHandler @Inject constructor(
117117
}
118118
} ?: "$BITCOIN_SYMBOL ${sats.formatToModernDisplay()}"
119119

120-
return context.getString(R.string.notification_received_body_amount, amountText)
120+
return context.getString(R.string.notification__received__body_amount, amountText)
121121
}
122122

123123
companion object {

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,18 +60,20 @@ fun Long.toDateUTC(): String {
6060
return dateTime.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"))
6161
}
6262

63-
fun Long.toLocalizedTimestamp(): String {
64-
val uLocale = ULocale.forLocale(Locale.US)
65-
val formatter = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT, uLocale)
66-
?: return SimpleDateFormat("MMMM d, yyyy 'at' h:mm a", Locale.US).format(Date(this))
67-
return formatter.format(Date(this))
63+
fun Long.toLocalizedTimestamp(locale: Locale = Locale.US): String {
64+
val date = Date(this)
65+
val formatter = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT, ULocale.forLocale(locale))
66+
?: return SimpleDateFormat("MMMM d, yyyy 'at' h:mm a", locale).format(date)
67+
return formatter.format(date)
6868
}
6969

7070
@Suppress("LongMethod")
7171
@OptIn(ExperimentalTime::class)
7272
fun Long.toRelativeTimeString(
7373
locale: Locale = Locale.getDefault(),
7474
clock: Clock = Clock.System,
75+
style: RelativeDateTimeFormatter.Style = RelativeDateTimeFormatter.Style.LONG,
76+
capitalizationContext: DisplayContext = DisplayContext.CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE,
7577
): String {
7678
val now = nowMillis(clock)
7779
val diffMillis = now - this
@@ -82,9 +84,9 @@ fun Long.toRelativeTimeString(
8284
val formatter = RelativeDateTimeFormatter.getInstance(
8385
uLocale,
8486
numberFormat,
85-
RelativeDateTimeFormatter.Style.LONG,
86-
DisplayContext.CAPITALIZATION_FOR_MIDDLE_OF_SENTENCE,
87-
) ?: return toLocalizedTimestamp()
87+
style,
88+
capitalizationContext,
89+
) ?: return toLocalizedTimestamp(locale)
8890

8991
val seconds = diffMillis / Factor.MILLIS_TO_SECONDS
9092
val minutes = seconds / Factor.SECONDS_TO_MINUTES

app/src/main/java/to/bitkit/fcm/WakeNodeWorker.kt

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ class WakeNodeWorker @AssistedInject constructor(
7676
Logger.warn("Notification type is null, proceeding with node wake", context = TAG)
7777
}
7878

79-
try {
80-
measured(TAG) {
79+
return runCatching {
80+
measured(label = "doWork", context = TAG) {
8181
lightningRepo.start(
8282
walletIndex = 0,
8383
timeout = timeout,
@@ -92,31 +92,35 @@ class WakeNodeWorker @AssistedInject constructor(
9292
Logger.error("Missing orderId", context = TAG)
9393
} else {
9494
Logger.info("Open channel request for order $orderId", context = TAG)
95-
blocktankRepo.openChannel(orderId).onFailure { e ->
96-
Logger.error("Failed to open channel", e, context = TAG)
95+
blocktankRepo.openChannel(orderId).onFailure {
96+
Logger.error("Failed to open channel", it, context = TAG)
9797
bestAttemptContent = NotificationDetails(
98-
title = appContext.getString(R.string.notification_channel_open_failed_title),
99-
body = e.message ?: appContext.getString(R.string.notification_unknown_error),
98+
title = appContext.getString(R.string.notification__channel_open_failed_title),
99+
body = it.message ?: appContext.getString(R.string.common__error_body),
100100
)
101101
deliver()
102102
}
103103
}
104104
}
105105
}
106-
withTimeout(timeout) { deliverSignal.await() } // Stops node on timeout & avoids notification replay by OS
107-
return Result.success()
108-
} catch (e: Exception) {
109-
val reason = e.message ?: appContext.getString(R.string.notification_unknown_error)
106+
// Stops node on timeout & avoids notification replay by OS
107+
withTimeout(timeout) { deliverSignal.await() }
108+
}
109+
.fold(
110+
onSuccess = { Result.success() },
111+
onFailure = { e ->
112+
val reason = e.message ?: appContext.getString(R.string.common__error_body)
110113

111-
bestAttemptContent = NotificationDetails(
112-
title = appContext.getString(R.string.notification_lightning_error_title),
113-
body = reason,
114-
)
115-
Logger.error("Lightning error", e, context = TAG)
116-
deliver()
114+
bestAttemptContent = NotificationDetails(
115+
title = appContext.getString(R.string.notification__lightning_error_title),
116+
body = reason,
117+
)
118+
Logger.error("Lightning error", e, context = TAG)
119+
deliver()
117120

118-
return Result.failure(workDataOf("Reason" to reason))
119-
}
121+
Result.failure(workDataOf("Reason" to reason))
122+
}
123+
)
120124
}
121125

122126
/**
@@ -125,14 +129,14 @@ class WakeNodeWorker @AssistedInject constructor(
125129
*/
126130
private suspend fun handleLdkEvent(event: Event) {
127131
val showDetails = settingsStore.data.first().showNotificationDetails
128-
val hiddenBody = appContext.getString(R.string.notification_received_body_hidden)
132+
val hiddenBody = appContext.getString(R.string.notification__received__body_hidden)
129133
when (event) {
130134
is Event.PaymentReceived -> onPaymentReceived(event, showDetails, hiddenBody)
131135

132136
is Event.ChannelPending -> {
133137
bestAttemptContent = NotificationDetails(
134-
title = appContext.getString(R.string.notification_channel_opened_title),
135-
body = appContext.getString(R.string.notification_channel_pending_body),
138+
title = appContext.getString(R.string.notification__channel_opened_title),
139+
body = appContext.getString(R.string.notification__channel_pending_body),
136140
)
137141
// Don't deliver, give a chance for channelReady event to update the content if it's a turbo channel
138142
}
@@ -142,7 +146,7 @@ class WakeNodeWorker @AssistedInject constructor(
142146

143147
is Event.PaymentFailed -> {
144148
bestAttemptContent = NotificationDetails(
145-
title = appContext.getString(R.string.notification_payment_failed_title),
149+
title = appContext.getString(R.string.notification__payment_failed_title),
146150
body = "${event.reason}",
147151
)
148152

@@ -158,18 +162,18 @@ class WakeNodeWorker @AssistedInject constructor(
158162
private suspend fun onChannelClosed(event: Event.ChannelClosed) {
159163
bestAttemptContent = when (notificationType) {
160164
mutualClose -> NotificationDetails(
161-
title = appContext.getString(R.string.notification_channel_closed_title),
162-
body = appContext.getString(R.string.notification_channel_closed_mutual_body),
165+
title = appContext.getString(R.string.notification__channel_closed__title),
166+
body = appContext.getString(R.string.notification__channel_closed__mutual_body),
163167
)
164168

165169
orderPaymentConfirmed -> NotificationDetails(
166-
title = appContext.getString(R.string.notification_channel_open_bg_failed_title),
167-
body = appContext.getString(R.string.notification_please_try_again_body),
170+
title = appContext.getString(R.string.notification__channel_open_bg_failed_title),
171+
body = appContext.getString(R.string.notification__please_try_again_body),
168172
)
169173

170174
else -> NotificationDetails(
171-
title = appContext.getString(R.string.notification_channel_closed_title),
172-
body = appContext.getString(R.string.notification_channel_closed_reason_body, event.reason),
175+
title = appContext.getString(R.string.notification__channel_closed__title),
176+
body = appContext.getString(R.string.notification__channel_closed__reason_body, event.reason),
173177
)
174178
}
175179

@@ -193,7 +197,7 @@ class WakeNodeWorker @AssistedInject constructor(
193197
)
194198
val content = if (showDetails) "$BITCOIN_SYMBOL $sats" else hiddenBody
195199
bestAttemptContent = NotificationDetails(
196-
title = appContext.getString(R.string.notification_received_title),
200+
title = appContext.getString(R.string.notification__received__title),
197201
body = content,
198202
)
199203
if (notificationType == incomingHtlc) {
@@ -206,10 +210,10 @@ class WakeNodeWorker @AssistedInject constructor(
206210
showDetails: Boolean,
207211
hiddenBody: String,
208212
) {
209-
val viaNewChannel = appContext.getString(R.string.notification_via_new_channel_body)
213+
val viaNewChannel = appContext.getString(R.string.notification__received__body_channel)
210214
if (notificationType == cjitPaymentArrived) {
211215
bestAttemptContent = NotificationDetails(
212-
title = appContext.getString(R.string.notification_received_title),
216+
title = appContext.getString(R.string.notification__received__title),
213217
body = viaNewChannel,
214218
)
215219

@@ -235,8 +239,8 @@ class WakeNodeWorker @AssistedInject constructor(
235239
}
236240
} else if (notificationType == orderPaymentConfirmed) {
237241
bestAttemptContent = NotificationDetails(
238-
title = appContext.getString(R.string.notification_channel_opened_title),
239-
body = appContext.getString(R.string.notification_channel_ready_body),
242+
title = appContext.getString(R.string.notification__channel_opened_title),
243+
body = appContext.getString(R.string.notification__channel_ready_body),
240244
)
241245
}
242246
deliver()

app/src/main/java/to/bitkit/models/ActivityBannerType.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ enum class ActivityBannerType(
1414
SPENDING(
1515
color = Colors.Purple,
1616
icon = R.drawable.ic_transfer,
17-
title = R.string.activity_banner__transfer_in_progress
17+
title = R.string.lightning__transfer_in_progress
1818
),
1919
SAVINGS(
2020
color = Colors.Brand,
2121
icon = R.drawable.ic_transfer,
22-
title = R.string.activity_banner__transfer_in_progress
22+
title = R.string.lightning__transfer_in_progress
2323
)
2424
}

app/src/main/java/to/bitkit/models/NodeLifecycleState.kt

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package to.bitkit.models
22

3+
import android.content.Context
4+
import to.bitkit.R
5+
36
sealed class NodeLifecycleState {
47
data object Stopped : NodeLifecycleState()
58
data object Starting : NodeLifecycleState()
@@ -14,16 +17,14 @@ sealed class NodeLifecycleState {
1417
fun isRunning() = this is Running
1518
fun canRun() = this.isRunningOrStarting() || this is Initializing
1619

17-
// TODO add missing localized texts
18-
val uiText: String
19-
get() = when (this) {
20-
is Stopped -> "Stopped"
21-
is Starting -> "Starting"
22-
is Running -> "Running"
23-
is Stopping -> "Stopping"
24-
is ErrorStarting -> "Error starting: ${cause.message}"
25-
is Initializing -> "Setting up wallet..."
26-
}
20+
fun uiText(context: Context): String = when (this) {
21+
is Stopped -> context.getString(R.string.other__node_stopped)
22+
is Starting -> context.getString(R.string.other__node_starting)
23+
is Running -> context.getString(R.string.other__node_running)
24+
is Stopping -> context.getString(R.string.other__node_stopping)
25+
is ErrorStarting -> context.getString(R.string.other__node_error_starting, cause.message ?: "")
26+
is Initializing -> context.getString(R.string.other__node_initializing)
27+
}
2728

2829
fun asHealth() = when (this) {
2930
Running -> HealthState.READY

app/src/main/java/to/bitkit/models/widget/ArticleModel.kt

Lines changed: 18 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,71 +2,53 @@ package to.bitkit.models.widget
22

33
import kotlinx.serialization.Serializable
44
import to.bitkit.data.dto.ArticleDTO
5+
import to.bitkit.ext.toRelativeTimeString
56
import to.bitkit.utils.Logger
67
import java.time.OffsetDateTime
78
import java.time.format.DateTimeFormatter
89
import java.time.format.DateTimeParseException
9-
import java.time.temporal.ChronoUnit
1010
import java.util.Locale
11+
import kotlin.time.ExperimentalTime
1112

1213
@Serializable
1314
data class ArticleModel(
1415
val title: String,
1516
val timeAgo: String,
1617
val link: String,
17-
val publisher: String
18+
val publisher: String,
1819
)
1920

2021
fun ArticleDTO.toArticleModel() = ArticleModel(
2122
title = this.title,
2223
timeAgo = timeAgo(this.publishedDate),
2324
link = this.link,
24-
publisher = this.publisher.title
25+
publisher = this.publisher.title,
2526
)
2627

27-
/**
28-
* Converts a date string to a human-readable time ago format
29-
* @param dateString Date string in format "EEE, dd MMM yyyy HH:mm:ss Z"
30-
* @return Human-readable time difference (e.g. "5 hours ago")
31-
*/
28+
private const val TAG = "ArticleModel"
29+
30+
@OptIn(ExperimentalTime::class)
3231
private fun timeAgo(dateString: String): String {
33-
return try {
32+
return runCatching {
3433
val formatters = listOf(
35-
DateTimeFormatter.RFC_1123_DATE_TIME, // Handles "EEE, dd MMM yyyy HH:mm:ss zzz" (like GMT)
36-
DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH) // Handles "+0000"
34+
DateTimeFormatter.RFC_1123_DATE_TIME,
35+
DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH)
3736
)
3837

3938
var parsedDateTime: OffsetDateTime? = null
4039
for (formatter in formatters) {
4140
try {
4241
parsedDateTime = OffsetDateTime.parse(dateString, formatter)
43-
break // Successfully parsed, stop trying other formatters
44-
} catch (e: DateTimeParseException) {
45-
// Continue to the next formatter if this one fails
42+
break
43+
} catch (_: DateTimeParseException) {
44+
// Continue to the next formatter
4645
}
4746
}
4847

49-
if (parsedDateTime == null) {
50-
Logger.debug("Failed to parse date: Unparseable date: $dateString")
51-
return ""
52-
}
53-
54-
val now = OffsetDateTime.now()
48+
requireNotNull(parsedDateTime) { "Unparseable date: '$dateString'" }
5549

56-
val diffMinutes = ChronoUnit.MINUTES.between(parsedDateTime, now)
57-
val diffHours = ChronoUnit.HOURS.between(parsedDateTime, now)
58-
val diffDays = ChronoUnit.DAYS.between(parsedDateTime, now)
59-
val diffMonths = ChronoUnit.MONTHS.between(parsedDateTime, now)
60-
61-
return when {
62-
diffMinutes < 1 -> "just now"
63-
diffMinutes < 60 -> "$diffMinutes minutes ago"
64-
diffHours < 24 -> "$diffHours hours ago"
65-
diffDays < 30 -> "$diffDays days ago" // Approximate for months
66-
else -> "$diffMonths months ago"
67-
}
68-
} catch (e: Exception) {
69-
Logger.warn("An unexpected error occurred while parsing date: ${e.message}")
70-
""
71-
}
50+
parsedDateTime.toInstant().toEpochMilli().toRelativeTimeString()
51+
}.onFailure {
52+
Logger.warn("Failed to parse date: ${it.message}", it, context = TAG)
53+
}.getOrDefault("")
7254
}

0 commit comments

Comments
 (0)