From 4188d58b56faa9507ad763a2de78d53a0e1682b8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Dec 2024 23:43:20 +0100 Subject: [PATCH 01/10] Implement month separator for the Gallery. Improve day separator rendering in the timeline. Use Today, Yesterday, and the name of the day if less than 7 days and do not render the year for the current year. Improve date format for the media viewer. Rework how date and time are computed. ActionListView: Time can take more space, so update the layout. --- .../messages/impl/MessagesFlowNode.kt | 12 +- .../impl/actionlist/ActionListPresenter.kt | 8 + .../impl/actionlist/ActionListState.kt | 1 + .../actionlist/ActionListStateProvider.kt | 11 + .../impl/actionlist/ActionListView.kt | 34 ++- .../event/TimelineItemEventFactory.kt | 25 +- .../TimelineItemDaySeparatorFactory.kt | 13 +- .../impl/timeline/model/TimelineItem.kt | 1 + .../messages/impl/MessagesViewTest.kt | 3 + .../actionlist/ActionListPresenterTest.kt | 28 +- .../fixtures/TimelineItemsFactoryFixtures.kt | 7 +- .../history/model/PollHistoryItemsFactory.kt | 11 +- .../impl/history/PollHistoryPresenterTest.kt | 4 +- .../datasource/RoomListRoomSummaryFactory.kt | 11 +- .../roomlist/impl/RoomListPresenterTest.kt | 12 +- .../impl/datasource/RoomListDataSourceTest.kt | 18 +- .../RoomListRoomSummaryFactoryTest.kt | 7 +- .../impl/model/RoomListRoomSummaryTest.kt | 4 +- .../search/RoomListSearchPresenterTest.kt | 4 +- .../incoming/IncomingVerificationPresenter.kt | 10 +- .../IncomingVerificationPresenterTest.kt | 15 +- .../core/extensions/BasicExtensions.kt | 15 + .../dateformatter/api/DateFormatter.kt | 26 ++ .../api/DaySeparatorFormatter.kt | 12 - .../api/LastMessageTimestampFormatter.kt | 12 - libraries/dateformatter/impl/build.gradle.kts | 14 + .../dateformatter/impl/DateFormatterDay.kt | 57 ++++ .../dateformatter/impl/DateFormatterFull.kt | 38 +++ .../dateformatter/impl/DateFormatterMonth.kt | 32 ++ ...stampFormatter.kt => DateFormatterTime.kt} | 18 +- .../impl/DateFormatterTimeOnly.kt | 22 ++ .../dateformatter/impl/DateFormatters.kt | 54 ++-- .../dateformatter/impl/DateTimeFormatters.kt | 54 ++++ .../impl/DefaultDateFormatter.kt | 48 +++ .../impl/DefaultDaySeparatorFormatter.kt | 25 -- .../impl/LocaleChangeObserver.kt | 56 ++++ .../impl/di/DateFormatterModule.kt | 4 - .../src/main/res/values-fr/translations.xml | 5 + .../impl/src/main/res/values/localazy.xml | 5 + .../impl/DefaultDateFormatterFrTest.kt | 277 ++++++++++++++++++ .../impl/DefaultDateFormatterTest.kt | 277 ++++++++++++++++++ ...efaultLastMessageTimestampFormatterTest.kt | 109 ------- .../dateformatter/test/FakeDateFormatter.kt | 25 ++ .../test/FakeDaySeparatorFormatter.kt | 22 -- .../test/FakeLastMessageTimestampFormatter.kt | 24 -- .../matrix/impl/room/RustMatrixRoom.kt | 2 +- .../libraries/mediaviewer/api/MediaInfo.kt | 11 + .../impl/DefaultMediaViewerEntryPoint.kt | 1 + .../impl/details/MediaDetailsBottomSheet.kt | 2 +- .../mediaviewer/impl/details/Preview.kt | 5 +- .../impl/gallery/EventItemFactory.kt | 32 +- .../impl/gallery/VirtualItemFactory.kt | 11 +- .../impl/local/AndroidLocalMediaFactory.kt | 4 + .../gallery/DefaultEventItemFactoryTest.kt | 23 +- .../impl/gallery/MediaGalleryPresenterTest.kt | 8 +- .../local/AndroidLocalMediaFactoryTest.kt | 17 +- .../mediaviewer/test/FakeLocalMediaFactory.kt | 3 +- tests/testutils/build.gradle.kts | 1 + .../InstrumentationStringProvider.kt | 26 ++ tools/localazy/config.json | 6 + 60 files changed, 1271 insertions(+), 351 deletions(-) create mode 100644 libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DateFormatter.kt delete mode 100644 libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DaySeparatorFormatter.kt delete mode 100644 libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/LastMessageTimestampFormatter.kt create mode 100644 libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterDay.kt create mode 100644 libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterFull.kt create mode 100644 libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterMonth.kt rename libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/{DefaultLastMessageTimestampFormatter.kt => DateFormatterTime.kt} (62%) create mode 100644 libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTimeOnly.kt create mode 100644 libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateTimeFormatters.kt create mode 100644 libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatter.kt delete mode 100644 libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDaySeparatorFormatter.kt create mode 100644 libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocaleChangeObserver.kt create mode 100644 libraries/dateformatter/impl/src/main/res/values-fr/translations.xml create mode 100644 libraries/dateformatter/impl/src/main/res/values/localazy.xml create mode 100644 libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt create mode 100644 libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt delete mode 100644 libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt create mode 100644 libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeDateFormatter.kt delete mode 100644 libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeDaySeparatorFormatter.kt delete mode 100644 libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeLastMessageTimestampFormatter.kt create mode 100644 tests/testutils/src/main/kotlin/io/element/android/tests/testutils/InstrumentationStringProvider.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 3b5fb67540..6754f5c683 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -55,6 +55,8 @@ import io.element.android.libraries.architecture.createNode import io.element.android.libraries.architecture.overlay.Overlay import io.element.android.libraries.architecture.overlay.operation.hide import io.element.android.libraries.architecture.overlay.operation.show +import io.element.android.libraries.dateformatter.api.DateFormatter +import io.element.android.libraries.dateformatter.api.DateFormatterMode import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.EventId @@ -97,6 +99,7 @@ class MessagesFlowNode @AssistedInject constructor( private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider, private val timelineController: TimelineController, private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint, + private val dateFormatter: DateFormatter, ) : BaseFlowNode( backstack = BackStack( initialElement = plugins.filterIsInstance().first().initialTarget.toNavTarget(), @@ -436,7 +439,14 @@ class MessagesFlowNode @AssistedInject constructor( senderId = event.senderId, senderName = event.safeSenderName, senderAvatar = event.senderAvatar.url, - dateSent = event.sentTime, + dateSent = dateFormatter.format( + event.sentTimeMillis, + mode = DateFormatterMode.Day, + ), + dateSentFull = dateFormatter.format( + timestamp = event.sentTimeMillis, + mode = DateFormatterMode.Full, + ), ), mediaSource = mediaSource, thumbnailSource = thumbnailSource, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index 411ff37c8f..95a6dc5a6f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -37,6 +37,8 @@ import io.element.android.features.messages.impl.timeline.model.event.canBeCopie import io.element.android.features.messages.impl.timeline.model.event.canBeForwarded import io.element.android.features.messages.impl.timeline.model.event.canReact import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.dateformatter.api.DateFormatter +import io.element.android.libraries.dateformatter.api.DateFormatterMode import io.element.android.libraries.di.RoomScope import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags @@ -64,6 +66,7 @@ class DefaultActionListPresenter @AssistedInject constructor( private val room: MatrixRoom, private val userSendFailureFactory: VerifiedUserSendFailureFactory, private val featureFlagService: FeatureFlagService, + private val dateFormatter: DateFormatter, ) : ActionListPresenter { @AssistedFactory @ContributesBinding(RoomScope::class) @@ -131,6 +134,11 @@ class DefaultActionListPresenter @AssistedInject constructor( if (actions.isNotEmpty() || displayEmojiReactions || verifiedUserSendFailure != VerifiedUserSendFailure.None) { target.value = ActionListState.Target.Success( event = timelineItem, + sentTimeFull = dateFormatter.format( + timelineItem.sentTimeMillis, + DateFormatterMode.Full, + useRelative = true, + ), displayEmojiReactions = displayEmojiReactions, verifiedUserSendFailure = verifiedUserSendFailure, actions = actions.toImmutableList() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt index 75c598df36..56bc1ca0bd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt @@ -24,6 +24,7 @@ data class ActionListState( data class Loading(val event: TimelineItem.Event) : Target data class Success( val event: TimelineItem.Event, + val sentTimeFull: String, val displayEmojiReactions: Boolean, val verifiedUserSendFailure: VerifiedUserSendFailure, val actions: ImmutableList, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt index a5f027a535..1638a03fa3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt @@ -37,6 +37,7 @@ open class ActionListStateProvider : PreviewParameterProvider { event = aTimelineItemEvent( timelineItemReactions = reactionsState ), + sentTimeFull = "January 1, 1970 at 12:00 AM", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), @@ -49,6 +50,7 @@ open class ActionListStateProvider : PreviewParameterProvider { displayNameAmbiguous = true, timelineItemReactions = reactionsState, ), + sentTimeFull = "January 1, 1970 at 12:00 AM", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList( @@ -62,6 +64,7 @@ open class ActionListStateProvider : PreviewParameterProvider { content = aTimelineItemVideoContent(), timelineItemReactions = reactionsState ), + sentTimeFull = "January 1, 1970 at 12:00 AM", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList( @@ -75,6 +78,7 @@ open class ActionListStateProvider : PreviewParameterProvider { content = aTimelineItemFileContent(), timelineItemReactions = reactionsState ), + sentTimeFull = "January 1, 1970 at 12:00 AM", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList( @@ -88,6 +92,7 @@ open class ActionListStateProvider : PreviewParameterProvider { content = aTimelineItemAudioContent(), timelineItemReactions = reactionsState ), + sentTimeFull = "January 1, 1970 at 12:00 AM", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList( @@ -101,6 +106,7 @@ open class ActionListStateProvider : PreviewParameterProvider { content = aTimelineItemVoiceContent(caption = null), timelineItemReactions = reactionsState ), + sentTimeFull = "January 1, 1970 at 12:00 AM", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList( @@ -114,6 +120,7 @@ open class ActionListStateProvider : PreviewParameterProvider { content = aTimelineItemLocationContent(), timelineItemReactions = reactionsState ), + sentTimeFull = "January 1, 1970 at 12:00 AM", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), @@ -125,6 +132,7 @@ open class ActionListStateProvider : PreviewParameterProvider { content = aTimelineItemLocationContent(), timelineItemReactions = reactionsState ), + sentTimeFull = "January 1, 1970 at 12:00 AM", displayEmojiReactions = false, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), @@ -136,6 +144,7 @@ open class ActionListStateProvider : PreviewParameterProvider { content = aTimelineItemPollContent(), timelineItemReactions = reactionsState ), + sentTimeFull = "January 1, 1970 at 12:00 AM", displayEmojiReactions = false, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemPollActionList(), @@ -147,6 +156,7 @@ open class ActionListStateProvider : PreviewParameterProvider { timelineItemReactions = reactionsState, messageShield = MessageShield.UnknownDevice(isCritical = true) ), + sentTimeFull = "January 1, 1970 at 12:00 AM", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = aTimelineItemActionList(), @@ -155,6 +165,7 @@ open class ActionListStateProvider : PreviewParameterProvider { anActionListState( target = ActionListState.Target.Success( event = aTimelineItemEvent(), + sentTimeFull = "January 1, 1970 at 12:00 AM", displayEmojiReactions = true, verifiedUserSendFailure = anUnsignedDeviceSendFailure(), actions = aTimelineItemActionList(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt index 7d30edd116..4cf0928d5c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt @@ -185,6 +185,7 @@ private fun ActionListViewContent( Column { MessageSummary( event = target.event, + sentTimeFull = target.sentTimeFull, modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp) @@ -245,7 +246,11 @@ private fun ActionListViewContent( @Suppress("MultipleEmitters") // False positive @Composable -private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modifier) { +private fun MessageSummary( + event: TimelineItem.Event, + sentTimeFull: String, + modifier: Modifier = Modifier, +) { val content: @Composable () -> Unit val icon: @Composable () -> Unit = { Avatar(avatarData = event.senderAvatar.copy(size = AvatarSize.MessageActionSender)) } val contentStyle = ElementTheme.typography.fontBodyMdRegular.copy(color = MaterialTheme.colorScheme.secondary) @@ -300,20 +305,23 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif icon() Spacer(modifier = Modifier.width(8.dp)) Column(modifier = Modifier.weight(1f)) { - SenderName( - senderId = event.senderId, - senderProfile = event.senderProfile, - senderNameMode = SenderNameMode.ActionList, - ) + Row { + SenderName( + modifier = Modifier.weight(1f), + senderId = event.senderId, + senderProfile = event.senderProfile, + senderNameMode = SenderNameMode.ActionList, + ) + Spacer(modifier = Modifier.width(16.dp)) + Text( + text = sentTimeFull, + style = ElementTheme.typography.fontBodyXsRegular, + color = MaterialTheme.colorScheme.secondary, + textAlign = TextAlign.End, + ) + } content() } - Spacer(modifier = Modifier.width(16.dp)) - Text( - event.sentTime, - style = ElementTheme.typography.fontBodyXsRegular, - color = MaterialTheme.colorScheme.secondary, - textAlign = TextAlign.End, - ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt index d94ca9013a..3700e02ccf 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt @@ -20,7 +20,8 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemGrou import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts import io.element.android.libraries.core.bool.orTrue -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.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.MatrixClient @@ -32,14 +33,13 @@ import io.element.android.libraries.matrix.api.timeline.item.event.getDisambigua import io.element.android.libraries.matrix.ui.messages.reply.map import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import java.text.DateFormat import java.util.Date class TimelineItemEventFactory @AssistedInject constructor( @Assisted private val config: TimelineItemsFactoryConfig, private val contentFactory: TimelineItemContentFactory, private val matrixClient: MatrixClient, - private val lastMessageTimestampFormatter: LastMessageTimestampFormatter, + private val dateFormatter: DateFormatter, private val permalinkParser: PermalinkParser, ) { @AssistedFactory @@ -57,9 +57,10 @@ class TimelineItemEventFactory @AssistedInject constructor( val groupPosition = computeGroupPosition(currentTimelineItem, timelineItems, index) val senderProfile = currentTimelineItem.event.senderProfile - val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT) - val sentTime = timeFormatter.format(Date(currentTimelineItem.event.timestamp)) - + val sentTime = dateFormatter.format( + timestamp = currentTimelineItem.event.timestamp, + mode = DateFormatterMode.TimeOnly, + ) val senderAvatarData = AvatarData( id = currentSender.value, name = senderProfile.getDisambiguatedDisplayName(currentSender), @@ -78,6 +79,7 @@ class TimelineItemEventFactory @AssistedInject constructor( isMine = currentTimelineItem.event.isOwn, isEditable = currentTimelineItem.event.isEditable, canBeRepliedTo = currentTimelineItem.event.canBeRepliedTo, + sentTimeMillis = currentTimelineItem.event.timestamp, sentTime = sentTime, groupPosition = groupPosition, reactionsState = currentTimelineItem.computeReactionsState(), @@ -106,7 +108,6 @@ class TimelineItemEventFactory @AssistedInject constructor( if (!config.computeReactions) { return TimelineItemReactions(reactions = persistentListOf()) } - val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT) var aggregatedReactions = this.event.reactions.map { reaction -> // Sort reactions within an aggregation by timestamp descending. // This puts the most recent at the top, useful in cases like the @@ -121,7 +122,10 @@ class TimelineItemEventFactory @AssistedInject constructor( AggregatedReactionSender( senderId = it.senderId, timestamp = date, - sentTime = timeFormatter.format(date), + sentTime = dateFormatter.format( + it.timestamp, + DateFormatterMode.TimeOrDate, + ), ) } .toImmutableList() @@ -157,7 +161,10 @@ class TimelineItemEventFactory @AssistedInject constructor( url = roomMember?.avatarUrl, size = AvatarSize.TimelineReadReceipt, ), - formattedDate = lastMessageTimestampFormatter.format(receipt.timestamp) + formattedDate = dateFormatter.format( + receipt.timestamp, + mode = DateFormatterMode.TimeOrDate, + ) ) } .toImmutableList() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemDaySeparatorFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemDaySeparatorFactory.kt index 41966c036b..cd680d4e80 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemDaySeparatorFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemDaySeparatorFactory.kt @@ -9,13 +9,20 @@ package io.element.android.features.messages.impl.timeline.factories.virtual import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel -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.item.virtual.VirtualTimelineItem import javax.inject.Inject -class TimelineItemDaySeparatorFactory @Inject constructor(private val daySeparatorFormatter: DaySeparatorFormatter) { +class TimelineItemDaySeparatorFactory @Inject constructor( + private val dateFormatter: DateFormatter, +) { fun create(virtualItem: VirtualTimelineItem.DayDivider): TimelineItemVirtualModel { - val formattedDate = daySeparatorFormatter.format(virtualItem.timestamp) + val formattedDate = dateFormatter.format( + timestamp = virtualItem.timestamp, + mode = DateFormatterMode.Day, + useRelative = true, + ) return TimelineItemDaySeparatorModel( formattedDate = formattedDate ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt index 0a392aac6a..53237ef4de 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt @@ -71,6 +71,7 @@ sealed interface TimelineItem { val senderProfile: ProfileTimelineDetails, val senderAvatar: AvatarData, val content: TimelineItemEventContent, + val sentTimeMillis: Long = 0L, val sentTime: String = "", val isMine: Boolean = false, val isEditable: Boolean, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index b15f358828..c8305f971b 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -327,6 +327,7 @@ class MessagesViewTest { actionListState = anActionListState( target = ActionListState.Target.Success( event = timelineItem, + sentTimeFull = "", displayEmojiReactions = true, actions = persistentListOf(TimelineItemAction.Edit), verifiedUserSendFailure = VerifiedUserSendFailure.None, @@ -399,6 +400,7 @@ class MessagesViewTest { actionListState = anActionListState( target = ActionListState.Target.Success( event = timelineItem, + sentTimeFull = "", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf(TimelineItemAction.Edit), @@ -427,6 +429,7 @@ class MessagesViewTest { actionListState = anActionListState( target = ActionListState.Target.Success( event = timelineItem, + sentTimeFull = "", displayEmojiReactions = true, verifiedUserSendFailure = aChangedIdentitySendFailure(), actions = persistentListOf(), diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt index 49db6f6c95..14605f31d5 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenterTest.kt @@ -26,6 +26,7 @@ import io.element.android.features.messages.impl.timeline.model.event.aTimelineI import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemStateEventContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList +import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.room.MatrixRoom @@ -86,6 +87,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = false, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -128,6 +130,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = false, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -170,6 +173,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -215,6 +219,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -263,6 +268,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -308,6 +314,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -355,6 +362,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = false, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -403,6 +411,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -448,6 +457,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -496,6 +506,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -542,6 +553,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -592,6 +604,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -641,6 +654,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -691,6 +705,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -738,6 +753,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = stateEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = false, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -808,6 +824,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -855,6 +872,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -909,6 +927,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -1006,6 +1025,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -1046,6 +1066,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -1089,6 +1110,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -1131,6 +1153,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -1174,6 +1197,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = true, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -1214,6 +1238,7 @@ class ActionListPresenterTest { assertThat(successState.target).isEqualTo( ActionListState.Target.Success( event = messageEvent, + sentTimeFull = "0 Full true", displayEmojiReactions = false, verifiedUserSendFailure = VerifiedUserSendFailure.None, actions = persistentListOf( @@ -1268,6 +1293,7 @@ private fun createActionListPresenter( initialState = mapOf( FeatureFlags.MediaCaptionCreation.key to allowCaption, ), - ) + ), + dateFormatter = FakeDateFormatter(), ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt index 51c4cb43ba..df76e15b6c 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/fixtures/TimelineItemsFactoryFixtures.kt @@ -28,8 +28,7 @@ import io.element.android.features.messages.impl.utils.FakeTextPillificationHelp import io.element.android.features.messages.test.timeline.FakeHtmlConverterProvider import io.element.android.features.poll.test.pollcontent.FakePollContentStateFactory import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter -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.eventformatter.api.TimelineEventFormatter import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem @@ -80,7 +79,7 @@ internal fun TestScope.aTimelineItemsFactory( failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(), ), matrixClient = matrixClient, - lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(), + dateFormatter = FakeDateFormatter(), permalinkParser = FakePermalinkParser(), config = config ) @@ -88,7 +87,7 @@ internal fun TestScope.aTimelineItemsFactory( }, virtualItemFactory = TimelineItemVirtualFactory( daySeparatorFactory = TimelineItemDaySeparatorFactory( - FakeDaySeparatorFormatter() + FakeDateFormatter() ), ), timelineItemGrouper = TimelineItemGrouper(), diff --git a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItemsFactory.kt b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItemsFactory.kt index 60814477c9..1c667efffb 100644 --- a/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItemsFactory.kt +++ b/features/poll/impl/src/main/kotlin/io/element/android/features/poll/impl/history/model/PollHistoryItemsFactory.kt @@ -9,7 +9,8 @@ package io.element.android.features.poll.impl.history.model import io.element.android.features.poll.api.pollcontent.PollContentStateFactory import io.element.android.libraries.core.coroutine.CoroutineDispatchers -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.event.PollContent import kotlinx.collections.immutable.toPersistentList @@ -18,7 +19,7 @@ import javax.inject.Inject class PollHistoryItemsFactory @Inject constructor( private val pollContentStateFactory: PollContentStateFactory, - private val daySeparatorFormatter: DaySeparatorFormatter, + private val dateFormatter: DateFormatter, private val dispatchers: CoroutineDispatchers, ) { suspend fun create(timelineItems: List): PollHistoryItems = withContext(dispatchers.computation) { @@ -45,7 +46,11 @@ class PollHistoryItemsFactory @Inject constructor( val pollContent = timelineItem.event.content as? PollContent ?: return null val pollContentState = pollContentStateFactory.create(timelineItem.event, pollContent) PollHistoryItem( - formattedDate = daySeparatorFormatter.format(timelineItem.event.timestamp), + formattedDate = dateFormatter.format( + timestamp = timelineItem.event.timestamp, + mode = DateFormatterMode.Day, + useRelative = true + ), state = pollContentState ) } diff --git a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt index 6dfa8df752..d3e67e223e 100644 --- a/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt +++ b/features/poll/impl/src/test/kotlin/io/element/android/features/poll/impl/history/PollHistoryPresenterTest.kt @@ -21,7 +21,7 @@ import io.element.android.features.poll.impl.history.model.PollHistoryItemsFacto import io.element.android.features.poll.impl.model.DefaultPollContentStateFactory import io.element.android.features.poll.test.actions.FakeEndPollAction import io.element.android.features.poll.test.actions.FakeSendPollResponseAction -import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter +import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.test.AN_EVENT_ID @@ -161,7 +161,7 @@ class PollHistoryPresenterTest { sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(), pollHistoryItemFactory: PollHistoryItemsFactory = PollHistoryItemsFactory( pollContentStateFactory = DefaultPollContentStateFactory(FakeMatrixClient()), - daySeparatorFormatter = FakeDaySeparatorFormatter(), + dateFormatter = FakeDateFormatter(), dispatchers = testCoroutineDispatchers(), ), ): PollHistoryPresenter { diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt index 534de3c4d4..a77f1545da 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactory.kt @@ -10,7 +10,8 @@ package io.element.android.features.roomlist.impl.datasource import io.element.android.features.roomlist.impl.model.RoomListRoomSummary import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType import io.element.android.libraries.core.extensions.orEmpty -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.designsystem.components.avatar.AvatarSize import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.matrix.api.room.CurrentUserMembership @@ -22,7 +23,7 @@ import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject class RoomListRoomSummaryFactory @Inject constructor( - private val lastMessageTimestampFormatter: LastMessageTimestampFormatter, + private val dateFormatter: DateFormatter, private val roomLastMessageFormatter: RoomLastMessageFormatter, ) { fun create(roomSummary: RoomSummary): RoomListRoomSummary { @@ -36,7 +37,11 @@ class RoomListRoomSummaryFactory @Inject constructor( numberOfUnreadMentions = roomInfo.numUnreadMentions, numberOfUnreadNotifications = roomInfo.numUnreadNotifications, isMarkedUnread = roomInfo.isMarkedUnread, - timestamp = lastMessageTimestampFormatter.format(roomSummary.lastMessageTimestamp), + timestamp = dateFormatter.format( + timestamp = roomSummary.lastMessageTimestamp, + mode = DateFormatterMode.TimeOrDate, + useRelative = true, + ), lastMessage = roomSummary.lastMessage?.let { message -> roomLastMessageFormatter.format(message.event, roomInfo.isDm) }.orEmpty(), diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt index 69e9a7d401..84ef3078e4 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt @@ -31,9 +31,8 @@ import io.element.android.features.roomlist.impl.search.RoomListSearchState import io.element.android.features.roomlist.impl.search.aRoomListSearchState import io.element.android.libraries.androidutils.system.DateTimeObserver import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter -import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE -import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter +import io.element.android.libraries.dateformatter.api.DateFormatter +import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter @@ -188,6 +187,7 @@ class RoomListPresenterTest { createRoomListRoomSummary( numberOfUnreadMentions = 1, numberOfUnreadMessages = 2, + timestamp = "0 TimeOrDate true", ) ) cancelAndIgnoreRemainingEvents() @@ -633,9 +633,7 @@ class RoomListPresenterTest { networkMonitor: NetworkMonitor = FakeNetworkMonitor(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), leaveRoomState: LeaveRoomState = aLeaveRoomState(), - lastMessageTimestampFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter().apply { - givenFormat(A_FORMATTED_DATE) - }, + dateFormatter: DateFormatter = FakeDateFormatter(), roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(), sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(), featureFlagService: FeatureFlagService = FakeFeatureFlagService(), @@ -652,7 +650,7 @@ class RoomListPresenterTest { roomListDataSource = RoomListDataSource( roomListService = client.roomListService, roomListRoomSummaryFactory = aRoomListRoomSummaryFactory( - lastMessageTimestampFormatter = lastMessageTimestampFormatter, + dateFormatter = dateFormatter, roomLastMessageFormatter = roomLastMessageFormatter, ), coroutineDispatchers = testCoroutineDispatchers(), diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt index f02c53e6f6..1839b35688 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListDataSourceTest.kt @@ -11,7 +11,7 @@ import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.roomlist.impl.FakeDateTimeObserver import io.element.android.libraries.androidutils.system.DateTimeObserver -import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter +import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.matrix.api.roomlist.RoomListService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.room.aRoomSummary @@ -30,12 +30,12 @@ class RoomListDataSourceTest { postAllRooms(listOf(aRoomSummary())) } val dateTimeObserver = FakeDateTimeObserver() - val lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter() - lastMessageTimestampFormatter.givenFormat("Today") + var dateFormatterResult = "Today" + val dateFormatter = FakeDateFormatter({ _, _, _ -> dateFormatterResult }) val roomListDataSource = createRoomListDataSource( roomListService = roomListService, roomListRoomSummaryFactory = aRoomListRoomSummaryFactory( - lastMessageTimestampFormatter = lastMessageTimestampFormatter, + dateFormatter = dateFormatter, ), dateTimeObserver = dateTimeObserver, ) @@ -47,7 +47,7 @@ class RoomListDataSourceTest { val initialRoomList = awaitItem() assertThat(initialRoomList).isNotEmpty() assertThat(initialRoomList.first().timestamp).isEqualTo("Today") - lastMessageTimestampFormatter.givenFormat("Yesterday") + dateFormatterResult = "Yesterday" // Trigger a date change dateTimeObserver.given(DateTimeObserver.Event.DateChanged(Instant.MIN, Instant.now())) // Check there is a new list and it's not the same as the previous one @@ -64,12 +64,12 @@ class RoomListDataSourceTest { postAllRooms(listOf(aRoomSummary())) } val dateTimeObserver = FakeDateTimeObserver() - val lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter() - lastMessageTimestampFormatter.givenFormat("Today") + var dateFormatterResult = "Today" + val dateFormatter = FakeDateFormatter({ _, _, _ -> dateFormatterResult }) val roomListDataSource = createRoomListDataSource( roomListService = roomListService, roomListRoomSummaryFactory = aRoomListRoomSummaryFactory( - lastMessageTimestampFormatter = lastMessageTimestampFormatter, + dateFormatter = dateFormatter, ), dateTimeObserver = dateTimeObserver, ) @@ -80,7 +80,7 @@ class RoomListDataSourceTest { val initialRoomList = awaitItem() assertThat(initialRoomList).isNotEmpty() assertThat(initialRoomList.first().timestamp).isEqualTo("Today") - lastMessageTimestampFormatter.givenFormat("Yesterday") + dateFormatterResult = "Yesterday" // Trigger a timezone change dateTimeObserver.given(DateTimeObserver.Event.TimeZoneChanged) // Check there is a new list and it's not the same as the previous one diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactoryTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactoryTest.kt index 8a26120a9e..41996b24db 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactoryTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/datasource/RoomListRoomSummaryFactoryTest.kt @@ -7,13 +7,14 @@ package io.element.android.features.roomlist.impl.datasource -import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter +import io.element.android.libraries.dateformatter.api.DateFormatter +import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter fun aRoomListRoomSummaryFactory( - lastMessageTimestampFormatter: LastMessageTimestampFormatter = LastMessageTimestampFormatter { _ -> "Today" }, + dateFormatter: DateFormatter = FakeDateFormatter { _, _, _ -> "Today" }, roomLastMessageFormatter: RoomLastMessageFormatter = RoomLastMessageFormatter { _, _ -> "Hey" } ) = RoomListRoomSummaryFactory( - lastMessageTimestampFormatter = lastMessageTimestampFormatter, + dateFormatter = dateFormatter, roomLastMessageFormatter = roomLastMessageFormatter ) diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt index 7e91fa59de..fbe7137ed8 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/model/RoomListRoomSummaryTest.kt @@ -8,7 +8,6 @@ package io.element.android.features.roomlist.impl.model import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.room.RoomNotificationMode @@ -84,6 +83,7 @@ internal fun createRoomListRoomSummary( isFavorite: Boolean = false, displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM, heroes: List = emptyList(), + timestamp: String? = null, ) = RoomListRoomSummary( id = A_ROOM_ID.value, roomId = A_ROOM_ID, @@ -92,7 +92,7 @@ internal fun createRoomListRoomSummary( numberOfUnreadMessages = numberOfUnreadMessages, numberOfUnreadNotifications = numberOfUnreadNotifications, isMarkedUnread = isMarkedUnread, - timestamp = A_FORMATTED_DATE, + timestamp = timestamp, lastMessage = "", avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem), displayType = displayType, diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTest.kt index 6ede9544ec..0d86860445 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/search/RoomListSearchPresenterTest.kt @@ -12,7 +12,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.roomlist.impl.datasource.aRoomListRoomSummaryFactory -import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter +import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags @@ -143,7 +143,7 @@ fun TestScope.createRoomListSearchPresenter( dataSource = RoomListSearchDataSource( roomListService = roomListService, roomSummaryFactory = aRoomListRoomSummaryFactory( - lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(), + dateFormatter = FakeDateFormatter(), roomLastMessageFormatter = FakeRoomLastMessageFormatter(), ), coroutineDispatchers = testCoroutineDispatchers(), diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt index ebd897d84c..601e7cce16 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt @@ -20,7 +20,8 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.features.verifysession.impl.incoming.IncomingVerificationState.Step import io.element.android.libraries.architecture.Presenter -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.matrix.api.verification.SessionVerificationRequestDetails import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.VerificationFlowState @@ -37,7 +38,7 @@ class IncomingVerificationPresenter @AssistedInject constructor( @Assisted private val navigator: IncomingVerificationNavigator, private val sessionVerificationService: SessionVerificationService, private val stateMachine: IncomingVerificationStateMachine, - private val dateFormatter: LastMessageTimestampFormatter, + private val dateFormatter: DateFormatter, ) : Presenter { @AssistedFactory interface Factory { @@ -59,7 +60,10 @@ class IncomingVerificationPresenter @AssistedInject constructor( } val stateAndDispatch = stateMachine.rememberStateAndDispatch() val formattedSignInTime = remember { - dateFormatter.format(sessionVerificationRequestDetails.firstSeenTimestamp) + dateFormatter.format( + timestamp = sessionVerificationRequestDetails.firstSeenTimestamp, + mode = DateFormatterMode.TimeOrDate, + ) } val step by remember { derivedStateOf { diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt index 773b7b390b..c4406009da 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt @@ -9,9 +9,8 @@ package io.element.android.features.verifysession.impl.incoming import com.google.common.truth.Truth.assertThat import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData -import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter -import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE -import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter +import io.element.android.libraries.dateformatter.api.DateFormatter +import io.element.android.libraries.dateformatter.test.FakeDateFormatter import io.element.android.libraries.matrix.api.core.FlowId import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails import io.element.android.libraries.matrix.api.verification.SessionVerificationService @@ -56,7 +55,7 @@ class IncomingVerificationPresenterTest { IncomingVerificationState.Step.Initial( deviceDisplayName = "a device name", deviceId = A_DEVICE_ID, - formattedSignInTime = A_FORMATTED_DATE, + formattedSignInTime = "567 TimeOrDate false", isWaiting = false, ) ) @@ -119,7 +118,7 @@ class IncomingVerificationPresenterTest { IncomingVerificationState.Step.Initial( deviceDisplayName = "a device name", deviceId = A_DEVICE_ID, - formattedSignInTime = A_FORMATTED_DATE, + formattedSignInTime = "567 TimeOrDate false", isWaiting = false, ) ) @@ -178,7 +177,7 @@ class IncomingVerificationPresenterTest { IncomingVerificationState.Step.Initial( deviceDisplayName = "a device name", deviceId = A_DEVICE_ID, - formattedSignInTime = A_FORMATTED_DATE, + formattedSignInTime = "567 TimeOrDate false", isWaiting = false, ) ) @@ -210,7 +209,7 @@ class IncomingVerificationPresenterTest { IncomingVerificationState.Step.Initial( deviceDisplayName = "a device name", deviceId = A_DEVICE_ID, - formattedSignInTime = A_FORMATTED_DATE, + formattedSignInTime = "567 TimeOrDate false", isWaiting = false, ) ) @@ -281,7 +280,7 @@ class IncomingVerificationPresenterTest { sessionVerificationRequestDetails: SessionVerificationRequestDetails = aSessionVerificationRequestDetails, navigator: IncomingVerificationNavigator = IncomingVerificationNavigator { lambdaError() }, service: SessionVerificationService = FakeSessionVerificationService(), - dateFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(A_FORMATTED_DATE), + dateFormatter: DateFormatter = FakeDateFormatter(), ) = IncomingVerificationPresenter( sessionVerificationRequestDetails = sessionVerificationRequestDetails, navigator = navigator, diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt index 12aa5c4bfe..22a0c518ec 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/extensions/BasicExtensions.kt @@ -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() + } + } +} diff --git a/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DateFormatter.kt b/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DateFormatter.kt new file mode 100644 index 0000000000..4475ced912 --- /dev/null +++ b/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DateFormatter.kt @@ -0,0 +1,26 @@ +/* + * 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, + Month, + Day, + // Time if same day, else date + TimeOrDate, + // Only time whatever the day + TimeOnly, +} diff --git a/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DaySeparatorFormatter.kt b/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DaySeparatorFormatter.kt deleted file mode 100644 index 4cc35218a0..0000000000 --- a/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DaySeparatorFormatter.kt +++ /dev/null @@ -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 -} diff --git a/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/LastMessageTimestampFormatter.kt b/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/LastMessageTimestampFormatter.kt deleted file mode 100644 index c5b9778669..0000000000 --- a/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/LastMessageTimestampFormatter.kt +++ /dev/null @@ -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 -} diff --git a/libraries/dateformatter/impl/build.gradle.kts b/libraries/dateformatter/impl/build.gradle.kts index eb05eb18e0..e814a1e2b8 100644 --- a/libraries/dateformatter/impl/build.gradle.kts +++ b/libraries/dateformatter/impl/build.gradle.kts @@ -16,15 +16,29 @@ setupAnvil() android { namespace = "io.element.android.libraries.dateformatter.impl" + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } + dependencies { implementation(libs.dagger) + implementation(projects.libraries.core) 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) } } diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterDay.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterDay.kt new file mode 100644 index 0000000000..2f34d480e0 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterDay.kt @@ -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() + } +} diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterFull.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterFull.kt new file mode 100644 index 0000000000..80e613e38e --- /dev/null +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterFull.kt @@ -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) + } + } +} diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterMonth.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterMonth.kt new file mode 100644 index 0000000000..3d56ebcea1 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterMonth.kt @@ -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() + } +} diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatter.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTime.kt similarity index 62% rename from libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatter.kt rename to libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTime.kt index 8c34905836..b0ad28fdcf 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatter.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTime.kt @@ -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, ) } } diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTimeOnly.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTimeOnly.kt new file mode 100644 index 0000000000..ce412f0d43 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateFormatterTimeOnly.kt @@ -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) + } +} 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 a78cc81c24..e2637b5613 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 @@ -7,57 +7,63 @@ 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) +) : 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.getDefault()) - 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 +81,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 } } diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateTimeFormatters.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateTimeFormatters.kt new file mode 100644 index 0000000000..15dc6aa05e --- /dev/null +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DateTimeFormatters.kt @@ -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 + } +} diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatter.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatter.kt new file mode 100644 index 0000000000..7497f8ee45 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatter.kt @@ -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) + } + } + } +} 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 deleted file mode 100644 index 89ef9ee412..0000000000 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDaySeparatorFormatter.kt +++ /dev/null @@ -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) - } -} diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocaleChangeObserver.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocaleChangeObserver.kt new file mode 100644 index 0000000000..e89bfe7a99 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/LocaleChangeObserver.kt @@ -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() + + 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) + } +} diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt index 568bee5378..3c409a977f 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt @@ -14,7 +14,6 @@ import io.element.android.libraries.dateformatter.impl.TimezoneProvider import io.element.android.libraries.di.AppScope import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone -import java.util.Locale @Module @ContributesTo(AppScope::class) @@ -22,9 +21,6 @@ object DateFormatterModule { @Provides fun providesClock(): Clock = Clock.System - @Provides - fun providesLocale(): Locale = Locale.getDefault() - @Provides fun providesTimezone(): TimezoneProvider = TimezoneProvider { TimeZone.currentSystemDefault() } } diff --git a/libraries/dateformatter/impl/src/main/res/values-fr/translations.xml b/libraries/dateformatter/impl/src/main/res/values-fr/translations.xml new file mode 100644 index 0000000000..f263536767 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values-fr/translations.xml @@ -0,0 +1,5 @@ + + + "%1$s à %2$s" + "Ce mois-ci" + diff --git a/libraries/dateformatter/impl/src/main/res/values/localazy.xml b/libraries/dateformatter/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..8b0dab8cff --- /dev/null +++ b/libraries/dateformatter/impl/src/main/res/values/localazy.xml @@ -0,0 +1,5 @@ + + + "%1$s at %2$s" + "This month" + diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt new file mode 100644 index 0000000000..6301698406 --- /dev/null +++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt @@ -0,0 +1,277 @@ +/* + * 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 io.element.android.libraries.dateformatter.test.FakeClock +import io.element.android.tests.testutils.InstrumentationStringProvider +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +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("Aujourd’hui") + 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("Aujourd’hui") + 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("Aujourd’hui") + 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("Aujourd’hui") + 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 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") + } + + /** + * Create DefaultLastMessageFormatter and set current time to the provided date. + */ + private fun createFormatter(@Suppress("SameParameterValue") 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 }, + ) + 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, + ), + ) + } +} diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt new file mode 100644 index 0000000000..57db7bc260 --- /dev/null +++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt @@ -0,0 +1,277 @@ +/* + * 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 io.element.android.libraries.dateformatter.test.FakeClock +import io.element.android.tests.testutils.InstrumentationStringProvider +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +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:00 AM") + 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:00 AM") + } + + @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:00 AM") + 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:00 AM") + } + + @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:35 PM") + 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:35 PM") + assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM") + } + + @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:35 PM") + 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:35 PM") + assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM") + } + + @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:35 PM") + 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:35 PM") + assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM") + } + + @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:35 PM") + 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:35 PM") + assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM") + } + + @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:34 PM") + 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:34 PM") + assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:34 PM") + } + + @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:34 PM") + 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:34 PM") + assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:34 PM") + } + + @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:35 PM") + 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:35 PM") + assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("5:35 PM") + } + + @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:35 PM") + 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:35 PM") + assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("5:35 PM") + } + + @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:35 PM") + 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:35 PM") + } + + @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:35 PM") + 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:35 PM") + } + + @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:35 PM") + 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:35 PM") + } + + @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:35 PM") + 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:35 PM") + } + + @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:35 PM") + 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:35 PM") + } + + @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:35 PM") + 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:35 PM") + } + + /** + * Create DefaultLastMessageFormatter and set current time to the provided date. + */ + private fun createFormatter(@Suppress("SameParameterValue") 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 }, + ) + 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, + ), + ) + } +} 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 deleted file mode 100644 index 5c8de4462b..0000000000 --- a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultLastMessageTimestampFormatterTest.kt +++ /dev/null @@ -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:35 PM") - } - - @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:35 PM") - } - - @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:34 PM") - } - - @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:35 PM") - } - - @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) - } -} diff --git a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeDateFormatter.kt b/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeDateFormatter.kt new file mode 100644 index 0000000000..722e43f2c9 --- /dev/null +++ b/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeDateFormatter.kt @@ -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) + } +} diff --git a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeDaySeparatorFormatter.kt b/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeDaySeparatorFormatter.kt deleted file mode 100644 index 529d884809..0000000000 --- a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeDaySeparatorFormatter.kt +++ /dev/null @@ -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 - } -} diff --git a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeLastMessageTimestampFormatter.kt b/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeLastMessageTimestampFormatter.kt deleted file mode 100644 index 7edcf321cb..0000000000 --- a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeLastMessageTimestampFormatter.kt +++ /dev/null @@ -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 - } -} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index c84a37cdc3..daf90d6356 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -235,7 +235,7 @@ class RustMatrixRoom( RoomMessageEventMessageType.VIDEO, RoomMessageEventMessageType.AUDIO, ), - dateDividerMode = DateDividerMode.DAILY, + dateDividerMode = DateDividerMode.MONTHLY, ).let { inner -> createTimeline(inner, mode = Timeline.Mode.MEDIA) } diff --git a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt index 17a1052954..7daa5ab7ef 100644 --- a/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt +++ b/libraries/mediaviewer/api/src/main/kotlin/io/element/android/libraries/mediaviewer/api/MediaInfo.kt @@ -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, ) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt index f9611a7023..d85bf08b8e 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/DefaultMediaViewerEntryPoint.kt @@ -53,6 +53,7 @@ class DefaultMediaViewerEntryPoint @Inject constructor() : MediaViewerEntryPoint senderName = null, senderAvatar = null, dateSent = null, + dateSentFull = null, ), mediaSource = MediaSource(url = avatarUrl), thumbnailSource = null, diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheet.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheet.kt index a11abe945b..42127db229 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheet.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/MediaDetailsBottomSheet.kt @@ -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), diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/Preview.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/Preview.kt index 880fcb2b91..5957fd480f 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/Preview.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/details/Preview.kt @@ -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, ) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/EventItemFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/EventItemFactory.kt index 6b96500149..8fcea07f52 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/EventItemFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/EventItemFactory.kt @@ -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, ) diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/VirtualItemFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/VirtualItemFactory.kt index 22d5ef546b..df0976b468 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/VirtualItemFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/VirtualItemFactory.kt @@ -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( diff --git a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt index 62706f120e..c17d613e55 100644 --- a/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt +++ b/libraries/mediaviewer/impl/src/main/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactory.kt @@ -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, ) ) } diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/DefaultEventItemFactoryTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/DefaultEventItemFactoryTest.kt index 3dde8176b4..36b767e870 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/DefaultEventItemFactoryTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/DefaultEventItemFactoryTest.kt @@ -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(), ) diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt index 4aeada8701..8eeaef976c 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/gallery/MediaGalleryPresenterTest.kt @@ -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, diff --git a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt index f60c43572e..d1f0f745f8 100644 --- a/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt +++ b/libraries/mediaviewer/impl/src/test/kotlin/io/element/android/libraries/mediaviewer/impl/local/AndroidLocalMediaFactoryTest.kt @@ -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" ) ) } diff --git a/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt index c41435afc0..39014f90cb 100644 --- a/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt +++ b/libraries/mediaviewer/test/src/main/kotlin/io/element/android/libraries/mediaviewer/test/FakeLocalMediaFactory.kt @@ -40,7 +40,8 @@ class FakeLocalMediaFactory( senderId = null, senderName = null, senderAvatar = null, - dateSent = null + dateSent = null, + dateSentFull = null, ) return aLocalMedia(uri, mediaInfo) } diff --git a/tests/testutils/build.gradle.kts b/tests/testutils/build.gradle.kts index ce9698ab3e..7d9fa12efc 100644 --- a/tests/testutils/build.gradle.kts +++ b/tests/testutils/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.core) implementation(projects.libraries.uiStrings) + implementation(projects.services.toolbox.api) implementation(libs.test.turbine) implementation(libs.molecule.runtime) implementation(libs.androidx.compose.ui.test.junit) diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/InstrumentationStringProvider.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/InstrumentationStringProvider.kt new file mode 100644 index 0000000000..fa60e497cd --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/InstrumentationStringProvider.kt @@ -0,0 +1,26 @@ +/* + * 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.tests.testutils + +import androidx.test.platform.app.InstrumentationRegistry +import io.element.android.services.toolbox.api.strings.StringProvider + +class InstrumentationStringProvider : StringProvider { + private val resource = InstrumentationRegistry.getInstrumentation().context.resources + override fun getString(resId: Int): String { + return resource.getString(resId) + } + + override fun getString(resId: Int, vararg formatArgs: Any?): String { + return resource.getString(resId, *formatArgs) + } + + override fun getQuantityString(resId: Int, quantity: Int, vararg formatArgs: Any?): String { + return resource.getQuantityString(resId, quantity, *formatArgs) + } +} diff --git a/tools/localazy/config.json b/tools/localazy/config.json index fe2f7d3e03..2efd8eac97 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -80,6 +80,12 @@ ".*voice_message_tooltip" ] }, + { + "name" : ":libraries:dateformatter:impl", + "includeRegex" : [ + "common\\.date\\..*" + ] + }, { "name" : ":libraries:permissions:api", "includeRegex" : [ From 927f620a2f3ec80620c5506d643fa04eb5705ea2 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 11 Dec 2024 23:10:31 +0000 Subject: [PATCH 02/10] Update screenshots --- ...ssages.impl.actionlist_ActionListViewContent_Day_10_en.png | 4 ++-- ...ssages.impl.actionlist_ActionListViewContent_Day_11_en.png | 4 ++-- ...ssages.impl.actionlist_ActionListViewContent_Day_12_en.png | 4 ++-- ...essages.impl.actionlist_ActionListViewContent_Day_2_en.png | 4 ++-- ...essages.impl.actionlist_ActionListViewContent_Day_3_en.png | 4 ++-- ...essages.impl.actionlist_ActionListViewContent_Day_4_en.png | 4 ++-- ...essages.impl.actionlist_ActionListViewContent_Day_5_en.png | 4 ++-- ...essages.impl.actionlist_ActionListViewContent_Day_6_en.png | 4 ++-- ...essages.impl.actionlist_ActionListViewContent_Day_7_en.png | 4 ++-- ...essages.impl.actionlist_ActionListViewContent_Day_8_en.png | 4 ++-- ...essages.impl.actionlist_ActionListViewContent_Day_9_en.png | 4 ++-- ...ages.impl.actionlist_ActionListViewContent_Night_10_en.png | 4 ++-- ...ages.impl.actionlist_ActionListViewContent_Night_11_en.png | 4 ++-- ...ages.impl.actionlist_ActionListViewContent_Night_12_en.png | 4 ++-- ...sages.impl.actionlist_ActionListViewContent_Night_2_en.png | 4 ++-- ...sages.impl.actionlist_ActionListViewContent_Night_3_en.png | 4 ++-- ...sages.impl.actionlist_ActionListViewContent_Night_4_en.png | 4 ++-- ...sages.impl.actionlist_ActionListViewContent_Night_5_en.png | 4 ++-- ...sages.impl.actionlist_ActionListViewContent_Night_6_en.png | 4 ++-- ...sages.impl.actionlist_ActionListViewContent_Night_7_en.png | 4 ++-- ...sages.impl.actionlist_ActionListViewContent_Night_8_en.png | 4 ++-- ...sages.impl.actionlist_ActionListViewContent_Night_9_en.png | 4 ++-- ...iaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png | 4 ++-- ...viewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png | 4 ++-- ...ies.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png | 4 ++-- ...s.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png | 4 ++-- ...ibraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png | 4 ++-- 27 files changed, 54 insertions(+), 54 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png index 0e0342e192..223d7869b6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e67a540966100272311381e87011149cdb15c8191a6f2bbc40d1febff999c431 -size 24067 +oid sha256:dbef9e8e887fa493ca9b875e64dc164bf35dd84ad25b84a1ad9b6fd523b26c38 +size 27280 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png index f500ff60dd..cc268a914f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65023f7233f112547ccd0850c321b2d6610cfb6a1371c494f68722e75874f871 -size 45915 +oid sha256:c1a0bc9c83b7e01f9492443433781509c0d899b397652fc7e9b7a539d8ce0412 +size 48219 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png index c80b8639e0..d2eec8525c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2c2e01bd133c7b84e381141b6baf22783cb585cb3af9f790785dbf8aeaeeed6 -size 47644 +oid sha256:3a624587095825c971186288a0ad5a40ddfcadc8f4b9f92b1102d8ae20ca3bda +size 49821 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png index 4088be4e1e..392c35230c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9bd6fb63059cebc7dc7b850b5aa73549ab9846eda68b1b6d9a4f8e0716d42c3c -size 40419 +oid sha256:4cdcc38bfae43298654c3e09d7ab1ca1e0d2fd32157a464539a360f3943f4f75 +size 42839 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png index b3dbf77d1d..89efb1c866 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b4e4a075bcb4b95455c27ce2e5f9f1bdac4a1aca22ad944dd53fdbaeb6ca970 -size 44550 +oid sha256:a56b3e488ceecd0bfb763d60b6827f7d8c460fa198d380a502ff0d97b13c9bc0 +size 45962 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png index 87c07faab3..496cf3053b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a2bc7e9098301d17e72a61a7baf01e52aa11566e1da4ffe3b43a66fa37652b2 -size 42103 +oid sha256:600f28097c6ef8c54fe136d9bb05b8b800d693bff23f238a4e3e0216ee9918ff +size 44404 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png index 25da7b8bf1..834430dfb3 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f097af2773ff2ecf70a69e4292dc118b4c8616f1c8f979e7d121e86a16ef3072 -size 38506 +oid sha256:054cfe9290f91cf0f5d17e8f7c49e71eb50a2bb17067b1edc3c29f89410f76e8 +size 40806 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png index 2bf210d590..f082532ffd 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f49870a5333cbaf1c6c3ac5ddcb85290d04efc79ffec93d8dc39b59f9712820 -size 42391 +oid sha256:814bb524af65d077e3610e8cb920e5207aa808111a530357e8687831bd9c0413 +size 44687 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png index 6de43e00ee..6db122607c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:883f160afa3fc3d2c0be5098205ea616875084ee0f122ebf994d0fee53a8ecd1 -size 39847 +oid sha256:4da93eb0b9fcea861ae80d6e14bc852acaacea52402b1b41e8c982b9c06b2b63 +size 42105 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png index cf03f3c12f..358a9d9a81 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7790c892daeee8910ce17caac2c957d09fffec0d41a51b3adc1d5bef8dcee1a1 -size 42261 +oid sha256:4886407cfe23d5d16aadeef4226698957dad99dfc4d9ac5c184fe64347b3ee41 +size 44524 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png index 63da19cb01..4de1129978 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bc7b733680a0d86ee1231345e58641fb3a208da21de62a50f624d9ae04c6e140 -size 31661 +oid sha256:3c0d8491dbf0b6f50ae6be5c7aaf0d77b9eaa3a31e7763f120d6e08993c1c74b +size 34028 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png index 7e43e760b2..65a49cb609 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20504466241e36817557812f6b378aceab9c2e271a596bd3d037c6be41af7c54 -size 23624 +oid sha256:64a6d1a6af14176c933387a7a36de0d7392b1cd31cb04fcc5c6a282317874aac +size 26597 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png index 1667fe5ff0..89e23b0444 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48028e4c3ca7e9b871683165f029430bcd4e0fc2411e4ffc83a93abb641d96a2 -size 45132 +oid sha256:f92ebc24de4dd34b84fcbbcc656a369d4a79bca160666c2fd4d32b5516c2f8fb +size 47245 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png index e44f324015..6e575e21c5 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b040fe47521f5d9d76d7892412bb2b5cf7e2b93b951f5de5f772503797fb6b24 -size 46548 +oid sha256:2a4a3730a941a4c6863c116b401ea7e4d5c2cef3fe4098e353954c4f93aa660a +size 48771 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png index 5797dc5345..053b325557 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6db046a97f14f2df3db2415d71a02ffd6e706fe73def67c21f0d844be279da59 -size 39617 +oid sha256:77521de94c04228ca0fb5d09cb7809527cf0dc0acc52a271a8fa4738a752aa66 +size 42028 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png index 9b837817e6..a3c9b39e27 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ee53243923d7b5adcc2094d3fe6bb87be00a179cae11966dcb230f2c3e8246e -size 43681 +oid sha256:171ef67bdc5a1b6d921ecbdb13e3c79dfab073d7289862181a69515e6063c4c5 +size 45246 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png index 6d0c2a905b..1374a97dd7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b88e54ba4743e1e245b5a4c7d6b4045f816241f6cfc8716b8cede0b62666222b -size 41286 +oid sha256:1a8328afeab339104a83db16d7b4b894c060ff7e80c2b5c4d6ec64d3bc4cdc3a +size 43613 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png index c479ebef74..6f45705e5e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d88b4d82bb0d416aff7b32647479181ac9702c36c08370e7671d6e5ef80687c7 -size 37825 +oid sha256:d575d08141bf446de510162f5dc1e04fccf4831649495bdf94945bd8957768a6 +size 40201 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png index 885d6afaed..59e4356eb2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3d4dd1766a3f46acd86bdd5ec920c56a5e5f897c81c1477df10ba59dcd3d5b7 -size 41554 +oid sha256:606379fcb517afa4583ac03fcb2af4f5649a7fc064df841f442c19f5a71f629b +size 43863 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png index 13b4a76016..8225529b52 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:842fbea74e53c8804fd0a3e9fe96dcb21b5c91a099e1de33d8ec983fd9a8a80a -size 39112 +oid sha256:d63dec242ea479beb9a7bf0a58c6f31e2bba84df0a7db51fb07ea46612944d9a +size 41355 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png index 18e022f292..26f908facc 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1311ec5e008b44d81e6556164f780c57dea3e4721cc47caccdbf337dd4027eb7 -size 41402 +oid sha256:f5421b7b5ce8441213b1323b4cd88c2f5aff6689ecd091b5be581fe5690cd611 +size 43685 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png index 99c6f765a5..0e328d0740 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2cf27d2f053f33c67d944a8f45e0206165b2db2f3b959eb9e0bb943f84fe6a1 -size 30657 +oid sha256:e1f4600552aebd3a567251a85105a423882942b23e96f7958238a9b5ba0bb8fc +size 33137 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png index 55e944524e..ceac4aade2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f3557280a4010e7ffdb6f22c11561573780c0b24c27e264073d3a3899169014 -size 29659 +oid sha256:2c7ba307cf21056623bf35c8558809fdbc6deaacf4e9365a99ffcf829e8d9188 +size 34477 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png index 33c24c3341..8a0b19ace2 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63efb9744640b71cfde672821c7c1ea8b33f9c3c2e2ad4d5f41149b9749b31c2 -size 28016 +oid sha256:724bff130c547e7f0065ccb4c1b3319162ded2e6c1c1db666f4e08e01289a5a0 +size 32776 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png index 03b462a778..319ab0eca4 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:983e505a211c92ae92e091f9ba7cc43a655d5f3ce6d6bcf70971d43984507326 -size 36153 +oid sha256:25ebb7460550b31bf2cebfca7bdab32e5f89e327b68f6687bf490e5d14cb9220 +size 40950 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png index 85ef965bae..8e10c8a8aa 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e7a43195bd617ee952ea084e6b17a7e08e8b3634e8f3ce7df6e98f067ab08dc -size 34296 +oid sha256:39ecc9a50285df492417f5f22adcc391be2dcad0cc388efb756274c83aba077d +size 39030 diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png index 99618a6ea0..a8777362d7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a6916d2441316cb3ef55ee4c6a3b3ba9134d246d72c27aa3871408a6b9e59fc -size 30716 +oid sha256:2d5f183f53f9e8d0dbbae473f2f853f4372dbff15b1d6ea17e78b4770781fa34 +size 35468 From 1b1ca04934c2d9e281001cdfc52f7db4184cdeae Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Dec 2024 00:04:55 +0100 Subject: [PATCH 03/10] Avoid code duplication between the 2 tests. --- .../impl/DefaultDateFormatterFrTest.kt | 43 --------------- .../impl/DefaultDateFormatterTest.kt | 43 --------------- .../libraries/dateformatter/impl/Factory.kt | 53 +++++++++++++++++++ 3 files changed, 53 insertions(+), 86 deletions(-) create mode 100644 libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt index 6301698406..cddfc5b8e0 100644 --- a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt +++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt @@ -10,10 +10,7 @@ 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 io.element.android.libraries.dateformatter.test.FakeClock -import io.element.android.tests.testutils.InstrumentationStringProvider import kotlinx.datetime.Instant -import kotlinx.datetime.TimeZone import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config @@ -234,44 +231,4 @@ class DefaultDateFormatterFrTest { assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("06.04.1979") assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35") } - - /** - * Create DefaultLastMessageFormatter and set current time to the provided date. - */ - private fun createFormatter(@Suppress("SameParameterValue") 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 }, - ) - 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, - ), - ) - } } diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt index 57db7bc260..142af801ea 100644 --- a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt +++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt @@ -10,10 +10,7 @@ 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 io.element.android.libraries.dateformatter.test.FakeClock -import io.element.android.tests.testutils.InstrumentationStringProvider import kotlinx.datetime.Instant -import kotlinx.datetime.TimeZone import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.Config @@ -234,44 +231,4 @@ class DefaultDateFormatterTest { assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("06.04.1979") assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM") } - - /** - * Create DefaultLastMessageFormatter and set current time to the provided date. - */ - private fun createFormatter(@Suppress("SameParameterValue") 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 }, - ) - 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, - ), - ) - } } diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt new file mode 100644 index 0000000000..02993ee7d7 --- /dev/null +++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt @@ -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 + +import io.element.android.libraries.dateformatter.test.FakeClock +import io.element.android.tests.testutils.InstrumentationStringProvider +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone + +/** + * 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 }, + ) + 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, + ), + ) +} From d5b716ac3ed1fa9a75a9a61f7361cdca7a9cf32c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Dec 2024 00:20:31 +0100 Subject: [PATCH 04/10] Add more tests. --- .../impl/DefaultDateFormatterFrTest.kt | 26 +++++++++++++++++++ .../impl/DefaultDateFormatterTest.kt | 26 +++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt index cddfc5b8e0..8dd0c61d9f 100644 --- a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt +++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt @@ -180,6 +180,32 @@ class DefaultDateFormatterFrTest { 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" diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt index 142af801ea..b7bf9d818e 100644 --- a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt +++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt @@ -180,6 +180,32 @@ class DefaultDateFormatterTest { assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM") } + @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:35 PM") + 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:35 PM") + } + + @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:35 PM") + 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:35 PM") + } + @Test fun `test one month before same time`() { val now = "1980-04-06T18:35:24.00Z" From daf0811bb36d21d0a4be99be6830dbbf5010f146 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Dec 2024 00:20:44 +0100 Subject: [PATCH 05/10] Add doc and examples. --- .../dateformatter/api/DateFormatter.kt | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DateFormatter.kt b/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DateFormatter.kt index 4475ced912..5632962582 100644 --- a/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DateFormatter.kt +++ b/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DateFormatter.kt @@ -16,11 +16,41 @@ interface DateFormatter { } 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 + + /** + * Time if same day, else date. + */ TimeOrDate, - // Only time whatever the day + + /** + * Only time whatever the day. + */ TimeOnly, } From 01a57664381b840ed9844341dcd4538d10b9a8b1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Dec 2024 09:21:51 +0100 Subject: [PATCH 06/10] Move FakeClock to the impl/test folder. --- .../io/element/android/libraries/dateformatter/impl/Factory.kt | 1 - .../element/android/libraries/dateformatter/impl}/FakeClock.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) rename libraries/dateformatter/{test/src/main/kotlin/io/element/android/libraries/dateformatter/test => impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl}/FakeClock.kt (88%) diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt index 02993ee7d7..98b20c81b6 100644 --- a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt +++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt @@ -7,7 +7,6 @@ package io.element.android.libraries.dateformatter.impl -import io.element.android.libraries.dateformatter.test.FakeClock import io.element.android.tests.testutils.InstrumentationStringProvider import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone diff --git a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeClock.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.kt similarity index 88% rename from libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeClock.kt rename to libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.kt index 79e0eda10f..c6bdbec73f 100644 --- a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeClock.kt +++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.kt @@ -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 From d21917278c5c356cb0280afd6880997648ec977d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Dec 2024 11:23:26 +0100 Subject: [PATCH 07/10] Add preview for date rendering --- libraries/dateformatter/impl/build.gradle.kts | 3 +- .../dateformatter/impl/DateFormatters.kt | 3 +- .../impl/previews/DateForPreview.kt | 53 ++++++++ .../previews/DateFormatterModeProvider.kt | 16 +++ .../previews/DateFormatterModeViewPreview.kt | 124 ++++++++++++++++++ .../dateformatter/impl/previews/Factory.kt | 66 ++++++++++ .../impl/previews/PreviewClock.kt | 21 +++ .../impl/previews/PreviewStringProvider.kt | 29 ++++ .../libraries/dateformatter/impl/Factory.kt | 2 + 9 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateForPreview.kt create mode 100644 libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeProvider.kt create mode 100644 libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeViewPreview.kt create mode 100644 libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/Factory.kt create mode 100644 libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewClock.kt create mode 100644 libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewStringProvider.kt diff --git a/libraries/dateformatter/impl/build.gradle.kts b/libraries/dateformatter/impl/build.gradle.kts index e814a1e2b8..2fb4f8461f 100644 --- a/libraries/dateformatter/impl/build.gradle.kts +++ b/libraries/dateformatter/impl/build.gradle.kts @@ -8,7 +8,7 @@ import extension.setupAnvil */ plugins { - id("io.element.android-library") + id("io.element.android-compose-library") } setupAnvil() @@ -25,6 +25,7 @@ android { 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) 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 e2637b5613..a041952fc3 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,12 +26,13 @@ class DateFormatters @Inject constructor( localeChangeObserver: LocaleChangeObserver, private val clock: Clock, private val timeZoneProvider: TimezoneProvider, + locale: Locale, ) : LocaleChangeListener { init { localeChangeObserver.addListener(this) } - private var dateTimeFormatters: DateTimeFormatters = DateTimeFormatters(Locale.getDefault()) + private var dateTimeFormatters: DateTimeFormatters = DateTimeFormatters(locale) override fun onLocaleChange() { Timber.w("Locale changed, updating formatters") diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateForPreview.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateForPreview.kt new file mode 100644 index 0000000000..5b9f732ceb --- /dev/null +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateForPreview.kt @@ -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", + ), +) diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeProvider.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeProvider.kt new file mode 100644 index 0000000000..36d7acabfc --- /dev/null +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeProvider.kt @@ -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 { + override val values: Sequence + get() = DateFormatterMode.entries.asSequence() +} diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeViewPreview.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeViewPreview.kt new file mode 100644 index 0000000000..d12f7b0724 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeViewPreview.kt @@ -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, + ) + } + } + } + } +} diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/Factory.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/Factory.kt new file mode 100644 index 0000000000..cf9787e9d3 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/Factory.kt @@ -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, + ), + ) +} diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewClock.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewClock.kt new file mode 100644 index 0000000000..3486d169a2 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewClock.kt @@ -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 +} diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewStringProvider.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewStringProvider.kt new file mode 100644 index 0000000000..6498b30d88 --- /dev/null +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewStringProvider.kt @@ -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) + } +} diff --git a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt index 98b20c81b6..dd1572fde6 100644 --- a/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt +++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt @@ -10,6 +10,7 @@ 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. @@ -21,6 +22,7 @@ fun createFormatter(currentDate: String): DefaultDateFormatter { localeChangeObserver = {}, clock = clock, timeZoneProvider = { TimeZone.UTC }, + locale = Locale.getDefault(), ) val stringProvider = InstrumentationStringProvider() val dateFormatterDay = DefaultDateFormatterDay( From f475f279d3bdff843f6febb4891d31e8e397d1fa Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 12 Dec 2024 10:39:07 +0000 Subject: [PATCH 08/10] Update screenshots --- ....dateformatter.impl.previews_DateFormatterModeView_0_en.png | 3 +++ ....dateformatter.impl.previews_DateFormatterModeView_1_en.png | 3 +++ ....dateformatter.impl.previews_DateFormatterModeView_2_en.png | 3 +++ ....dateformatter.impl.previews_DateFormatterModeView_3_en.png | 3 +++ ....dateformatter.impl.previews_DateFormatterModeView_4_en.png | 3 +++ 5 files changed, 15 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_0_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_1_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_2_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_4_en.png diff --git a/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_0_en.png new file mode 100644 index 0000000000..7c2a059e7c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0506879a20bd64cb3a4ea41c93dfa78da1ca3b0c2728ce3044caa56f6648584 +size 105611 diff --git a/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_1_en.png new file mode 100644 index 0000000000..160c9d66fd --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e78aa3521464a53c000298dbd4ef51d4d0ea1d3c75b7bb8dcbd933701bab36b +size 84060 diff --git a/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_2_en.png new file mode 100644 index 0000000000..36a6b1bf7a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9eec4fc7e72588f957cd7d741e29b8eaed71555fc0d66c43e6ae69cc64b924c +size 87650 diff --git a/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_3_en.png new file mode 100644 index 0000000000..ef7b68df39 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d4ae4608296b5bb24c128572cc6b80379a9e3cd12ec89db9693c301e427f6ae5 +size 82330 diff --git a/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_4_en.png new file mode 100644 index 0000000000..7b48600104 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e125730c22fcbc843fc0445e0af06d48b7dc31896053b5a7341b6dde84526eb8 +size 82540 From 53217b5112e00173283b32f2e1e7be0a35d90f98 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Dec 2024 12:09:59 +0100 Subject: [PATCH 09/10] Restore providing the Locale --- .../libraries/dateformatter/impl/di/DateFormatterModule.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt index 3c409a977f..568bee5378 100644 --- a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt +++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/di/DateFormatterModule.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.dateformatter.impl.TimezoneProvider import io.element.android.libraries.di.AppScope import kotlinx.datetime.Clock import kotlinx.datetime.TimeZone +import java.util.Locale @Module @ContributesTo(AppScope::class) @@ -21,6 +22,9 @@ object DateFormatterModule { @Provides fun providesClock(): Clock = Clock.System + @Provides + fun providesLocale(): Locale = Locale.getDefault() + @Provides fun providesTimezone(): TimezoneProvider = TimezoneProvider { TimeZone.currentSystemDefault() } } From 38dd411f6a360e9da7f877222c6d2c42e04cfdbc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 12 Dec 2024 17:47:40 +0100 Subject: [PATCH 10/10] We need to scroll more, there are too many items. --- .maestro/tests/roomList/createAndDeleteRoom.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.maestro/tests/roomList/createAndDeleteRoom.yaml b/.maestro/tests/roomList/createAndDeleteRoom.yaml index ae6f5772c6..7cbf455ba2 100644 --- a/.maestro/tests/roomList/createAndDeleteRoom.yaml +++ b/.maestro/tests/roomList/createAndDeleteRoom.yaml @@ -30,5 +30,6 @@ appId: ${MAESTRO_APP_ID} # assert there's 1 member and 2 invitees - tapOn: "Back" - scroll +- scroll - tapOn: "Leave room" - tapOn: "Leave"