Merge pull request #4026 from element-hq/feature/bma/monthSeparators

Implement month separator for the Gallery, and improve date rendering.
This commit is contained in:
Benoit Marty 2024-12-12 17:48:17 +01:00 committed by GitHub
commit d5b3eea824
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
100 changed files with 1704 additions and 403 deletions

View file

@ -7,6 +7,8 @@
package io.element.android.libraries.core.extensions
import java.util.Locale
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
fun Boolean.to01() = if (this) "1" else "0"
@ -68,3 +70,16 @@ fun String.replacePrefix(oldPrefix: String, newPrefix: String): String {
fun String.withBrackets(prefix: String = "(", suffix: String = ")"): String {
return "$prefix$this$suffix"
}
/**
* Capitalize the string.
*/
fun String.safeCapitalize(): String {
return replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(Locale.getDefault())
} else {
it.toString()
}
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.api
interface DateFormatter {
fun format(
timestamp: Long?,
mode: DateFormatterMode = DateFormatterMode.Full,
useRelative: Boolean = false,
): String
}
enum class DateFormatterMode {
/**
* Full date and time.
* Example:
* "April 6, 1980 at 6:35 PM"
* Format can be shorter when useRelative is true.
* Example:
* "6:35 PM"
*/
Full,
/**
* Only month and year.
* Example:
* "April 1980"
* "This month" can be returned when useRelative is true.
* Example:
* "This month"
*/
Month,
/**
* Only day.
* Example:
* "Sunday 6 April"
* "Today", "Yesterday" and day of week can be returned when useRelative is true.
*/
Day,
/**
* Time if same day, else date.
*/
TimeOrDate,
/**
* Only time whatever the day.
*/
TimeOnly,
}

View file

@ -1,12 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.api
interface DaySeparatorFormatter {
fun format(timestamp: Long): String
}

View file

@ -1,12 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.api
fun interface LastMessageTimestampFormatter {
fun format(timestamp: Long?): String
}

View file

@ -8,7 +8,7 @@ import extension.setupAnvil
*/
plugins {
id("io.element.android-library")
id("io.element.android-compose-library")
}
setupAnvil()
@ -16,15 +16,30 @@ setupAnvil()
android {
namespace = "io.element.android.libraries.dateformatter.impl"
testOptions {
unitTests {
isIncludeAndroidResources = true
}
}
dependencies {
implementation(libs.dagger)
implementation(projects.libraries.core)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.di)
implementation(projects.libraries.uiStrings)
implementation(projects.services.toolbox.api)
api(projects.libraries.dateformatter.api)
api(libs.datetime)
testImplementation(libs.test.junit)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(libs.test.robolectric)
testImplementation(projects.libraries.dateformatter.test)
testImplementation(projects.services.toolbox.test)
testImplementation(projects.tests.testutils)
testImplementation(libs.androidx.compose.ui.test.junit)
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.core.extensions.safeCapitalize
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
interface DateFormatterDay {
fun format(
timestamp: Long,
useRelative: Boolean,
): String
}
@ContributesBinding(AppScope::class)
class DefaultDateFormatterDay @Inject constructor(
private val localDateTimeProvider: LocalDateTimeProvider,
private val dateFormatters: DateFormatters,
) : DateFormatterDay {
override fun format(
timestamp: Long,
useRelative: Boolean,
): String {
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
val today = localDateTimeProvider.providesNow()
return if (useRelative) {
val dayDiff = today.date.toEpochDays() - dateToFormat.date.toEpochDays()
when (dayDiff) {
0 -> dateFormatters.getRelativeDay(timestamp, "Today")
1 -> dateFormatters.getRelativeDay(timestamp, "Yesterday")
else -> if (dayDiff < 7) {
dateFormatters.formatDateWithDay(dateToFormat)
} else {
if (today.year == dateToFormat.year) {
dateFormatters.formatDateWithFullFormatNoYear(dateToFormat)
} else {
dateFormatters.formatDateWithFullFormat(dateToFormat)
}
}
}
} else {
if (today.year == dateToFormat.year) {
dateFormatters.formatDateWithFullFormatNoYear(dateToFormat)
} else {
dateFormatters.formatDateWithFullFormat(dateToFormat)
}
}
.safeCapitalize()
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl
import io.element.android.services.toolbox.api.strings.StringProvider
import javax.inject.Inject
class DateFormatterFull @Inject constructor(
private val stringProvider: StringProvider,
private val localDateTimeProvider: LocalDateTimeProvider,
private val dateFormatters: DateFormatters,
private val dateFormatterDay: DateFormatterDay,
) {
fun format(
timestamp: Long,
useRelative: Boolean,
): String {
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
val time = dateFormatters.formatTime(dateToFormat)
return if (useRelative) {
val now = localDateTimeProvider.providesNow()
if (now.date == dateToFormat.date) {
time
} else {
val dateStr = dateFormatterDay.format(timestamp, true)
stringProvider.getString(R.string.common_date_date_at_time, dateStr, time)
}
} else {
val dateStr = dateFormatters.formatDateWithFullFormat(dateToFormat)
stringProvider.getString(R.string.common_date_date_at_time, dateStr, time)
}
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl
import io.element.android.libraries.core.extensions.safeCapitalize
import io.element.android.services.toolbox.api.strings.StringProvider
import javax.inject.Inject
class DateFormatterMonth @Inject constructor(
private val stringProvider: StringProvider,
private val localDateTimeProvider: LocalDateTimeProvider,
private val dateFormatters: DateFormatters,
) {
fun format(
timestamp: Long,
useRelative: Boolean,
): String {
val today = localDateTimeProvider.providesNow()
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
return if (useRelative && dateToFormat.month == today.month && dateToFormat.year == today.year) {
stringProvider.getString(R.string.common_date_this_month)
} else {
dateFormatters.formatDateWithMonthAndYear(dateToFormat)
}
.safeCapitalize()
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
@ -7,18 +7,16 @@
package io.element.android.libraries.dateformatter.impl
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultLastMessageTimestampFormatter @Inject constructor(
class DateFormatterTime @Inject constructor(
private val localDateTimeProvider: LocalDateTimeProvider,
private val dateFormatters: DateFormatters,
) : LastMessageTimestampFormatter {
override fun format(timestamp: Long?): String {
if (timestamp == null) return ""
) {
fun format(
timestamp: Long,
useRelative: Boolean,
): String {
val currentDate = localDateTimeProvider.providesNow()
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
val isSameDay = currentDate.date == dateToFormat.date
@ -30,7 +28,7 @@ class DefaultLastMessageTimestampFormatter @Inject constructor(
dateFormatters.formatDate(
dateToFormat = dateToFormat,
currentDate = currentDate,
useRelative = true
useRelative = useRelative,
)
}
}

View file

@ -0,0 +1,22 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl
import javax.inject.Inject
class DateFormatterTimeOnly @Inject constructor(
private val localDateTimeProvider: LocalDateTimeProvider,
private val dateFormatters: DateFormatters,
) {
fun format(
timestamp: Long,
): String {
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
return dateFormatters.formatTime(dateToFormat)
}
}

View file

@ -7,57 +7,64 @@
package io.element.android.libraries.dateformatter.impl
import android.text.format.DateFormat
import android.text.format.DateUtils
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.SingleIn
import kotlinx.datetime.Clock
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.toInstant
import kotlinx.datetime.toJavaLocalDate
import kotlinx.datetime.toJavaLocalDateTime
import timber.log.Timber
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
@SingleIn(AppScope::class)
class DateFormatters @Inject constructor(
private val locale: Locale,
localeChangeObserver: LocaleChangeObserver,
private val clock: Clock,
private val timeZoneProvider: TimezoneProvider,
) {
private val onlyTimeFormatter: DateTimeFormatter by lazy {
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale)
locale: Locale,
) : LocaleChangeListener {
init {
localeChangeObserver.addListener(this)
}
private val dateWithMonthFormatter: DateTimeFormatter by lazy {
val pattern = DateFormat.getBestDateTimePattern(locale, "d MMM") ?: "d MMM"
DateTimeFormatter.ofPattern(pattern, locale)
}
private var dateTimeFormatters: DateTimeFormatters = DateTimeFormatters(locale)
private val dateWithYearFormatter: DateTimeFormatter by lazy {
val pattern = DateFormat.getBestDateTimePattern(locale, "dd.MM.yyyy") ?: "dd.MM.yyyy"
DateTimeFormatter.ofPattern(pattern, locale)
}
private val dateWithFullFormatFormatter: DateTimeFormatter by lazy {
DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(locale)
override fun onLocaleChange() {
Timber.w("Locale changed, updating formatters")
dateTimeFormatters = DateTimeFormatters(Locale.getDefault())
}
internal fun formatTime(localDateTime: LocalDateTime): String {
return onlyTimeFormatter.format(localDateTime.toJavaLocalDateTime())
return dateTimeFormatters.onlyTimeFormatter.format(localDateTime.toJavaLocalDateTime())
}
internal fun formatDateWithMonthAndYear(localDateTime: LocalDateTime): String {
return dateTimeFormatters.dateWithMonthAndYearFormatter.format(localDateTime.toJavaLocalDateTime())
}
internal fun formatDateWithMonth(localDateTime: LocalDateTime): String {
return dateWithMonthFormatter.format(localDateTime.toJavaLocalDateTime())
return dateTimeFormatters.dateWithMonthFormatter.format(localDateTime.toJavaLocalDateTime())
}
internal fun formatDateWithDay(localDateTime: LocalDateTime): String {
return dateTimeFormatters.dateWithDayFormatter.format(localDateTime.toJavaLocalDateTime())
}
internal fun formatDateWithYear(localDateTime: LocalDateTime): String {
return dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime())
return dateTimeFormatters.dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime())
}
internal fun formatDateWithFullFormat(localDateTime: LocalDateTime): String {
return dateWithFullFormatFormatter.format(localDateTime.toJavaLocalDateTime())
return dateTimeFormatters.dateWithFullFormatFormatter.format(localDateTime.toJavaLocalDateTime())
}
internal fun formatDateWithFullFormatNoYear(localDateTime: LocalDateTime): String {
return dateTimeFormatters.dateWithFullFormatNoYearFormatter.format(localDateTime.toJavaLocalDateTime())
}
internal fun formatDate(
@ -75,12 +82,12 @@ class DateFormatters @Inject constructor(
}
}
private fun getRelativeDay(ts: Long): String {
internal fun getRelativeDay(ts: Long, default: String = ""): String {
return DateUtils.getRelativeTimeSpanString(
ts,
clock.now().toEpochMilliseconds(),
DateUtils.DAY_IN_MILLIS,
DateUtils.FORMAT_SHOW_WEEKDAY
)?.toString() ?: ""
)?.toString() ?: default
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl
import android.text.format.DateFormat
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale
class DateTimeFormatters(
private val locale: Locale,
) {
val onlyTimeFormatter: DateTimeFormatter by lazy {
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale)
}
val dateWithMonthAndYearFormatter: DateTimeFormatter by lazy {
val pattern = bestDateTimePattern("MMMM YYYY")
DateTimeFormatter.ofPattern(pattern, locale)
}
val dateWithMonthFormatter: DateTimeFormatter by lazy {
val pattern = bestDateTimePattern("d MMM")
DateTimeFormatter.ofPattern(pattern, locale)
}
val dateWithDayFormatter: DateTimeFormatter by lazy {
val pattern = bestDateTimePattern("EEEE")
DateTimeFormatter.ofPattern(pattern, locale)
}
val dateWithYearFormatter: DateTimeFormatter by lazy {
val pattern = bestDateTimePattern("dd.MM.yyyy")
DateTimeFormatter.ofPattern(pattern, locale)
}
val dateWithFullFormatFormatter: DateTimeFormatter by lazy {
DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).withLocale(locale)
}
val dateWithFullFormatNoYearFormatter: DateTimeFormatter by lazy {
val pattern = DateFormat.getBestDateTimePattern(locale, "EEEE d MMMM") ?: "EEEE d MMMM"
DateTimeFormatter.ofPattern(pattern, locale)
}
private fun bestDateTimePattern(pattern: String): String {
return DateFormat.getBestDateTimePattern(locale, pattern) ?: pattern
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultDateFormatter @Inject constructor(
private val dateFormatterFull: DateFormatterFull,
private val dateFormatterMonth: DateFormatterMonth,
private val dateFormatterDay: DateFormatterDay,
private val dateFormatterTime: DateFormatterTime,
private val dateFormatterTimeOnly: DateFormatterTimeOnly,
) : DateFormatter {
override fun format(
timestamp: Long?,
mode: DateFormatterMode,
useRelative: Boolean,
): String {
timestamp ?: return ""
return when (mode) {
DateFormatterMode.Full -> {
dateFormatterFull.format(timestamp, useRelative)
}
DateFormatterMode.Month -> {
dateFormatterMonth.format(timestamp, useRelative)
}
DateFormatterMode.Day -> {
dateFormatterDay.format(timestamp, useRelative)
}
DateFormatterMode.TimeOrDate -> {
dateFormatterTime.format(timestamp, useRelative)
}
DateFormatterMode.TimeOnly -> {
dateFormatterTimeOnly.format(timestamp)
}
}
}
}

View file

@ -1,25 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.dateformatter.api.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)
// TODO use relative formatting once iOS uses it too
return dateFormatters.formatDateWithFullFormat(dateToFormat)
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.AppScope
import io.element.android.libraries.di.ApplicationContext
import io.element.android.libraries.di.SingleIn
import javax.inject.Inject
fun interface LocaleChangeObserver {
fun addListener(listener: LocaleChangeListener)
}
interface LocaleChangeListener {
fun onLocaleChange()
}
@SingleIn(AppScope::class)
@ContributesBinding(AppScope::class)
class DefaultLocaleChangeObserver @Inject constructor(
@ApplicationContext private val context: Context,
) : LocaleChangeObserver {
init {
registerReceiver(object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
listeners.forEach(LocaleChangeListener::onLocaleChange)
}
})
}
private val listeners = mutableSetOf<LocaleChangeListener>()
override fun addListener(listener: LocaleChangeListener) {
listeners.add(listener)
}
private fun registerReceiver(receiver: BroadcastReceiver) {
val filter = IntentFilter()
filter.addAction(Intent.ACTION_LOCALE_CHANGED)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
filter.addAction(Intent.ACTION_APPLICATION_LOCALE_CHANGED)
}
context.registerReceiver(receiver, filter)
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl.previews
data class DateForPreview(
val semantic: String,
val date: String,
)
val dateForPreviewToday = DateForPreview(
semantic = "Today",
date = "1980-04-06T18:35:24.00Z",
)
val dateForPreviews = listOf(
DateForPreview(
semantic = "Now",
date = dateForPreviewToday.date,
),
DateForPreview(
semantic = "One second ago",
date = "1980-04-06T18:35:23.00Z",
),
DateForPreview(
semantic = "One minute ago",
date = "1980-04-06T18:34:24.00Z",
),
DateForPreview(
semantic = "One hour ago",
date = "1980-04-06T17:35:24.00Z",
),
DateForPreview(
semantic = "One day ago",
date = "1980-04-05T18:35:24.00Z",
),
DateForPreview(
semantic = "Two days ago",
date = "1980-04-04T18:35:24.00Z",
),
DateForPreview(
semantic = "One month ago",
date = "1980-03-06T18:35:24.00Z",
),
DateForPreview(
semantic = "One year ago",
date = "1979-04-06T18:35:24.00Z",
),
)

View file

@ -0,0 +1,16 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl.previews
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.dateformatter.api.DateFormatterMode
class DateFormatterModeProvider : PreviewParameterProvider<DateFormatterMode> {
override val values: Sequence<DateFormatterMode>
get() = DateFormatterMode.entries.asSequence()
}

View file

@ -0,0 +1,124 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl.previews
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.dateformatter.impl.DefaultDateFormatter
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.utils.allBooleans
import kotlinx.datetime.Instant
@Preview
@Composable
internal fun DateFormatterModeViewPreview(
@PreviewParameter(DateFormatterModeProvider::class) dateFormatterMode: DateFormatterMode,
) = ElementPreview {
DateFormatterModeView(dateFormatterMode)
}
@Composable
private fun DateFormatterModeView(
mode: DateFormatterMode,
) {
val context = LocalContext.current
val composeLocale = Locale.current
val dateFormatter = remember {
createFormatter(
context = context,
currentDate = dateForPreviewToday.date,
locale = java.util.Locale.Builder()
.setLanguageTag(composeLocale.toLanguageTag())
.build(),
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = "Mode $mode / $composeLocale",
style = ElementTheme.typography.fontHeadingSmMedium
)
val today = Instant.parse(dateForPreviewToday.date).toEpochMilliseconds()
Text(
text = "Today is: ${dateFormatter.format(today, DateFormatterMode.Full, useRelative = false)}",
style = ElementTheme.typography.fontHeadingSmMedium,
)
dateForPreviews.forEach { dateForPreview ->
DateForPreviewItem(
dateForPreview = dateForPreview,
dateFormatter = dateFormatter,
mode = mode,
)
}
}
}
@Composable
private fun DateForPreviewItem(
dateForPreview: DateForPreview,
dateFormatter: DefaultDateFormatter,
mode: DateFormatterMode,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(2.dp),
) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(start = 8.dp),
text = dateForPreview.semantic,
style = ElementTheme.typography.fontBodyMdMedium,
color = ElementTheme.colors.textSecondary,
)
val ts = Instant.parse(dateForPreview.date).toEpochMilliseconds()
Row {
Column {
listOf("Absolute:", "Relative:").forEach { label ->
Text(
text = label,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textPrimary,
)
}
}
Spacer(modifier = Modifier.width(8.dp))
Column {
allBooleans.forEach { useRelative ->
Text(
modifier = Modifier.fillMaxWidth(),
text = dateFormatter.format(ts, mode, useRelative),
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textPrimary,
)
}
}
}
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl.previews
import android.content.Context
import io.element.android.libraries.dateformatter.impl.DateFormatterFull
import io.element.android.libraries.dateformatter.impl.DateFormatterMonth
import io.element.android.libraries.dateformatter.impl.DateFormatterTime
import io.element.android.libraries.dateformatter.impl.DateFormatterTimeOnly
import io.element.android.libraries.dateformatter.impl.DateFormatters
import io.element.android.libraries.dateformatter.impl.DefaultDateFormatter
import io.element.android.libraries.dateformatter.impl.DefaultDateFormatterDay
import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import java.util.Locale
/**
* Create DefaultDateFormatter and set current time to the provided date.
*/
fun createFormatter(
context: Context,
currentDate: String,
locale: Locale,
): DefaultDateFormatter {
val clock = PreviewClock().apply { givenInstant(Instant.parse(currentDate)) }
val localDateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.UTC }
val dateFormatters = DateFormatters(
localeChangeObserver = {},
clock = clock,
timeZoneProvider = { TimeZone.UTC },
locale = locale,
)
val stringProvider = PreviewStringProvider(context.resources)
val dateFormatterDay = DefaultDateFormatterDay(
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
)
return DefaultDateFormatter(
dateFormatterFull = DateFormatterFull(
stringProvider = stringProvider,
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
dateFormatterDay = dateFormatterDay,
),
dateFormatterMonth = DateFormatterMonth(
stringProvider = stringProvider,
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
),
dateFormatterDay = dateFormatterDay,
dateFormatterTime = DateFormatterTime(
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
),
dateFormatterTimeOnly = DateFormatterTimeOnly(
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
),
)
}

View file

@ -0,0 +1,21 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl.previews
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
class PreviewClock : Clock {
private var instant: Instant = Instant.fromEpochMilliseconds(0)
fun givenInstant(instant: Instant) {
this.instant = instant
}
override fun now(): Instant = instant
}

View file

@ -0,0 +1,29 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl.previews
import android.content.res.Resources
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import io.element.android.services.toolbox.api.strings.StringProvider
class PreviewStringProvider(
private val resources: Resources
) : StringProvider {
override fun getString(@StringRes resId: Int): String {
return resources.getString(resId)
}
override fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String {
return resources.getString(resId, *formatArgs)
}
override fun getQuantityString(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any?): String {
return resources.getQuantityString(resId, quantity, *formatArgs)
}
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="common_date_date_at_time">"%1$s à %2$s"</string>
<string name="common_date_this_month">"Ce mois-ci"</string>
</resources>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="common_date_date_at_time">"%1$s at %2$s"</string>
<string name="common_date_this_month">"This month"</string>
</resources>

View file

@ -0,0 +1,260 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import kotlinx.datetime.Instant
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
@Config(qualifiers = "fr")
class DefaultDateFormatterFrTest {
@Test
fun `test null`() {
val now = "1980-04-06T18:35:24.00Z"
val ts: Long? = null
val formatter = createFormatter(now)
assertThat(formatter.format(ts)).isEmpty()
}
@Test
fun `test epoch`() {
val now = "1980-04-06T18:35:24.00Z"
val ts = 0L
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("1 janvier 1970 à 00:00")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Janvier 1970")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("1 janvier 1970")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("01.01.1970")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("00:00")
}
@Test
fun `test epoch relative`() {
val now = "1980-04-06T18:35:24.00Z"
val ts = 0L
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("1 janvier 1970 à 00:00")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Janvier 1970")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("1 janvier 1970")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("01.01.1970")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("00:00")
}
@Test
fun `test now`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1980 à 18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Dimanche 6 avril")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("18:35")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
}
@Test
fun `test now relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Aujourdhui")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("18:35")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
}
@Test
fun `test one second before`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:35:23.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1980 à 18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Dimanche 6 avril")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("18:35")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
}
@Test
fun `test one second before relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:35:23.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Aujourdhui")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("18:35")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
}
@Test
fun `test one minute before`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:34:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1980 à 18:34")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Dimanche 6 avril")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("18:34")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:34")
}
@Test
fun `test one minute before relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:34:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("18:34")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Aujourdhui")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("18:34")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:34")
}
@Test
fun `test one hour before`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T17:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1980 à 17:35")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Dimanche 6 avril")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("17:35")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("17:35")
}
@Test
fun `test one hour before relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T17:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("17:35")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Aujourdhui")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("17:35")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("17:35")
}
@Test
fun `test one day before same time`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-05T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("5 avril 1980 à 18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Samedi 5 avril")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("5 avr.")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
}
@Test
fun `test one day before same time relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-05T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Hier à 18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Hier")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("Hier")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
}
@Test
fun `test two days before same time`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-04T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("4 avril 1980 à 18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Vendredi 4 avril")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("4 avr.")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
}
@Test
fun `test two days before same time relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-04T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Vendredi à 18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Vendredi")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("4 avr.")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
}
@Test
fun `test one month before same time`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-03-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 mars 1980 à 18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Mars 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Jeudi 6 mars")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6 mars")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
}
@Test
fun `test one month before same time relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-03-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Jeudi 6 mars à 18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Mars 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Jeudi 6 mars")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6 mars")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
}
@Test
fun `test one year before same time`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1979-04-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1979 à 18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1979")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("6 avril 1979")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("06.04.1979")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
}
@Test
fun `test one year before same time relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1979-04-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("6 avril 1979 à 18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Avril 1979")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("6 avril 1979")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("06.04.1979")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
}
}

