diff --git a/features/messages/build.gradle.kts b/features/messages/build.gradle.kts index b6f6fe4f33..1eb054d3d0 100644 --- a/features/messages/build.gradle.kts +++ b/features/messages/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.designsystem) implementation(projects.libraries.textcomposer) + implementation(projects.libraries.dateformatter) implementation(libs.coil.compose) implementation(libs.datetime) implementation(libs.accompanist.flowlayout) diff --git a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/factories/virtual/TimelineItemDaySeparatorFactory.kt b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/factories/virtual/TimelineItemDaySeparatorFactory.kt index 00a36c06e5..43a6beada7 100644 --- a/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/factories/virtual/TimelineItemDaySeparatorFactory.kt +++ b/features/messages/src/main/kotlin/io/element/android/features/messages/timeline/factories/virtual/TimelineItemDaySeparatorFactory.kt @@ -18,30 +18,16 @@ package io.element.android.features.messages.timeline.factories.virtual import io.element.android.features.messages.timeline.model.virtual.TimelineItemDaySeparatorModel import io.element.android.features.messages.timeline.model.virtual.TimelineItemVirtualModel -import kotlinx.datetime.Instant -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toJavaLocalDateTime -import kotlinx.datetime.toLocalDateTime +import io.element.android.libraries.dateformatter.DaySeparatorFormatter import org.matrix.rustcomponents.sdk.VirtualTimelineItem -import java.time.format.DateTimeFormatter -import java.util.Locale import javax.inject.Inject -class TimelineItemDaySeparatorFactory @Inject constructor() { - - //TODO use proper formatter - private val locale: Locale = Locale.getDefault() - - private val dateWithYearFormatter: DateTimeFormatter by lazy { - val pattern = android.text.format.DateFormat.getBestDateTimePattern(locale, "dd.MM.yyyy") - DateTimeFormatter.ofPattern(pattern) - } +class TimelineItemDaySeparatorFactory @Inject constructor(private val daySeparatorFormatter: DaySeparatorFormatter) { fun create(virtualItem: VirtualTimelineItem.DayDivider): TimelineItemVirtualModel { - val tsInstant = Instant.fromEpochMilliseconds(virtualItem.ts.toLong()) - val tsDateTime = tsInstant.toLocalDateTime(TimeZone.currentSystemDefault()) + val formattedDate = daySeparatorFormatter.format(virtualItem.ts.toLong()) return TimelineItemDaySeparatorModel( - formattedDate = dateWithYearFormatter.format(tsDateTime.toJavaLocalDateTime()) + formattedDate = formattedDate ) } } diff --git a/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/DaySeparatorFormatter.kt b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/DaySeparatorFormatter.kt new file mode 100644 index 0000000000..453bbb2154 --- /dev/null +++ b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/DaySeparatorFormatter.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.dateformatter + +interface DaySeparatorFormatter { + fun format(timestamp: Long): String +} diff --git a/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt new file mode 100644 index 0000000000..030963aa8f --- /dev/null +++ b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatters.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.dateformatter.impl + +import android.content.Context +import android.text.format.DateFormat +import android.text.format.DateUtils +import io.element.android.libraries.di.ApplicationContext +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toInstant +import kotlinx.datetime.toJavaLocalDate +import kotlinx.datetime.toJavaLocalDateTime +import java.time.Period +import java.time.format.DateTimeFormatter +import java.util.Locale +import javax.inject.Inject +import kotlin.math.absoluteValue + +class DateFormatters @Inject constructor( + @ApplicationContext private val context: Context, + private val locale: Locale, + private val clock: Clock, + private val timeZone: TimeZone, +) { + + private val hourFormatter by lazy { + if (DateFormat.is24HourFormat(context)) { + DateTimeFormatter.ofPattern("HH:mm", locale) + } else { + DateTimeFormatter.ofPattern("h:mm a", locale) + } + } + + private val fullDateFormatter by lazy { + val pattern = if (DateFormat.is24HourFormat(context)) { + DateFormat.getBestDateTimePattern(locale, "EEE, d MMM yyyy HH:mm") + } else { + DateFormat.getBestDateTimePattern(locale, "EEE, d MMM yyyy h:mm a") + } + DateTimeFormatter.ofPattern(pattern, locale) + } + + private val dateWithMonthFormatter: DateTimeFormatter by lazy { + val pattern = DateFormat.getBestDateTimePattern(locale, "d MMM") + DateTimeFormatter.ofPattern(pattern) + } + + private val dateWithYearFormatter: DateTimeFormatter by lazy { + val pattern = DateFormat.getBestDateTimePattern(locale, "d MMM y") + DateTimeFormatter.ofPattern(pattern) + } + + internal fun formatFullDate(localDateTime: LocalDateTime): String { + return fullDateFormatter.format(localDateTime.toJavaLocalDateTime()) + } + + internal fun formatHour(localDateTime: LocalDateTime): String { + return hourFormatter.format(localDateTime.toJavaLocalDateTime()) + } + + internal fun formatDateWithMonth(localDateTime: LocalDateTime): String { + return dateWithMonthFormatter.format(localDateTime.toJavaLocalDateTime()) + } + + internal fun formatDateWithYear(localDateTime: LocalDateTime): String { + return dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime()) + } + + internal fun formatDate( + dateToFormat: LocalDateTime, + currentDate: LocalDateTime, + useRelative: Boolean + ): String { + val period = Period.between(dateToFormat.date.toJavaLocalDate(), currentDate.date.toJavaLocalDate()) + return if (period.years.absoluteValue >= 1) { + formatDateWithYear(dateToFormat) + } else if (useRelative && period.days.absoluteValue < 2 && period.months.absoluteValue < 1) { + getRelativeDay(dateToFormat.toInstant(timeZone).toEpochMilliseconds()) + } else { + formatDateWithMonth(dateToFormat) + } + } + + private fun getRelativeDay(ts: Long): String { + return DateUtils.getRelativeTimeSpanString( + ts, + clock.now().toEpochMilliseconds(), + DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_SHOW_WEEKDAY + ).toString() + } +} diff --git a/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDaySeparatorFormatter.kt b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDaySeparatorFormatter.kt new file mode 100644 index 0000000000..29e2c87221 --- /dev/null +++ b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDaySeparatorFormatter.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.dateformatter.impl + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.dateformatter.DaySeparatorFormatter +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultDaySeparatorFormatter @Inject constructor( + private val localDateTimeProvider: LocalDateTimeProvider, + private val dateFormatters: DateFormatters, +) : DaySeparatorFormatter { + + override fun format(timestamp: Long): String { + val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp) + return dateFormatters.formatDateWithYear(dateToFormat) + } +} diff --git a/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt index a466491766..0ab309340d 100644 --- a/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt +++ b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageFormatter.kt @@ -16,91 +16,33 @@ package io.element.android.libraries.dateformatter.impl -import android.text.format.DateFormat -import android.text.format.DateUtils import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.dateformatter.LastMessageFormatter import io.element.android.libraries.di.AppScope -import kotlinx.datetime.Clock -import kotlinx.datetime.Instant -import kotlinx.datetime.LocalDateTime -import kotlinx.datetime.TimeZone -import kotlinx.datetime.toInstant -import kotlinx.datetime.toJavaLocalDate -import kotlinx.datetime.toJavaLocalDateTime -import kotlinx.datetime.toLocalDateTime -import java.time.Period -import java.time.format.DateTimeFormatter -import java.util.Locale import javax.inject.Inject -import kotlin.math.absoluteValue @ContributesBinding(AppScope::class) class DefaultLastMessageFormatter @Inject constructor( - private val clock: Clock, - private val locale: Locale, - private val timezone: TimeZone, + private val localDateTimeProvider: LocalDateTimeProvider, + private val dateFormatters: DateFormatters, ) : LastMessageFormatter { - private val onlyTimeFormatter: DateTimeFormatter by lazy { - val pattern = DateFormat.getBestDateTimePattern(locale, "HH:mm") ?: "HH:mm" - DateTimeFormatter.ofPattern(pattern) - } - - private val dateWithMonthFormatter: DateTimeFormatter by lazy { - val pattern = DateFormat.getBestDateTimePattern(locale, "d MMM") ?: "d MMM" - DateTimeFormatter.ofPattern(pattern) - } - - private val dateWithYearFormatter: DateTimeFormatter by lazy { - val pattern = DateFormat.getBestDateTimePattern(locale, "dd.MM.yyyy") ?: "dd.MM.yyyy" - DateTimeFormatter.ofPattern(pattern) - } override fun format(timestamp: Long?): String { if (timestamp == null) return "" - val now: Instant = clock.now() - val tsInstant = Instant.fromEpochMilliseconds(timestamp) - val nowDateTime = now.toLocalDateTime(timezone) - val tsDateTime = tsInstant.toLocalDateTime(timezone) - val isSameDay = nowDateTime.date == tsDateTime.date + val currentDate = localDateTimeProvider.providesNow() + val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp) + val isSameDay = currentDate.date == dateToFormat.date return when { isSameDay -> { - onlyTimeFormatter.format(tsDateTime.toJavaLocalDateTime()) + dateFormatters.formatHour(dateToFormat) } else -> { - formatDate(tsDateTime, nowDateTime) + dateFormatters.formatDate( + dateToFormat = dateToFormat, + currentDate = currentDate, + useRelative = true + ) } } } - - private fun formatDate( - date: LocalDateTime, - currentDate: LocalDateTime, - ): String { - val period = Period.between(date.date.toJavaLocalDate(), currentDate.date.toJavaLocalDate()) - return if (period.years.absoluteValue >= 1) { - formatDateWithYear(date) - } else if (period.days.absoluteValue < 2 && period.months.absoluteValue < 1) { - getRelativeDay(date.toInstant(timezone).toEpochMilliseconds()) - } else { - formatDateWithMonth(date) - } - } - - private fun formatDateWithMonth(localDateTime: LocalDateTime): String { - return dateWithMonthFormatter.format(localDateTime.toJavaLocalDateTime()) - } - - private fun formatDateWithYear(localDateTime: LocalDateTime): String { - return dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime()) - } - - private fun getRelativeDay(ts: Long): String { - return DateUtils.getRelativeTimeSpanString( - ts, - clock.now().toEpochMilliseconds(), - DateUtils.DAY_IN_MILLIS, - DateUtils.FORMAT_SHOW_WEEKDAY - )?.toString() ?: "" - } } diff --git a/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocalDateTimeProvider.kt b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocalDateTimeProvider.kt new file mode 100644 index 0000000000..8395cb476c --- /dev/null +++ b/libraries/dateformatter/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocalDateTimeProvider.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.dateformatter.impl + +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime +import javax.inject.Inject + +class LocalDateTimeProvider @Inject constructor( + private val clock: Clock, + private val timezone: TimeZone, +) { + + fun providesNow(): LocalDateTime { + val now: Instant = clock.now() + return now.toLocalDateTime(timezone) + } + + fun providesFromTimestamp(timestamp: Long): LocalDateTime { + val tsInstant = Instant.fromEpochMilliseconds(timestamp) + return tsInstant.toLocalDateTime(timezone) + } +}