Repository: element-hq/element-x-android (language: kotlin)

Issue:
Use a sane date format for day markers
### Your use case

I would like to never have to see a MM/DD/YYYY format while using any application. Instead, the international YYYY-MM-DD format should be used.

### Have you considered any alternatives?

An expanded format with written out day and month names, as done by Element Web for example and requested in #2637. However, implementing that would need comparatively more effort.

### Additional context

_No response_

### Are you willing to provide a PR?

No


Discussion hints:
Another possibility is to take the format from the system locale.

Unfortunately Android really sucks in this regard though as it only has a single locale setting rather than `LC_*`. There is some limited possibility to customise here as you could, for instance, use en_CA.

While I'm personally a big fan of ISO8601 dates, I could also see using the system locale being a better default.

Pull request: Use full date format for day dividers in timeline
<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->
 
## Type of change

- [ ] Feature
- [ ] Bugfix
- [ ] Technical
- [x] Other : improve UX

## Content

- Adds a new `dateWithFullFormatFormatter` that will use the same format as iOS.
- Changes the `onlyTimeFormatter` so it also adds AM/PM for locales that use them.

## Motivation and context

Fixes #2916.

## Screenshots / GIFs

|Before|After|
|-|-|
|<img width="128" alt="image" src="https://github.com/element-hq/element-x-android/assets/480955/9aedec9b-5526-4ee9-977c-6028dbee2810">|<img width="255" alt="image" src="https://github.com/element-hq/element-x-android/assets/480955/b9256a80-a6f4-4ddb-958a-006fcf9c2a42">|
|<img width="539" alt="image" src="https://github.com/element-hq/element-x-android/assets/480955/8dccd4ea-1a12-492d-9ff0-c419105b7b47">|<img width="541" alt="image" src="https://github.com/element-hq/element-x-android/assets/480955/f98a6b56-deb4-4be6-9816-566332506ca5">|

## Tests

- Check the new formats are used.

## Tested devices

- [x] Physical
- [ ] Emulator
- OS version(s): 14

## Checklist

<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->

- [ ] Changes have been tested on an Android device or Android emulator with API 23
- [ ] UI change has been tested on both light and dark themes
- [ ] Accessibility has been taken into account. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [x] Pull request is based on the develop branch
- [x] Pull request includes a new file under ./changelog.d. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#changelog
- [x] Pull request includes screenshots or videos if containing UI changes
- [ ] Pull request includes a [sign off](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off)
- [x] You've made a self review of your PR


Test command: ./gradlew :libraries:dateformatter:impl:testDebugUnitTest --tests "io.element.android.libraries.dateformatter.impl.DefaultLastMessageTimestampFormatterTest"

Fix patch:
diff --git a/changelog.d/2916.misc b/changelog.d/2916.misc
new file mode 100644
index 00000000000..b87146dd580
--- /dev/null
+++ b/changelog.d/2916.misc
@@ -0,0 +1,1 @@
+Use a more natural date format for day dividers in the timeline. Also improve the time format for last messages in the room list.
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt
index 9493079925c..5d92f137e58 100644
--- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt
@@ -26,19 +26,18 @@ import kotlinx.datetime.toJavaLocalDate
 import kotlinx.datetime.toJavaLocalDateTime
 import java.time.Period
 import java.time.format.DateTimeFormatter
+import java.time.format.FormatStyle
 import java.util.Locale
 import javax.inject.Inject
 import kotlin.math.absoluteValue
 
-// TODO rework this date formatting
 class DateFormatters @Inject constructor(
     private val locale: Locale,
     private val clock: Clock,
     private val timeZone: TimeZone,
 ) {
     private val onlyTimeFormatter: DateTimeFormatter by lazy {
-        val pattern = DateFormat.getBestDateTimePattern(locale, "HH:mm") ?: "HH:mm"
-        DateTimeFormatter.ofPattern(pattern, locale)
+        DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale)
     }
 
     private val dateWithMonthFormatter: DateTimeFormatter by lazy {
@@ -51,6 +50,10 @@ class DateFormatters @Inject constructor(
         DateTimeFormatter.ofPattern(pattern, locale)
     }
 
+    private val dateWithFullFormatFormatter: DateTimeFormatter by lazy {
+        DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(locale)
+    }
+
     internal fun formatTime(localDateTime: LocalDateTime): String {
         return onlyTimeFormatter.format(localDateTime.toJavaLocalDateTime())
     }
@@ -63,6 +66,10 @@ class DateFormatters @Inject constructor(
         return dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime())
     }
 
