From 4188d58b56faa9507ad763a2de78d53a0e1682b8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 11 Dec 2024 23:43:20 +0100 Subject: [PATCH] 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" : [