View file

@ -0,0 +1,260 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import kotlinx.datetime.Instant
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
@Config(qualifiers = "en")
class DefaultDateFormatterTest {
@Test
fun `test null`() {
val now = "1980-04-06T18:35:24.00Z"
val ts: Long? = null
val formatter = createFormatter(now)
assertThat(formatter.format(ts)).isEmpty()
}
@Test
fun `test epoch`() {
val now = "1980-04-06T18:35:24.00Z"
val ts = 0L
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("January 1, 1970 at 12:00AM")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("January 1970")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("January 1, 1970")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("01.01.1970")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("12:00AM")
}
@Test
fun `test epoch relative`() {
val now = "1980-04-06T18:35:24.00Z"
val ts = 0L
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("January 1, 1970 at 12:00AM")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("January 1970")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("January 1, 1970")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("01.01.1970")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("12:00AM")
}
@Test
fun `test now`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35PM")
}
@Test
fun `test now relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Today")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35PM")
}
@Test
fun `test one second before`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:35:23.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35PM")
}
@Test
fun `test one second before relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:35:23.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Today")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35PM")
}
@Test
fun `test one minute before`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:34:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 6:34PM")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6:34PM")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:34PM")
}
@Test
fun `test one minute before relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:34:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("6:34PM")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Today")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6:34PM")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:34PM")
}
@Test
fun `test one hour before`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T17:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 5:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("5:35PM")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("5:35PM")
}
@Test
fun `test one hour before relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T17:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("5:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Today")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("5:35PM")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("5:35PM")
}
@Test
fun `test one day before same time`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-05T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 5, 1980 at 6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Saturday 5 April")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("5 Apr")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35PM")
}
@Test
fun `test one day before same time relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-05T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Yesterday at 6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Yesterday")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("Yesterday")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35PM")
}
@Test
fun `test two days before same time`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-04T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 4, 1980 at 6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Friday 4 April")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("4 Apr")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35PM")
}
@Test
fun `test two days before same time relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-04T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Friday at 6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Friday")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("4 Apr")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35PM")
}
@Test
fun `test one month before same time`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-03-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("March 6, 1980 at 6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("March 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Thursday 6 March")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6 Mar")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35PM")
}
@Test
fun `test one month before same time relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-03-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Thursday 6 March at 6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("March 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Thursday 6 March")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6 Mar")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35PM")
}
@Test
fun `test one year before same time`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1979-04-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1979 at 6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1979")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("April 6, 1979")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("06.04.1979")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35PM")
}
@Test
fun `test one year before same time relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1979-04-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("April 6, 1979 at 6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("April 1979")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("April 6, 1979")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("06.04.1979")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35PM")
}
}

