From f9294d66aca0e673475673fd5bc26530ebf1a3c1 Mon Sep 17 00:00:00 2001 From: fethij <32542424+fethij@users.noreply.github.com> Date: Thu, 28 May 2026 23:54:32 +0200 Subject: [PATCH] Fix jump-to-date being off by one day in non-UTC timezones MaterialDatePicker reports the selected day, the calendar constraints, and the validator callbacks in terms of UTC midnight. When reading those values back, both JumpToDateValidator.normalizeToLocalMidnight() and the date picker's positive-button handler converted the UTC-midnight instant using the local zone: Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).toLocalDate() For any negative UTC offset, the UTC-midnight instant of calendar day D falls on the previous evening locally, so toLocalDate() yields D-1. As a result a message sent on local day D became selectable under picker day D+1 (the reported "selectable dates are one day ahead" symptom), and the positive-button handler shifted the jump target by a day as well. Read the calendar day in UTC (matching how MaterialDatePicker emits it), then re-anchor to local midnight, which is how message days are keyed in the database (messageExistsOnDays). Add a regression test exercising a negative-offset zone (America/New_York); it fails before this change and passes after. --- .../conversation/v2/ConversationFragment.kt | 3 ++- .../conversation/v2/JumpToDateValidator.kt | 3 ++- .../v2/JumpToDateValidatorTest.kt | 23 +++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index a1afdcfc1d9..674a9c38838 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -390,6 +390,7 @@ import org.thoughtcrime.securesms.wallpaper.ChatWallpaperDimLevelUtil import java.time.Instant import java.time.LocalDateTime import java.time.ZoneId +import java.time.ZoneOffset import java.util.Locale import java.util.Optional import java.util.concurrent.ExecutionException @@ -5247,7 +5248,7 @@ class ConversationFragment : datePicker.addOnPositiveButtonClickListener { selectedDate -> if (selectedDate != null) { val localMidnightTimestamp = Instant.ofEpochMilli(selectedDate) - .atZone(ZoneId.systemDefault()) + .atZone(ZoneOffset.UTC) .toLocalDate() .atStartOfDay(ZoneId.systemDefault()) .toInstant() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/JumpToDateValidator.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/JumpToDateValidator.kt index 4187032070e..ad988856d3c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/JumpToDateValidator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/JumpToDateValidator.kt @@ -16,6 +16,7 @@ import java.time.Instant import java.time.LocalDate import java.time.LocalDateTime import java.time.ZoneId +import java.time.ZoneOffset import java.time.temporal.TemporalAdjusters import java.util.concurrent.Executor import kotlin.time.Duration.Companion.days @@ -105,7 +106,7 @@ class JumpToDateValidator private constructor( private fun normalizeToLocalMidnight(timestamp: Long): Long { return Instant.ofEpochMilli(timestamp) - .atZone(zoneId) + .atZone(ZoneOffset.UTC) .toLocalDate() .atStartOfDay(zoneId) .toInstant() diff --git a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/JumpToDateValidatorTest.kt b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/JumpToDateValidatorTest.kt index c3e5d57b409..16a8f79c16c 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/JumpToDateValidatorTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/JumpToDateValidatorTest.kt @@ -187,4 +187,27 @@ class JumpToDateValidatorTest { assertThat(validator.isValid(june15Utc)).isTrue() } + + @Test + fun `picker date maps to the same calendar day in a negative-offset zone`() { + val newYorkZone = ZoneId.of("America/New_York") + + // Days are keyed by local midnight in the database. + val june15NewYorkMidnight = LocalDate.of(2024, 6, 15) + .atStartOfDay(newYorkZone) + .toInstant() + .toEpochMilli() + + val lookup = { dates: Collection -> + dates.associateWith { it == june15NewYorkMidnight } + } + val validator = createValidator(lookup, zone = newYorkZone) + + // The picker reports June 15 as UTC midnight; it must map to June 15 locally, not June 14. + val june15PickerUtc = timestampForDate(2024, 6, 15) + + validator.isValid(june15PickerUtc) + + assertThat(validator.isValid(june15PickerUtc)).isTrue() + } }