+    internal fun formatDateWithFullFormat(localDateTime: LocalDateTime): String {
+        return dateWithFullFormatFormatter.format(localDateTime.toJavaLocalDateTime())
+    }
+
     internal fun formatDate(
         dateToFormat: LocalDateTime,
         currentDate: LocalDateTime,
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDaySeparatorFormatter.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDaySeparatorFormatter.kt
index 90d7f5c205e..27608946457 100644
--- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDaySeparatorFormatter.kt
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDaySeparatorFormatter.kt
@@ -28,6 +28,7 @@ class DefaultDaySeparatorFormatter @Inject constructor(
 ) : DaySeparatorFormatter {
     override fun format(timestamp: Long): String {
         val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
-        return dateFormatters.formatDateWithYear(dateToFormat)
+        // TODO use relative formatting once iOS uses it too
+        return dateFormatters.formatDateWithFullFormat(dateToFormat)
     }
 }


Test patch:
diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt
index dd8f75cae30..38e4fbffb35 100644
--- a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt
+++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt
@@ -21,6 +21,7 @@ import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormat
 import io.element.android.libraries.dateformatter.test.FakeClock
 import kotlinx.datetime.Instant
 import kotlinx.datetime.TimeZone
+import kotlinx.datetime.toLocalDateTime
 import org.junit.Test
 import java.util.Locale
 
@@ -44,7 +45,7 @@ class DefaultLastMessageTimestampFormatterTest {
         val now = "1980-04-06T18:35:24.00Z"
         val dat = "1980-04-06T18:35:24.00Z"
         val formatter = createFormatter(now)
-        assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("18:35")
+        assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("6:35 PM")
     }
 
     @Test
@@ -52,7 +53,7 @@ class DefaultLastMessageTimestampFormatterTest {
         val now = "1980-04-06T18:35:24.00Z"
         val dat = "1980-04-06T18:35:23.00Z"
         val formatter = createFormatter(now)
-        assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("18:35")
+        assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("6:35 PM")
     }
 
     @Test
@@ -60,7 +61,7 @@ class DefaultLastMessageTimestampFormatterTest {
         val now = "1980-04-06T18:35:24.00Z"
         val dat = "1980-04-06T18:34:24.00Z"
         val formatter = createFormatter(now)
-        assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("18:34")
+        assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("6:34 PM")
     }
 
     @Test
@@ -68,7 +69,7 @@ class DefaultLastMessageTimestampFormatterTest {
         val now = "1980-04-06T18:35:24.00Z"
         val dat = "1980-04-06T17:35:24.00Z"
         val formatter = createFormatter(now)
-        assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("17:35")
+        assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("5:35 PM")
     }
 
     @Test
@@ -96,6 +97,15 @@ class DefaultLastMessageTimestampFormatterTest {
         assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("06.04.1979")
     }
 
+    @Test
+    fun `test full format`() {
+        val now = "1980-04-06T18:35:24.00Z"
+        val dat = "1979-04-06T18:35:24.00Z"
+        val clock = FakeClock().apply { givenInstant(Instant.parse(now)) }
+        val dateFormatters = DateFormatters(Locale.US, clock, TimeZone.UTC)
+        assertThat(dateFormatters.formatDateWithFullFormat(Instant.parse(dat).toLocalDateTime(TimeZone.UTC))).isEqualTo("Friday, April 6, 1979")
+    }
+
     /**
      * Create DefaultLastMessageFormatter and set current time to the provided date.
      */