View file

@ -1,109 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
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
class DefaultLastMessageTimestampFormatterTest {
@Test
fun `test null`() {
val now = "1980-04-06T18:35:24.00Z"
val formatter = createFormatter(now)
assertThat(formatter.format(null)).isEmpty()
}
@Test
fun `test epoch`() {
val now = "1980-04-06T18:35:24.00Z"
val formatter = createFormatter(now)
assertThat(formatter.format(0)).isEqualTo("01.01.1970")
}
@Test
fun `test now`() {
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("6:35PM")
}
@Test
fun `test one second before`() {
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("6:35PM")
}
@Test
fun `test one minute before`() {
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("6:34PM")
}
@Test
fun `test one hour before`() {
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("5:35PM")
}
@Test
fun `test one day before same time`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-05T18:35:24.00Z"
val formatter = createFormatter(now)
// TODO DateUtils.getRelativeTimeSpanString returns null.
assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("")
}
@Test
fun `test one month before same time`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-03-06T18:35:24.00Z"
val formatter = createFormatter(now)
assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("6 Mar")
}
@Test
fun `test one year before same time`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1979-04-06T18:35:24.00Z"
val formatter = createFormatter(now)
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.
*/
private fun createFormatter(@Suppress("SameParameterValue") currentDate: String): LastMessageTimestampFormatter {
val clock = FakeClock().apply { givenInstant(Instant.parse(currentDate)) }
val localDateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.UTC }
val dateFormatters = DateFormatters(Locale.US, clock) { TimeZone.UTC }
return DefaultLastMessageTimestampFormatter(localDateTimeProvider, dateFormatters)
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl
import io.element.android.tests.testutils.InstrumentationStringProvider
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import java.util.Locale
/**
* Create DefaultDateFormatter and set current time to the provided date.
*/
fun createFormatter(currentDate: String): DefaultDateFormatter {
val clock = FakeClock().apply { givenInstant(Instant.parse(currentDate)) }
val localDateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.UTC }
val dateFormatters = DateFormatters(
localeChangeObserver = {},
clock = clock,
timeZoneProvider = { TimeZone.UTC },
locale = Locale.getDefault(),
)
val stringProvider = InstrumentationStringProvider()
val dateFormatterDay = DefaultDateFormatterDay(
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
)
return DefaultDateFormatter(
dateFormatterFull = DateFormatterFull(
stringProvider = stringProvider,
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
dateFormatterDay = dateFormatterDay,
),
dateFormatterMonth = DateFormatterMonth(
stringProvider = stringProvider,
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
),
dateFormatterDay = dateFormatterDay,
dateFormatterTime = DateFormatterTime(
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
),
dateFormatterTimeOnly = DateFormatterTimeOnly(
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
),
)
}

