Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion app/src/main/java/to/bitkit/ext/DateTime.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down Expand Up @@ -238,6 +241,7 @@ enum class UiDateStyle {
DATE,
DATE_TIME,
DATE_TIME_YEAR,
DATE_TIME_YEAR_SHORT,
;

fun pattern(is24Hour: Boolean): String {
Expand All @@ -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"
}
}

Expand All @@ -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"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -297,7 +295,7 @@ private fun ChannelDetailContent(
SectionRow(
name = stringResource(R.string.lightning__created_on),
valueContent = {
CaptionB(text = formatDate(createdAt))
CaptionB(text = channelDateText(createdAt))
}
)

Expand All @@ -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))
}
)
}
Expand Down Expand Up @@ -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))
}
)
}
Expand All @@ -434,7 +432,7 @@ private fun ChannelDetailContent(
SectionRow(
name = stringResource(R.string.lightning__closed_on),
valueContent = {
CaptionB(text = formatDate(closedAt))
CaptionB(text = channelDateText(closedAt))
}
)
}
Expand Down Expand Up @@ -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(
Expand Down
57 changes: 57 additions & 0 deletions app/src/test/java/to/bitkit/ext/DateTimeExtTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
}
1 change: 1 addition & 0 deletions changelog.d/next/1123.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Lightning connection timestamps now follow the device's 12/24-hour time setting.
Loading