diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/FilterBottomSheet.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/FilterBottomSheet.kt index 56c1d88..48cd53f 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/FilterBottomSheet.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/FilterBottomSheet.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import com.bitcointxoko.gudariwallet.LocalAppStrings +import com.bitcointxoko.gudariwallet.util.formatEpochShort @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/HistoryScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/HistoryScreen.kt index 4f98e43..64ec165 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/HistoryScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/HistoryScreen.kt @@ -59,6 +59,7 @@ import androidx.compose.ui.unit.dp import com.bitcointxoko.gudariwallet.api.PaymentRecord import com.bitcointxoko.gudariwallet.LocalAppStrings import com.bitcointxoko.gudariwallet.ui.HistoryState +import com.bitcointxoko.gudariwallet.util.formatEpochShort import kotlinx.coroutines.launch // ── List screen ────────────────────────────────────────────────────────────── diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/HistoryUtils.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/HistoryUtils.kt index b121f88..c11ce8b 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/HistoryUtils.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/HistoryUtils.kt @@ -4,36 +4,8 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import com.bitcointxoko.gudariwallet.ui.theme.semanticColors - -// ── Pure helpers ───────────────────────────────────────────────────────────── - -internal fun formatEpochShort(epoch: Long): String = - java.time.format.DateTimeFormatter.ofPattern("dd MMM") - .withZone(java.time.ZoneId.systemDefault()) - .format(java.time.Instant.ofEpochSecond(epoch)) - -internal fun formatTimestamp(createdAt: Long?, rawTime: String): String { - val formatter = java.time.format.DateTimeFormatter - .ofPattern("dd MMM yyyy, HH:mm") - .withZone(java.time.ZoneId.systemDefault()) - if (createdAt != null && createdAt > 1_000_000_000L) { // sanity check - return formatter.format(java.time.Instant.ofEpochSecond(createdAt)) - } - // Fallback: only reached for payments not yet written through Room - return runCatching { - formatter.format(java.time.Instant.ofEpochSecond(rawTime.toLong())) - }.recoverCatching { - formatter.format(java.time.OffsetDateTime.parse(rawTime)) - }.getOrDefault(rawTime) -} - -internal fun isPendingStatus(status: String): Boolean = - status.lowercase() in setOf("pending", "in_flight", "inflight") - -internal fun isFailedStatus(status: String): Boolean = - status.lowercase() in setOf("failed", "error", "expired") - -// ── Composable helpers ─────────────────────────────────────────────────────── +import com.bitcointxoko.gudariwallet.util.isFailedStatus +import com.bitcointxoko.gudariwallet.util.isPendingStatus @Composable internal fun paymentAmountColor(status: String, isOutgoing: Boolean): Color { diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/PaymentDetailScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/PaymentDetailScreen.kt index b5f652f..1b906b4 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/PaymentDetailScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/PaymentDetailScreen.kt @@ -47,6 +47,7 @@ import com.bitcointxoko.gudariwallet.LocalAppStrings import com.bitcointxoko.gudariwallet.ui.DetailState import com.bitcointxoko.gudariwallet.util.feePpm import com.bitcointxoko.gudariwallet.util.formatFiatForSats +import com.bitcointxoko.gudariwallet.util.formatTimestamp import kotlinx.coroutines.launch // ── Detail screen ──────────────────────────────────────────────────────────── diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/PaymentRow.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/PaymentRow.kt index 2d2fabb..b48980f 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/PaymentRow.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/history/PaymentRow.kt @@ -26,6 +26,9 @@ import com.bitcointxoko.gudariwallet.api.PaymentRecord import com.bitcointxoko.gudariwallet.LocalAppStrings import com.bitcointxoko.gudariwallet.ui.theme.semanticColors import com.bitcointxoko.gudariwallet.util.formatFiatForSats +import com.bitcointxoko.gudariwallet.util.formatTimestamp +import com.bitcointxoko.gudariwallet.util.isFailedStatus +import com.bitcointxoko.gudariwallet.util.isPendingStatus @Composable internal fun PaymentRow( diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcConnectionRow.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcConnectionRow.kt index a12ff70..6fb59d9 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcConnectionRow.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcConnectionRow.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.bitcointxoko.gudariwallet.api.NwcGetResponse import com.bitcointxoko.gudariwallet.ui.theme.semanticColors +import com.bitcointxoko.gudariwallet.util.formatEpoch import kotlinx.coroutines.launch import java.time.Instant diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcDetailScreen.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcDetailScreen.kt index 6e5eac6..6188d29 100644 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcDetailScreen.kt +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcDetailScreen.kt @@ -62,6 +62,7 @@ import com.bitcointxoko.gudariwallet.LocalAppStrings import com.bitcointxoko.gudariwallet.i18n.AppStrings import com.bitcointxoko.gudariwallet.ui.history.DetailRow import com.bitcointxoko.gudariwallet.ui.history.DetailSection +import com.bitcointxoko.gudariwallet.util.formatEpoch import kotlinx.coroutines.launch // ── ConnectionDetailScreen.kt ───────────────────────────────────────────────── diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcUtils.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcUtils.kt deleted file mode 100644 index 384de31..0000000 --- a/app/src/main/java/com/bitcointxoko/gudariwallet/ui/nwc/NwcUtils.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.bitcointxoko.gudariwallet.ui.nwc - -import java.time.Instant -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle - -// ── Date / time formatting ──────────────────────────────────────────── - -internal val dateFormatter: DateTimeFormatter = - DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT) - .withZone(ZoneId.systemDefault()) - -/** Formats an epoch-seconds timestamp into a localized human-readable string. - * Returns "—" if the timestamp cannot be formatted. */ -internal fun formatEpoch(epochSeconds: Long): String = - runCatching { dateFormatter.format(Instant.ofEpochSecond(epochSeconds)) } - .getOrDefault("—") diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/util/DateTimeUtils.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/util/DateTimeUtils.kt new file mode 100644 index 0000000..be23f63 --- /dev/null +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/util/DateTimeUtils.kt @@ -0,0 +1,57 @@ +package com.bitcointxoko.gudariwallet.util + +import timber.log.Timber +import java.time.Instant +import java.time.ZoneId +import java.time.format.DateTimeFormatter +import java.time.format.FormatStyle + +// ── Epoch formatters ────────────────────────────────────────────────────────── +// +// All formatters are built on each call so that ZoneId.systemDefault() and the +// current locale are always fresh (a file-level val would capture them once at +// class-load time and become stale after a timezone/locale change). + +/** + * "dd MMM" — e.g. "09 Jun" + * Suitable for compact list labels. + */ +fun formatEpochShort(epochSeconds: Long): String = + DateTimeFormatter.ofPattern("dd MMM") + .withZone(ZoneId.systemDefault()) + .format(Instant.ofEpochSecond(epochSeconds)) + +/** + * "dd MMM yyyy, HH:mm" — e.g. "09 Jun 2025, 14:32" + * Accepts either a numeric epoch-seconds value or an ISO offset-date-time string + * as [rawTime] fallback (for payments not yet persisted through Room). + */ +fun formatTimestamp(createdAt: Long?, rawTime: String): String { + val formatter = DateTimeFormatter + .ofPattern("dd MMM yyyy, HH:mm") + .withZone(ZoneId.systemDefault()) + if (createdAt != null && createdAt > 1_000_000_000L) { + return formatter.format(Instant.ofEpochSecond(createdAt)) + } + return runCatching { + formatter.format(Instant.ofEpochSecond(rawTime.toLong())) + }.recoverCatching { + formatter.format(java.time.OffsetDateTime.parse(rawTime)) + }.getOrDefault(rawTime) +} + +/** + * Localized SHORT date-time — e.g. "6/9/25, 2:32 PM" (locale-dependent). + * Returns "—" for out-of-range epoch values; callers should handle sentinel + * values (e.g. 0L = "never expires") before calling this. + */ +fun formatEpoch(epochSeconds: Long): String = + try { + DateTimeFormatter + .ofLocalizedDateTime(FormatStyle.SHORT) + .withZone(ZoneId.systemDefault()) + .format(Instant.ofEpochSecond(epochSeconds)) + } catch (e: Exception) { + Timber.tag("DateTimeUtils").w(e, "formatEpoch: could not format epochSeconds=$epochSeconds") + "—" + } diff --git a/app/src/main/java/com/bitcointxoko/gudariwallet/util/PaymentUtils.kt b/app/src/main/java/com/bitcointxoko/gudariwallet/util/PaymentUtils.kt new file mode 100644 index 0000000..7e12dbe --- /dev/null +++ b/app/src/main/java/com/bitcointxoko/gudariwallet/util/PaymentUtils.kt @@ -0,0 +1,7 @@ +package com.bitcointxoko.gudariwallet.util + +fun isPendingStatus(status: String): Boolean = + status.lowercase() in setOf("pending", "in_flight", "inflight") + +fun isFailedStatus(status: String): Boolean = + status.lowercase() in setOf("failed", "error", "expired")