View file

@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.test
package io.element.android.libraries.dateformatter.impl
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant

View file

@ -0,0 +1,25 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.test
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
class FakeDateFormatter(
private val formatLambda: (Long?, DateFormatterMode, Boolean) -> String = { timestamp, mode, useRelative ->
"$timestamp $mode $useRelative"
},
) : DateFormatter {
override fun format(
timestamp: Long?,
mode: DateFormatterMode,
useRelative: Boolean,
): String {
return formatLambda(timestamp, mode, useRelative)
}
}

View file

@ -1,22 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.test
import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
class FakeDaySeparatorFormatter : DaySeparatorFormatter {
private var format = ""
fun givenFormat(format: String) {
this.format = format
}
override fun format(timestamp: Long): String {
return format
}
}

View file

@ -1,24 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.test
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
const val A_FORMATTED_DATE = "formatted_date"
class FakeLastMessageTimestampFormatter(
var format: String = "",
) : LastMessageTimestampFormatter {
fun givenFormat(format: String) {
this.format = format
}
override fun format(timestamp: Long?): String {
return format
}
}

View file

@ -235,7 +235,7 @@ class RustMatrixRoom(
RoomMessageEventMessageType.VIDEO,
RoomMessageEventMessageType.AUDIO,
),
dateDividerMode = DateDividerMode.DAILY,
dateDividerMode = DateDividerMode.MONTHLY,
).let { inner ->
createTimeline(inner, mode = Timeline.Mode.MEDIA)
}

