From 2b8419ef013e1b791f0132babff2fbe37498b9d6 Mon Sep 17 00:00:00 2001 From: Guzinowich Date: Fri, 31 Jul 2026 19:18:57 +0300 Subject: [PATCH 1/2] fix: use system time format in channel details --- app/src/main/java/to/bitkit/ext/DateTime.kt | 7 ++- .../settings/lightning/ChannelDetailScreen.kt | 36 ++++-------- .../java/to/bitkit/ext/DateTimeExtTest.kt | 57 +++++++++++++++++++ changelog.d/next/1110.fixed.md | 1 + 4 files changed, 75 insertions(+), 26 deletions(-) create mode 100644 changelog.d/next/1110.fixed.md diff --git a/app/src/main/java/to/bitkit/ext/DateTime.kt b/app/src/main/java/to/bitkit/ext/DateTime.kt index 69c94349d5..f9ace59142 100644 --- a/app/src/main/java/to/bitkit/ext/DateTime.kt +++ b/app/src/main/java/to/bitkit/ext/DateTime.kt @@ -56,6 +56,9 @@ fun ULong?.formatToString(pattern: String = DatePattern.DATE_TIME): String? { return this?.let { Instant.ofEpochSecond(toLong()).formatted(pattern) } } +fun String.toEpochSecondsOrNull(): ULong? = + runCatching { Instant.parse(this).epochSecond.toULong() }.getOrNull() + fun Long.toTimeUTC(): String { val instant = Instant.ofEpochMilli(this) val dateTime = LocalDateTime.ofInstant(instant, ZoneId.of("UTC")) @@ -238,6 +241,7 @@ enum class UiDateStyle { DATE, DATE_TIME, DATE_TIME_YEAR, + DATE_TIME_YEAR_SHORT, ; fun pattern(is24Hour: Boolean): String { @@ -247,6 +251,7 @@ enum class UiDateStyle { DATE -> DAY DATE_TIME -> "$DAY, $time" DATE_TIME_YEAR -> "$DAY_WITH_YEAR, $time" + DATE_TIME_YEAR_SHORT -> "$SHORT_DAY_WITH_YEAR, $time" } } @@ -255,12 +260,12 @@ enum class UiDateStyle { const val TIME_24H = "HH:mm" const val DAY = "MMMM d" const val DAY_WITH_YEAR = "MMMM d yyyy" + const val SHORT_DAY_WITH_YEAR = "MMM d, yyyy" } } object DatePattern { const val DATE_TIME = "dd/MM/yyyy, HH:mm" - const val CHANNEL_DETAILS = "MMM d, yyyy, HH:mm" const val LOG_FILE = "yyyy-MM-dd_HH-mm-ss" const val LOG_LINE = "yyyy-MM-dd HH:mm:ss.SSS" diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index ae3bea0b6e..9c919e89e1 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -56,11 +56,12 @@ import kotlinx.collections.immutable.persistentListOf import org.lightningdevkit.ldknode.OutPoint import to.bitkit.R import to.bitkit.env.Env -import to.bitkit.ext.DatePattern +import to.bitkit.ext.UiDateStyle import to.bitkit.ext.amountOnClose import to.bitkit.ext.createChannelDetails import to.bitkit.ext.resolveDisplayShortChannelId import to.bitkit.ext.setClipboardText +import to.bitkit.ext.toEpochSecondsOrNull import to.bitkit.models.Toast import to.bitkit.models.msatFloorOf import to.bitkit.ui.Routes @@ -83,10 +84,7 @@ import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.getBlockExplorerUrl -import java.time.Instant -import java.time.ZoneId -import java.time.format.DateTimeFormatter -import java.util.Locale +import to.bitkit.ui.utils.uiDateText @Composable fun ChannelDetailScreen( @@ -297,7 +295,7 @@ private fun ChannelDetailContent( SectionRow( name = stringResource(R.string.lightning__created_on), valueContent = { - CaptionB(text = formatDate(createdAt)) + CaptionB(text = channelDateText(createdAt)) } ) @@ -307,7 +305,7 @@ private fun ChannelDetailContent( SectionRow( name = stringResource(R.string.lightning__order_expiry), valueContent = { - CaptionB(text = formatDate(blocktankOrder.orderExpiresAt)) + CaptionB(text = channelDateText(blocktankOrder.orderExpiresAt)) } ) } @@ -420,7 +418,7 @@ private fun ChannelDetailContent( SectionRow( name = stringResource(R.string.lightning__opened_on), valueContent = { - CaptionB(text = formatUnixTimestamp(txTime.toLong())) + CaptionB(text = uiDateText(txTime, UiDateStyle.DATE_TIME_YEAR_SHORT)) } ) } @@ -434,7 +432,7 @@ private fun ChannelDetailContent( SectionRow( name = stringResource(R.string.lightning__closed_on), valueContent = { - CaptionB(text = formatDate(closedAt)) + CaptionB(text = channelDateText(closedAt)) } ) } @@ -585,22 +583,10 @@ private fun getChannelStatus( return if (channel.details.isChannelReady) ChannelStatusUi.OPEN else ChannelStatusUi.PENDING } -private fun formatDate(dateString: String): String { - return runCatching { - val instant = Instant.parse(dateString) - val formatter = DateTimeFormatter.ofPattern(DatePattern.CHANNEL_DETAILS, Locale.getDefault()) - .withZone(ZoneId.systemDefault()) - formatter.format(instant) - }.getOrDefault(dateString) -} - -private fun formatUnixTimestamp(timestamp: Long): String { - return runCatching { - val instant = Instant.ofEpochSecond(timestamp) - val formatter = DateTimeFormatter.ofPattern(DatePattern.CHANNEL_DETAILS, Locale.getDefault()) - .withZone(ZoneId.systemDefault()) - formatter.format(instant) - }.getOrDefault(timestamp.toString()) +@Composable +private fun channelDateText(dateString: String): String { + val timestamp = remember(dateString) { dateString.toEpochSecondsOrNull() } + return if (timestamp != null) uiDateText(timestamp, UiDateStyle.DATE_TIME_YEAR_SHORT) else dateString } private fun contactSupport( diff --git a/app/src/test/java/to/bitkit/ext/DateTimeExtTest.kt b/app/src/test/java/to/bitkit/ext/DateTimeExtTest.kt index 5332e70919..19050e6ec9 100644 --- a/app/src/test/java/to/bitkit/ext/DateTimeExtTest.kt +++ b/app/src/test/java/to/bitkit/ext/DateTimeExtTest.kt @@ -10,6 +10,7 @@ import java.util.Locale import java.util.concurrent.TimeUnit import kotlin.test.assertEquals import kotlin.test.assertNotNull +import kotlin.test.assertNull import kotlin.test.assertTrue import kotlin.time.ExperimentalTime @@ -261,5 +262,61 @@ class DateTimeExtTest : BaseUnitTest() { assertEquals(UiDateStyle.DATE_TIME, uiDateStyleFor(lateEvening.epochSecond.toULong(), today, bucharest)) } + @Test + fun `toEpochSecondsOrNull parses an ISO-8601 instant`() { + val result = "2026-03-07T15:23:00Z".toEpochSecondsOrNull() + + assertEquals(AFTERNOON.epochSecond.toULong(), result) + } + + @Test + fun `toEpochSecondsOrNull parses the millisecond form Blocktank returns`() { + val result = "2026-03-07T15:23:00.000Z".toEpochSecondsOrNull() + + assertEquals(AFTERNOON.epochSecond.toULong(), result) + } + + @Test + fun `toEpochSecondsOrNull returns null for an unparseable string`() { + val result = "not a date".toEpochSecondsOrNull() + + assertNull(result) + } + + @Test + fun `toEpochSecondsOrNull returns null for an empty string`() { + val result = "".toEpochSecondsOrNull() + + assertNull(result) + } + + @Test + fun `DATE_TIME_YEAR_SHORT abbreviates the month and applies the selected clock format`() { + val in24Hour = AFTERNOON.formattedInUtc(UiDateStyle.DATE_TIME_YEAR_SHORT.pattern(is24Hour = true)) + val in12Hour = AFTERNOON.formattedInUtc(UiDateStyle.DATE_TIME_YEAR_SHORT.pattern(is24Hour = false)) + + assertEquals("Mar 7, 2026, 15:23", in24Hour) + assertEquals("Mar 7, 2026, 3:23 PM", in12Hour) + } + + @Test + fun `DATE_TIME_YEAR_SHORT localizes the month name`() { + val pattern = UiDateStyle.DATE_TIME_YEAR_SHORT.pattern(is24Hour = true) + val result = AFTERNOON.formatted(pattern, Locale.GERMANY, UTC) + + assertEquals("März 7, 2026, 15:23", result) + } + + @Test + fun `toEpochSecondsOrNull round-trips through the channel details format`() { + val timestamp = "2026-03-07T15:23:00.000Z".toEpochSecondsOrNull() + + assertNotNull(timestamp) + val result = Instant.ofEpochSecond(timestamp.toLong()) + .formatted(UiDateStyle.DATE_TIME_YEAR_SHORT.pattern(is24Hour = true), Locale.US, UTC) + + assertEquals("Mar 7, 2026, 15:23", result) + } + private fun Instant.formattedInUtc(pattern: String) = formatted(pattern, Locale.US, UTC) } diff --git a/changelog.d/next/1110.fixed.md b/changelog.d/next/1110.fixed.md new file mode 100644 index 0000000000..d464fe7e11 --- /dev/null +++ b/changelog.d/next/1110.fixed.md @@ -0,0 +1 @@ +Lightning connection timestamps now follow the device's 12/24-hour time setting. From a01e1920be50050df93a581cf40804093135a08d Mon Sep 17 00:00:00 2001 From: Guzinowich Date: Fri, 31 Jul 2026 19:54:31 +0300 Subject: [PATCH 2/2] chore: rename changelog fragment --- changelog.d/next/{1110.fixed.md => 1123.fixed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/next/{1110.fixed.md => 1123.fixed.md} (100%) diff --git a/changelog.d/next/1110.fixed.md b/changelog.d/next/1123.fixed.md similarity index 100% rename from changelog.d/next/1110.fixed.md rename to changelog.d/next/1123.fixed.md