View file

@ -23,6 +23,7 @@ data class MediaInfo(
val senderName: String?,
val senderAvatar: String?,
val dateSent: String?,
val dateSentFull: String?,
) : Parcelable
fun anImageMediaInfo(
@ -30,6 +31,7 @@ fun anImageMediaInfo(
caption: String? = null,
senderName: String? = null,
dateSent: String? = null,
dateSentFull: String? = null,
): MediaInfo = MediaInfo(
filename = "an image file.jpg",
caption = caption,
@ -40,12 +42,14 @@ fun anImageMediaInfo(
senderName = senderName,
senderAvatar = null,
dateSent = dateSent,
dateSentFull = dateSentFull,
)
fun aVideoMediaInfo(
caption: String? = null,
senderName: String? = null,
dateSent: String? = null,
dateSentFull: String? = null,
): MediaInfo = MediaInfo(
filename = "a video file.mp4",
caption = caption,
@ -56,6 +60,7 @@ fun aVideoMediaInfo(
senderName = senderName,
senderAvatar = null,
dateSent = dateSent,
dateSentFull = dateSentFull,
)
fun aPdfMediaInfo(
@ -63,6 +68,7 @@ fun aPdfMediaInfo(
caption: String? = null,
senderName: String? = null,
dateSent: String? = null,
dateSentFull: String? = null,
): MediaInfo = MediaInfo(
filename = filename,
caption = caption,
@ -73,12 +79,14 @@ fun aPdfMediaInfo(
senderName = senderName,
senderAvatar = null,
dateSent = dateSent,
dateSentFull = dateSentFull,
)
fun anApkMediaInfo(
senderId: UserId? = UserId("@alice:server.org"),
senderName: String? = null,
dateSent: String? = null,
dateSentFull: String? = null,
): MediaInfo = MediaInfo(
filename = "an apk file.apk",
caption = null,
@ -89,11 +97,13 @@ fun anApkMediaInfo(
senderName = senderName,
senderAvatar = null,
dateSent = dateSent,
dateSentFull = dateSentFull,
)
fun anAudioMediaInfo(
senderName: String? = null,
dateSent: String? = null,
dateSentFull: String? = null,
): MediaInfo = MediaInfo(
filename = "an audio file.mp3",
caption = null,
@ -104,4 +114,5 @@ fun anAudioMediaInfo(
senderName = senderName,
senderAvatar = null,
dateSent = dateSent,
dateSentFull = dateSentFull,
)

View file

@ -53,6 +53,7 @@ class DefaultMediaViewerEntryPoint @Inject constructor() : MediaViewerEntryPoint
senderName = null,
senderAvatar = null,
dateSent = null,
dateSentFull = null,
),
mediaSource = MediaSource(url = avatarUrl),
thumbnailSource = null,

View file

@ -71,7 +71,7 @@ fun MediaDetailsBottomSheet(
}
SectionText(
title = stringResource(R.string.screen_media_details_uploaded_on),
text = state.mediaInfo.dateSent.orEmpty(),
text = state.mediaInfo.dateSentFull.orEmpty(),
)
SectionText(
title = stringResource(R.string.screen_media_details_filename),

View file

@ -10,12 +10,15 @@ package io.element.android.libraries.mediaviewer.impl.details
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
fun aMediaDetailsBottomSheetState(): MediaBottomSheetState.MediaDetailsBottomSheetState {
fun aMediaDetailsBottomSheetState(
dateSentFull: String = "December 6, 2024 at 12:59",
): MediaBottomSheetState.MediaDetailsBottomSheetState {
return MediaBottomSheetState.MediaDetailsBottomSheetState(
eventId = EventId("\$eventId"),
canDelete = true,
mediaInfo = anImageMediaInfo(
senderName = "Alice",
dateSentFull = dateSentFull,
),
thumbnailSource = null,
)

View file

@ -8,7 +8,8 @@
package io.element.android.libraries.mediaviewer.impl.gallery
import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.dateformatter.api.toHumanReadableDuration
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
@ -45,13 +46,20 @@ import javax.inject.Inject
class EventItemFactory @Inject constructor(
private val fileSizeFormatter: FileSizeFormatter,
private val fileExtensionExtractor: FileExtensionExtractor,
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
private val dateFormatter: DateFormatter,
) {
fun create(
currentTimelineItem: MatrixTimelineItem.Event,
): MediaItem.Event? {
val event = currentTimelineItem.event
val sentTime = lastMessageTimestampFormatter.format(currentTimelineItem.event.timestamp)
val dateSent = dateFormatter.format(
currentTimelineItem.event.timestamp,
mode = DateFormatterMode.Day,
)
val dateSentFull = dateFormatter.format(
timestamp = currentTimelineItem.event.timestamp,
mode = DateFormatterMode.Full,
)
return when (val content = event.content) {
CallNotifyContent,
is FailedToParseMessageLikeContent,
@ -90,7 +98,8 @@ class EventItemFactory @Inject constructor(
senderId = event.sender,
senderName = event.senderProfile.getDisambiguatedDisplayName(event.sender),
senderAvatar = event.senderProfile.getAvatarUrl(),
dateSent = sentTime,
dateSent = dateSent,
dateSentFull = dateSentFull,
),
mediaSource = type.source,
)
@ -106,7 +115,8 @@ class EventItemFactory @Inject constructor(
senderId = event.sender,
senderName = event.senderProfile.getDisambiguatedDisplayName(event.sender),
senderAvatar = event.senderProfile.getAvatarUrl(),
dateSent = sentTime,
dateSent = dateSent,
dateSentFull = dateSentFull,
),
mediaSource = type.source,
)
@ -122,7 +132,8 @@ class EventItemFactory @Inject constructor(
senderId = event.sender,
senderName = event.senderProfile.getDisambiguatedDisplayName(event.sender),
senderAvatar = event.senderProfile.getAvatarUrl(),
dateSent = sentTime,
dateSent = dateSent,
dateSentFull = dateSentFull,
),
mediaSource = type.source,
thumbnailSource = null,
@ -139,7 +150,8 @@ class EventItemFactory @Inject constructor(
senderId = event.sender,
senderName = event.senderProfile.getDisambiguatedDisplayName(event.sender),
senderAvatar = event.senderProfile.getAvatarUrl(),
dateSent = sentTime,
dateSent = dateSent,
dateSentFull = dateSentFull,
),
mediaSource = type.source,
thumbnailSource = null,
@ -156,7 +168,8 @@ class EventItemFactory @Inject constructor(
senderId = event.sender,
senderName = event.senderProfile.getDisambiguatedDisplayName(event.sender),
senderAvatar = event.senderProfile.getAvatarUrl(),
dateSent = sentTime,
dateSent = dateSent,
dateSentFull = dateSentFull,
),
mediaSource = type.source,
thumbnailSource = type.info?.thumbnailSource,
@ -174,7 +187,8 @@ class EventItemFactory @Inject constructor(
senderId = event.sender,
senderName = event.senderProfile.getDisambiguatedDisplayName(event.sender),
senderAvatar = event.senderProfile.getAvatarUrl(),
dateSent = sentTime,
dateSent = dateSent,
dateSentFull = dateSentFull,
),
mediaSource = type.source,
)

View file

@ -7,19 +7,24 @@
package io.element.android.libraries.mediaviewer.impl.gallery
import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import javax.inject.Inject
class VirtualItemFactory @Inject constructor(
private val daySeparatorFormatter: DaySeparatorFormatter,
private val dateFormatter: DateFormatter,
) {
fun create(timelineItem: MatrixTimelineItem.Virtual): MediaItem? {
return when (val virtual = timelineItem.virtual) {
is VirtualTimelineItem.DayDivider -> MediaItem.DateSeparator(
id = timelineItem.uniqueId,
formattedDate = daySeparatorFormatter.format(virtual.timestamp)
formattedDate = dateFormatter.format(
timestamp = virtual.timestamp,
mode = DateFormatterMode.Month,
useRelative = true,
)
)
VirtualTimelineItem.LastForwardIndicator -> null
is VirtualTimelineItem.LoadingIndicator -> MediaItem.LoadingIndicator(

View file

@ -46,6 +46,7 @@ class AndroidLocalMediaFactory @Inject constructor(
senderName = mediaInfo.senderName,
senderAvatar = mediaInfo.senderAvatar,
dateSent = mediaInfo.dateSent,
dateSentFull = mediaInfo.dateSentFull,
)
override fun createFromUri(
@ -63,6 +64,7 @@ class AndroidLocalMediaFactory @Inject constructor(
senderName = null,
senderAvatar = null,
dateSent = null,
dateSentFull = null,
)
private fun createFromUri(
@ -75,6 +77,7 @@ class AndroidLocalMediaFactory @Inject constructor(
senderName: String?,
senderAvatar: String?,
dateSent: String?,
dateSentFull: String?,
): LocalMedia {
val resolvedMimeType = mimeType ?: context.getMimeType(uri) ?: MimeTypes.OctetStream
val fileName = name ?: context.getFileName(uri) ?: ""
@ -92,6 +95,7 @@ class AndroidLocalMediaFactory @Inject constructor(
senderName = senderName,
senderAvatar = senderAvatar,
dateSent = dateSent,
dateSentFull = dateSentFull,
)
)
}

View file

@ -10,8 +10,7 @@ package io.element.android.libraries.mediaviewer.impl.gallery
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.matrix.api.media.AudioDetails
import io.element.android.libraries.matrix.api.media.AudioInfo
import io.element.android.libraries.matrix.api.media.FileInfo
@ -162,7 +161,8 @@ class DefaultEventItemFactoryTest {
senderId = A_USER_ID,
senderName = "alice",
senderAvatar = null,
dateSent = A_FORMATTED_DATE,
dateSent = "0 Day false",
dateSentFull = "0 Full false",
),
mediaSource = MediaSource(""),
)
@ -209,7 +209,8 @@ class DefaultEventItemFactoryTest {
senderId = A_USER_ID,
senderName = "alice",
senderAvatar = null,
dateSent = A_FORMATTED_DATE,
dateSent = "0 Day false",
dateSentFull = "0 Full false",
),
mediaSource = MediaSource(""),
thumbnailSource = null,
@ -253,7 +254,8 @@ class DefaultEventItemFactoryTest {
senderId = A_USER_ID,
senderName = "alice",
senderAvatar = null,
dateSent = A_FORMATTED_DATE,
dateSent = "0 Day false",
dateSentFull = "0 Full false",
),
mediaSource = MediaSource(""),
)
@ -301,7 +303,8 @@ class DefaultEventItemFactoryTest {
senderId = A_USER_ID,
senderName = "alice",
senderAvatar = null,
dateSent = A_FORMATTED_DATE,
dateSent = "0 Day false",
dateSentFull = "0 Full false",
),
mediaSource = MediaSource(""),
thumbnailSource = null,
@ -350,7 +353,8 @@ class DefaultEventItemFactoryTest {
senderId = A_USER_ID,
senderName = "alice",
senderAvatar = null,
dateSent = A_FORMATTED_DATE,
dateSent = "0 Day false",
dateSentFull = "0 Full false",
),
mediaSource = MediaSource(""),
)
@ -397,7 +401,8 @@ class DefaultEventItemFactoryTest {
senderId = A_USER_ID,
senderName = "alice",
senderAvatar = null,
dateSent = A_FORMATTED_DATE,
dateSent = "0 Day false",
dateSentFull = "0 Full false",
),
mediaSource = MediaSource(""),
thumbnailSource = null,
@ -409,5 +414,5 @@ class DefaultEventItemFactoryTest {
private fun createEventItemFactory() = EventItemFactory(
fileSizeFormatter = FakeFileSizeFormatter(),
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(A_FORMATTED_DATE),
dateFormatter = FakeDateFormatter(),
)

View file

@ -10,9 +10,7 @@ package io.element.android.libraries.mediaviewer.impl.gallery
import android.net.Uri
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
@ -254,12 +252,12 @@ class MediaGalleryPresenterTest {
timelineMediaItemsFactory = TimelineMediaItemsFactory(
dispatchers = testCoroutineDispatchers(),
virtualItemFactory = VirtualItemFactory(
daySeparatorFormatter = FakeDaySeparatorFormatter(),
dateFormatter = FakeDateFormatter(),
),
eventItemFactory = EventItemFactory(
fileSizeFormatter = FakeFileSizeFormatter(),
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(A_FORMATTED_DATE),
dateFormatter = FakeDateFormatter(),
),
),
localMediaFactory = localMediaFactory,

View file

@ -27,11 +27,15 @@ class AndroidLocalMediaFactoryTest {
@Test
fun `test AndroidLocalMediaFactory`() {
val sut = createAndroidLocalMediaFactory()
val result = sut.createFromMediaFile(aMediaFile(), anImageMediaInfo(
senderId = A_USER_ID,
senderName = A_USER_NAME,
dateSent = "12:34",
))
val result = sut.createFromMediaFile(
mediaFile = aMediaFile(),
mediaInfo = anImageMediaInfo(
senderId = A_USER_ID,
senderName = A_USER_NAME,
dateSent = "12:34",
dateSentFull = "full",
)
)
assertThat(result.uri.toString()).endsWith("aPath")
assertThat(result.info).isEqualTo(
MediaInfo(
@ -43,7 +47,8 @@ class AndroidLocalMediaFactoryTest {
senderId = A_USER_ID,
senderName = A_USER_NAME,
senderAvatar = null,
dateSent = "12:34"
dateSent = "12:34",
dateSentFull = "full"
)
)
}

View file

@ -40,7 +40,8 @@ class FakeLocalMediaFactory(
senderId = null,
senderName = null,
senderAvatar = null,
dateSent = null
dateSent = null,
dateSentFull = null,
)
return aLocalMedia(uri, mediaInfo)
}