Implement month separator for the Gallery, and improve date rendering.

<!-- Please read [CONTRIBUTING.md](https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md) before submitting your pull request -->

## Content

<!-- Describe shortly what has been changed -->
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.

## Motivation and context

<!-- Provide link to the corresponding issue if applicable or explain the context -->
Part of the work on the media gallery + Closes #2238

## Screenshots / GIFs

<!--
We have screenshot tests in the project, so attaching screenshots to a PR is not mandatory, as far as there
is a Composable Preview covering the changes. In this case, the change will appear in the file diff.
Note that all the UI composables should be covered by a Composable Preview.

Providing a video of the change is still very useful for the reviewer and for the history of the project.

You can use a table like this to show screenshots comparison.
Uncomment this markdown table below and edit the last line `|||`:
|copy screenshot of before here|copy screenshot of after here|

|Before|After|
|-|-|
|||
 -->

## Tests

<!-- Explain how you tested your development -->

- Run the app in your current locale and check that all the date across the application look good.
- Change the language of the phone / of the app and observe that the app uses the new languages (especially to render date and time).

## Tested devices

- [ ] Physical
- [x] Emulator
- OS version(s):

## Checklist

<!-- Depending on the Pull Request content, it can be acceptable if some of the following checkboxes stay unchecked. -->

- [ ] Changes have been tested on an Android device or Android emulator with API 23
- [ ] UI change has been tested on both light and dark themes
- [ ] Accessibility has been taken into account. See https://github.com/element-hq/element-x-android/blob/develop/CONTRIBUTING.md#accessibility
- [ ] Pull request is based on the develop branch
- [ ] Pull request title will be used in the release note, it clearly define what will change for the user
- [ ] Pull request includes screenshots or videos if containing UI changes
- [ ] Pull request includes a [sign off](https://matrix-org.github.io/synapse/latest/development/contributing_guide.html#sign-off)
- [ ] You've made a self review of your PR

Iterate on date rendering

Iterate on date rendering
There are (at least) two places where timestamp is only rendered as time and date could be helpful.

- [ ] In the message detail screen, when long clicking on a message. It can be helpful to have the date here, since when scrolling in the timeline the date can be hard to see in room with lots of message (we do not have floating date on the timeline). We may want to always display the full date here, but the space is quite small.
- [ ] In the reaction list. In this case, only the time is displayed, and if the reactions are sent on several days, it may look like they are not sorted correctly. Also I guess that the users may consider that if only the time is displayed, it's for the current day only.
- [ ] other?

For all those cases, maybe introduce a new date time formater, which displays only the time for the current day, and else the date and time for the previous days.

Another weirdness I've noticed is in date rendering in the read receipts list. If a user sends a message at 23:59, 2 minutes later the receipt simply says "yesterday". It would be useful in that case to display an absolute timestamp until the relative time becomes significantly long enough in the past that the loss of precision becomes negligible.

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 3b5fb675405..6754f5c683d 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<MessagesFlowNode.NavTarget>(
     backstack = BackStack(
         initialElement = plugins.filterIsInstance<MessagesEntryPoint.Params>().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 411ff37c8f8..95a6dc5a6f3 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 75c598df36d..56bc1ca0bd9 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<TimelineItemAction>,
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 a5f027a5355..1638a03fa31 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<ActionListState> {
                         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<ActionListState> {
                             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<ActionListState> {
                             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<ActionListState> {
                             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<ActionListState> {
                             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<ActionListState> {
                             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<ActionListState> {
                             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<ActionListState> {
                             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<ActionListState> {
                             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<ActionListState> {
                             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<ActionListState> {
                 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 7d30edd1167..4cf0928d5c4 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 d94ca9013a7..3700e02ccfa 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 41966c036bd..cd680d4e809 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 0a392aac6a0..53237ef4dec 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/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 60814477c9e..1c667efffbb 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<MatrixTimelineItem>): 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/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 534de3c4d4a..a77f1545da5 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/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 ebd897d84c6..601e7cce165 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<IncomingVerificationState> {
     @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/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 12aa5c4bfe6..22a0c518ecc 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 00000000000..5632962582a
--- /dev/null
+++ b/libraries/dateformatter/api/src/main/kotlin/io/element/android/libraries/dateformatter/api/DateFormatter.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.api
+
+interface DateFormatter {
+    fun format(
+        timestamp: Long?,
+        mode: DateFormatterMode = DateFormatterMode.Full,
+        useRelative: Boolean = false,
+    ): String
+}
+
+enum class DateFormatterMode {
+    /**
+     * Full date and time.
+     * Example:
+     * "April 6, 1980 at 6:35 PM"
+     * Format can be shorter when useRelative is true.
+     * Example:
+     * "6:35 PM"
+     */
+    Full,
+
+    /**
+     * Only month and year.
+     * Example:
+     * "April 1980"
+     * "This month" can be returned when useRelative is true.
+     * Example:
+     * "This month"
+     */
+    Month,
+
+    /**
+     * Only day.
+     * Example:
+     * "Sunday 6 April"
+     * "Today", "Yesterday" and day of week can be returned when useRelative is true.
+     */
+    Day,
+
+    /**
+     * Time if same day, else date.
+     */
+    TimeOrDate,
+
+    /**
+     * Only time whatever the day.
+     */
+    TimeOnly,
+}
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 4cc35218a0f..00000000000
--- 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 c5b9778669b..00000000000
--- 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 eb05eb18e0e..2fb4f8461fd 100644
--- a/libraries/dateformatter/impl/build.gradle.kts
+++ b/libraries/dateformatter/impl/build.gradle.kts
@@ -8,7 +8,7 @@ import extension.setupAnvil
  */

 plugins {
-    id("io.element.android-library")
+    id("io.element.android-compose-library")
 }

 setupAnvil()
@@ -16,15 +16,30 @@ setupAnvil()
 android {
     namespace = "io.element.android.libraries.dateformatter.impl"

+    testOptions {
+        unitTests {
+            isIncludeAndroidResources = true
+        }
+    }
+
     dependencies {
         implementation(libs.dagger)
+        implementation(projects.libraries.core)
+        implementation(projects.libraries.designsystem)
         implementation(projects.libraries.di)
+        implementation(projects.libraries.uiStrings)
+        implementation(projects.services.toolbox.api)

         api(projects.libraries.dateformatter.api)
         api(libs.datetime)

         testImplementation(libs.test.junit)
         testImplementation(libs.test.truth)
+        testImplementation(libs.test.turbine)
+        testImplementation(libs.test.robolectric)
         testImplementation(projects.libraries.dateformatter.test)
+        testImplementation(projects.services.toolbox.test)
+        testImplementation(projects.tests.testutils)
+        testImplementation(libs.androidx.compose.ui.test.junit)
     }
 }
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 00000000000..2f34d480e0c
--- /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 00000000000..80e613e38ee
--- /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 00000000000..3d56ebcea1c
--- /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 8c34905836b..b0ad28fdcfa 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 00000000000..ce412f0d435
--- /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 a78cc81c24c..a041952fc37 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,64 @@

 package io.element.android.libraries.dateformatter.impl

-import android.text.format.DateFormat
 import android.text.format.DateUtils
+import io.element.android.libraries.di.AppScope
+import io.element.android.libraries.di.SingleIn
 import kotlinx.datetime.Clock
 import kotlinx.datetime.LocalDateTime
 import kotlinx.datetime.toInstant
 import kotlinx.datetime.toJavaLocalDate
 import kotlinx.datetime.toJavaLocalDateTime
+import timber.log.Timber
 import java.time.Period
-import java.time.format.DateTimeFormatter
-import java.time.format.FormatStyle
 import java.util.Locale
 import javax.inject.Inject
 import kotlin.math.absoluteValue

+@SingleIn(AppScope::class)
 class DateFormatters @Inject constructor(
-    private val locale: Locale,
+    localeChangeObserver: LocaleChangeObserver,
     private val clock: Clock,
     private val timeZoneProvider: TimezoneProvider,
-) {
-    private val onlyTimeFormatter: DateTimeFormatter by lazy {
-        DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale)
+    locale: Locale,
+) : LocaleChangeListener {
+    init {
+        localeChangeObserver.addListener(this)
     }

-    private val dateWithMonthFormatter: DateTimeFormatter by lazy {
-        val pattern = DateFormat.getBestDateTimePattern(locale, "d MMM") ?: "d MMM"
-        DateTimeFormatter.ofPattern(pattern, locale)
-    }
+    private var dateTimeFormatters: DateTimeFormatters = DateTimeFormatters(locale)

-    private val dateWithYearFormatter: DateTimeFormatter by lazy {
-        val pattern = DateFormat.getBestDateTimePattern(locale, "dd.MM.yyyy") ?: "dd.MM.yyyy"
-        DateTimeFormatter.ofPattern(pattern, locale)
+    override fun onLocaleChange() {
+        Timber.w("Locale changed, updating formatters")
+        dateTimeFormatters = DateTimeFormatters(Locale.getDefault())
     }

-    private val dateWithFullFormatFormatter: DateTimeFormatter by lazy {
-        DateTimeFormatter.ofLocalizedDate(FormatStyle.FULL).withLocale(locale)
+    internal fun formatTime(localDateTime: LocalDateTime): String {
+        return dateTimeFormatters.onlyTimeFormatter.format(localDateTime.toJavaLocalDateTime())
     }

-    internal fun formatTime(localDateTime: LocalDateTime): String {
-        return onlyTimeFormatter.format(localDateTime.toJavaLocalDateTime())
+    internal fun formatDateWithMonthAndYear(localDateTime: LocalDateTime): String {
+        return dateTimeFormatters.dateWithMonthAndYearFormatter.format(localDateTime.toJavaLocalDateTime())
     }

     internal fun formatDateWithMonth(localDateTime: LocalDateTime): String {
-        return dateWithMonthFormatter.format(localDateTime.toJavaLocalDateTime())
+        return dateTimeFormatters.dateWithMonthFormatter.format(localDateTime.toJavaLocalDateTime())
+    }
+
+    internal fun formatDateWithDay(localDateTime: LocalDateTime): String {
+        return dateTimeFormatters.dateWithDayFormatter.format(localDateTime.toJavaLocalDateTime())
     }

     internal fun formatDateWithYear(localDateTime: LocalDateTime): String {
-        return dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime())
+        return dateTimeFormatters.dateWithYearFormatter.format(localDateTime.toJavaLocalDateTime())
     }

     internal fun formatDateWithFullFormat(localDateTime: LocalDateTime): String {
-        return dateWithFullFormatFormatter.format(localDateTime.toJavaLocalDateTime())
+        return dateTimeFormatters.dateWithFullFormatFormatter.format(localDateTime.toJavaLocalDateTime())
+    }
+
+    internal fun formatDateWithFullFormatNoYear(localDateTime: LocalDateTime): String {
+        return dateTimeFormatters.dateWithFullFormatNoYearFormatter.format(localDateTime.toJavaLocalDateTime())
     }

     internal fun formatDate(
@@ -75,12 +82,12 @@ class DateFormatters @Inject constructor(
         }
     }

-    private fun getRelativeDay(ts: Long): String {
+    internal fun getRelativeDay(ts: Long, default: String = ""): String {
         return DateUtils.getRelativeTimeSpanString(
             ts,
             clock.now().toEpochMilliseconds(),
             DateUtils.DAY_IN_MILLIS,
             DateUtils.FORMAT_SHOW_WEEKDAY
-        )?.toString() ?: ""
+        )?.toString() ?: default
     }
 }
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 00000000000..15dc6aa05eb
--- /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 00000000000..7497f8ee453
--- /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 89ef9ee412a..00000000000
--- 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 00000000000..e89bfe7a991
--- /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<LocaleChangeListener>()
+
+    override fun addListener(listener: LocaleChangeListener) {
+        listeners.add(listener)
+    }
+
+    private fun registerReceiver(receiver: BroadcastReceiver) {
+        val filter = IntentFilter()
+        filter.addAction(Intent.ACTION_LOCALE_CHANGED)
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            filter.addAction(Intent.ACTION_APPLICATION_LOCALE_CHANGED)
+        }
+        context.registerReceiver(receiver, filter)
+    }
+}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateForPreview.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateForPreview.kt
new file mode 100644
index 00000000000..5b9f732ceb3
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateForPreview.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl.previews
+
+data class DateForPreview(
+    val semantic: String,
+    val date: String,
+)
+
+val dateForPreviewToday = DateForPreview(
+    semantic = "Today",
+    date = "1980-04-06T18:35:24.00Z",
+)
+
+val dateForPreviews = listOf(
+    DateForPreview(
+        semantic = "Now",
+        date = dateForPreviewToday.date,
+    ),
+    DateForPreview(
+        semantic = "One second ago",
+        date = "1980-04-06T18:35:23.00Z",
+    ),
+    DateForPreview(
+        semantic = "One minute ago",
+        date = "1980-04-06T18:34:24.00Z",
+    ),
+    DateForPreview(
+        semantic = "One hour ago",
+        date = "1980-04-06T17:35:24.00Z",
+    ),
+    DateForPreview(
+        semantic = "One day ago",
+        date = "1980-04-05T18:35:24.00Z",
+    ),
+    DateForPreview(
+        semantic = "Two days ago",
+        date = "1980-04-04T18:35:24.00Z",
+    ),
+    DateForPreview(
+        semantic = "One month ago",
+        date = "1980-03-06T18:35:24.00Z",
+    ),
+    DateForPreview(
+        semantic = "One year ago",
+        date = "1979-04-06T18:35:24.00Z",
+    ),
+)
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeProvider.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeProvider.kt
new file mode 100644
index 00000000000..36d7acabfc2
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeProvider.kt
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl.previews
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import io.element.android.libraries.dateformatter.api.DateFormatterMode
+
+class DateFormatterModeProvider : PreviewParameterProvider<DateFormatterMode> {
+    override val values: Sequence<DateFormatterMode>
+        get() = DateFormatterMode.entries.asSequence()
+}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeViewPreview.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeViewPreview.kt
new file mode 100644
index 00000000000..d12f7b07247
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/DateFormatterModeViewPreview.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl.previews
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.intl.Locale
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.tooling.preview.PreviewParameter
+import androidx.compose.ui.unit.dp
+import io.element.android.compound.theme.ElementTheme
+import io.element.android.libraries.dateformatter.api.DateFormatterMode
+import io.element.android.libraries.dateformatter.impl.DefaultDateFormatter
+import io.element.android.libraries.designsystem.preview.ElementPreview
+import io.element.android.libraries.designsystem.theme.components.Text
+import io.element.android.libraries.designsystem.utils.allBooleans
+import kotlinx.datetime.Instant
+
+@Preview
+@Composable
+internal fun DateFormatterModeViewPreview(
+    @PreviewParameter(DateFormatterModeProvider::class) dateFormatterMode: DateFormatterMode,
+) = ElementPreview {
+    DateFormatterModeView(dateFormatterMode)
+}
+
+@Composable
+private fun DateFormatterModeView(
+    mode: DateFormatterMode,
+) {
+    val context = LocalContext.current
+    val composeLocale = Locale.current
+    val dateFormatter = remember {
+        createFormatter(
+            context = context,
+            currentDate = dateForPreviewToday.date,
+            locale = java.util.Locale.Builder()
+                .setLanguageTag(composeLocale.toLanguageTag())
+                .build(),
+        )
+    }
+    Column(
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(4.dp),
+        horizontalAlignment = Alignment.CenterHorizontally,
+    ) {
+        Text(
+            text = "Mode $mode / $composeLocale",
+            style = ElementTheme.typography.fontHeadingSmMedium
+        )
+        val today = Instant.parse(dateForPreviewToday.date).toEpochMilliseconds()
+        Text(
+            text = "Today is: ${dateFormatter.format(today, DateFormatterMode.Full, useRelative = false)}",
+            style = ElementTheme.typography.fontHeadingSmMedium,
+        )
+        dateForPreviews.forEach { dateForPreview ->
+            DateForPreviewItem(
+                dateForPreview = dateForPreview,
+                dateFormatter = dateFormatter,
+                mode = mode,
+            )
+        }
+    }
+}
+
+@Composable
+private fun DateForPreviewItem(
+    dateForPreview: DateForPreview,
+    dateFormatter: DefaultDateFormatter,
+    mode: DateFormatterMode,
+) {
+    Column(
+        modifier = Modifier
+            .fillMaxWidth()
+            .padding(2.dp),
+    ) {
+        Text(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(start = 8.dp),
+            text = dateForPreview.semantic,
+            style = ElementTheme.typography.fontBodyMdMedium,
+            color = ElementTheme.colors.textSecondary,
+        )
+        val ts = Instant.parse(dateForPreview.date).toEpochMilliseconds()
+        Row {
+            Column {
+                listOf("Absolute:", "Relative:").forEach { label ->
+                    Text(
+                        text = label,
+                        style = ElementTheme.typography.fontBodyMdRegular,
+                        color = ElementTheme.colors.textPrimary,
+                    )
+                }
+            }
+            Spacer(modifier = Modifier.width(8.dp))
+            Column {
+                allBooleans.forEach { useRelative ->
+                    Text(
+                        modifier = Modifier.fillMaxWidth(),
+                        text = dateFormatter.format(ts, mode, useRelative),
+                        style = ElementTheme.typography.fontBodyMdRegular,
+                        color = ElementTheme.colors.textPrimary,
+                    )
+                }
+            }
+        }
+    }
+}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/Factory.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/Factory.kt
new file mode 100644
index 00000000000..cf9787e9d3b
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/Factory.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl.previews
+
+import android.content.Context
+import io.element.android.libraries.dateformatter.impl.DateFormatterFull
+import io.element.android.libraries.dateformatter.impl.DateFormatterMonth
+import io.element.android.libraries.dateformatter.impl.DateFormatterTime
+import io.element.android.libraries.dateformatter.impl.DateFormatterTimeOnly
+import io.element.android.libraries.dateformatter.impl.DateFormatters
+import io.element.android.libraries.dateformatter.impl.DefaultDateFormatter
+import io.element.android.libraries.dateformatter.impl.DefaultDateFormatterDay
+import io.element.android.libraries.dateformatter.impl.LocalDateTimeProvider
+import kotlinx.datetime.Instant
+import kotlinx.datetime.TimeZone
+import java.util.Locale
+
+/**
+ * Create DefaultDateFormatter and set current time to the provided date.
+ */
+fun createFormatter(
+    context: Context,
+    currentDate: String,
+    locale: Locale,
+): DefaultDateFormatter {
+    val clock = PreviewClock().apply { givenInstant(Instant.parse(currentDate)) }
+    val localDateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.UTC }
+    val dateFormatters = DateFormatters(
+        localeChangeObserver = {},
+        clock = clock,
+        timeZoneProvider = { TimeZone.UTC },
+        locale = locale,
+    )
+    val stringProvider = PreviewStringProvider(context.resources)
+    val dateFormatterDay = DefaultDateFormatterDay(
+        localDateTimeProvider = localDateTimeProvider,
+        dateFormatters = dateFormatters,
+    )
+    return DefaultDateFormatter(
+        dateFormatterFull = DateFormatterFull(
+            stringProvider = stringProvider,
+            localDateTimeProvider = localDateTimeProvider,
+            dateFormatters = dateFormatters,
+            dateFormatterDay = dateFormatterDay,
+        ),
+        dateFormatterMonth = DateFormatterMonth(
+            stringProvider = stringProvider,
+            localDateTimeProvider = localDateTimeProvider,
+            dateFormatters = dateFormatters,
+        ),
+        dateFormatterDay = dateFormatterDay,
+        dateFormatterTime = DateFormatterTime(
+            localDateTimeProvider = localDateTimeProvider,
+            dateFormatters = dateFormatters,
+        ),
+        dateFormatterTimeOnly = DateFormatterTimeOnly(
+            localDateTimeProvider = localDateTimeProvider,
+            dateFormatters = dateFormatters,
+        ),
+    )
+}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewClock.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewClock.kt
new file mode 100644
index 00000000000..3486d169a21
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewClock.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl.previews
+
+import kotlinx.datetime.Clock
+import kotlinx.datetime.Instant
+
+class PreviewClock : Clock {
+    private var instant: Instant = Instant.fromEpochMilliseconds(0)
+
+    fun givenInstant(instant: Instant) {
+        this.instant = instant
+    }
+
+    override fun now(): Instant = instant
+}
diff --git a/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewStringProvider.kt b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewStringProvider.kt
new file mode 100644
index 00000000000..6498b30d885
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/kotlin/io/element/android/libraries/dateformatter/impl/previews/PreviewStringProvider.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl.previews
+
+import android.content.res.Resources
+import androidx.annotation.PluralsRes
+import androidx.annotation.StringRes
+import io.element.android.services.toolbox.api.strings.StringProvider
+
+class PreviewStringProvider(
+    private val resources: Resources
+) : StringProvider {
+    override fun getString(@StringRes resId: Int): String {
+        return resources.getString(resId)
+    }
+
+    override fun getString(@StringRes resId: Int, vararg formatArgs: Any?): String {
+        return resources.getString(resId, *formatArgs)
+    }
+
+    override fun getQuantityString(@PluralsRes resId: Int, quantity: Int, vararg formatArgs: Any?): String {
+        return resources.getQuantityString(resId, quantity, *formatArgs)
+    }
+}
diff --git a/libraries/dateformatter/impl/src/main/res/values-fr/translations.xml b/libraries/dateformatter/impl/src/main/res/values-fr/translations.xml
new file mode 100644
index 00000000000..f2635367677
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/res/values-fr/translations.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+  <string name="common_date_date_at_time">"%1$s à %2$s"</string>
+  <string name="common_date_this_month">"Ce mois-ci"</string>
+</resources>
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 00000000000..8b0dab8cff4
--- /dev/null
+++ b/libraries/dateformatter/impl/src/main/res/values/localazy.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+  <string name="common_date_date_at_time">"%1$s at %2$s"</string>
+  <string name="common_date_this_month">"This month"</string>
+</resources>
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 c84a37cdc38..daf90d63566 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 17a1052954f..7daa5ab7ef4 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 f9611a70236..d85bf08b8ea 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 a11abe945b7..42127db2290 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 880fcb2b914..5957fd480fa 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 6b965001495..8fcea07f529 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 22d5ef546b3..df0976b4688 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 62706f120ea..c17d613e550 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/tools/localazy/config.json b/tools/localazy/config.json
index fe2f7d3e036..2efd8eac976 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" : [

diff --git a/.maestro/tests/roomList/createAndDeleteRoom.yaml b/.maestro/tests/roomList/createAndDeleteRoom.yaml
index ae6f5772c65..7cbf455ba23 100644
--- a/.maestro/tests/roomList/createAndDeleteRoom.yaml
+++ b/.maestro/tests/roomList/createAndDeleteRoom.yaml
@@ -30,5 +30,6 @@ appId: ${MAESTRO_APP_ID}
 # assert there's 1 member and 2 invitees
 - tapOn: "Back"
 - scroll
+- scroll
 - tapOn: "Leave room"
 - tapOn: "Leave"
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 b15f3588285..c8305f971be 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 49db6f6c955..14605f31d5c 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 51c4cb43ba0..df76e15b6c3 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/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 6dfa8df752e..d3e67e223ed 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/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 69e9a7d4013..84ef3078e46 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 f02c53e6f62..1839b35688e 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 8a26120a9ee..41996b24db9 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 7e91fa59de3..fbe7137ed89 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<AvatarData> = 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 6ede9544ec3..0d86860445a 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/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 773b7b390b9..c4406009da0 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/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 00000000000..8dd0c61d9fb
--- /dev/null
+++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterFrTest.kt
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.dateformatter.api.DateFormatterMode
+import kotlinx.datetime.Instant
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+@Config(qualifiers = "fr")
+class DefaultDateFormatterFrTest {
+    @Test
+    fun `test null`() {
+        val now = "1980-04-06T18:35:24.00Z"
+        val ts: Long? = null
+        val formatter = createFormatter(now)
+        assertThat(formatter.format(ts)).isEmpty()
+    }
+
+    @Test
+    fun `test epoch`() {
+        val now = "1980-04-06T18:35:24.00Z"
+        val ts = 0L
+        val formatter = createFormatter(now)
+        assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("1 janvier 1970 à 00:00")
+        assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Janvier 1970")
+        assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("1 janvier 1970")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("01.01.1970")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("00:00")
+    }
+
+    @Test
+    fun `test epoch relative`() {
+        val now = "1980-04-06T18:35:24.00Z"
+        val ts = 0L
+        val formatter = createFormatter(now)
+        assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("1 janvier 1970 à 00:00")
+        assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Janvier 1970")
+        assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("1 janvier 1970")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("01.01.1970")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("00:00")
+    }
+
+    @Test
+    fun `test now`() {
+        val now = "1980-04-06T18:35:24.00Z"
+        val dat = "1980-04-06T18:35:24.00Z"
+        val ts = Instant.parse(dat).toEpochMilliseconds()
+        val formatter = createFormatter(now)
+        assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1980 à 18:35")
+        assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
+        assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Dimanche 6 avril")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("18:35")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
+    }
+
+    @Test
+    fun `test now relative`() {
+        val now = "1980-04-06T18:35:24.00Z"
+        val dat = "1980-04-06T18:35:24.00Z"
+        val ts = Instant.parse(dat).toEpochMilliseconds()
+        val formatter = createFormatter(now)
+        assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("18:35")
+        assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
+        assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("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 two days before same time`() {
+        val now = "1980-04-06T18:35:24.00Z"
+        val dat = "1980-04-04T18:35:24.00Z"
+        val ts = Instant.parse(dat).toEpochMilliseconds()
+        val formatter = createFormatter(now)
+        assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("4 avril 1980 à 18:35")
+        assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
+        assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Vendredi 4 avril")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("4 avr.")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
+    }
+
+    @Test
+    fun `test two days before same time relative`() {
+        val now = "1980-04-06T18:35:24.00Z"
+        val dat = "1980-04-04T18:35:24.00Z"
+        val ts = Instant.parse(dat).toEpochMilliseconds()
+        val formatter = createFormatter(now)
+        assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Vendredi à 18:35")
+        assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
+        assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Vendredi")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("4 avr.")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
+    }
+
+    @Test
+    fun `test one month before same time`() {
+        val now = "1980-04-06T18:35:24.00Z"
+        val dat = "1980-03-06T18:35:24.00Z"
+        val ts = Instant.parse(dat).toEpochMilliseconds()
+        val formatter = createFormatter(now)
+        assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 mars 1980 à 18:35")
+        assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Mars 1980")
+        assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Jeudi 6 mars")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6 mars")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
+    }
+
+    @Test
+    fun `test one month before same time relative`() {
+        val now = "1980-04-06T18:35:24.00Z"
+        val dat = "1980-03-06T18:35:24.00Z"
+        val ts = Instant.parse(dat).toEpochMilliseconds()
+        val formatter = createFormatter(now)
+        assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Jeudi 6 mars à 18:35")
+        assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Mars 1980")
+        assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Jeudi 6 mars")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6 mars")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
+    }
+
+    @Test
+    fun `test one year before same time`() {
+        val now = "1980-04-06T18:35:24.00Z"
+        val dat = "1979-04-06T18:35:24.00Z"
+        val ts = Instant.parse(dat).toEpochMilliseconds()
+        val formatter = createFormatter(now)
+        assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1979 à 18:35")
+        assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1979")
+        assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("6 avril 1979")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("06.04.1979")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
+    }
+
+    @Test
+    fun `test one year before same time relative`() {
+        val now = "1980-04-06T18:35:24.00Z"
+        val dat = "1979-04-06T18:35:24.00Z"
+        val ts = Instant.parse(dat).toEpochMilliseconds()
+        val formatter = createFormatter(now)
+        assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("6 avril 1979 à 18:35")
+        assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Avril 1979")
+        assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("6 avril 1979")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("06.04.1979")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
+    }
+}
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 00000000000..b7bf9d818eb
--- /dev/null
+++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/DefaultDateFormatterTest.kt
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2023, 2024 New Vector Ltd.
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ * Please see LICENSE in the repository root for full details.
+ */
+
+package io.element.android.libraries.dateformatter.impl
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import io.element.android.libraries.dateformatter.api.DateFormatterMode
+import kotlinx.datetime.Instant
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.annotation.Config
+
+@RunWith(AndroidJUnit4::class)
+@Config(qualifiers = "en")
+class DefaultDateFormatterTest {
+    @Test
+    fun `test null`() {
+        val now = "1980-04-06T18:35:24.00Z"
+        val ts: Long? = null
+        val formatter = createFormatter(now)
+        assertThat(formatter.format(ts)).isEmpty()
+    }
+
+    @Test
+    fun `test epoch`() {
+        val now = "1980-04-06T18:35:24.00Z"
+        val ts = 0L
+        val formatter = createFormatter(now)
+        assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("January 1, 1970 at 12: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 two days before same time`() {
+        val now = "1980-04-06T18:35:24.00Z"
+        val dat = "1980-04-04T18:35:24.00Z"
+        val ts = Instant.parse(dat).toEpochMilliseconds()
+        val formatter = createFormatter(now)
+        assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 4, 1980 at 6:35 PM")
+        assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
+        assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Friday 4 April")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("4 Apr")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM")
+    }
+
+    @Test
+    fun `test two days before same time relative`() {
+        val now = "1980-04-06T18:35:24.00Z"
+        val dat = "1980-04-04T18:35:24.00Z"
+        val ts = Instant.parse(dat).toEpochMilliseconds()
+        val formatter = createFormatter(now)
+        assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Friday at 6:35 PM")
+        assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
+        assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Friday")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("4 Apr")
+        assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM")
+    }
+
+    @Test
+    fun `test one month before same time`() {
+        val now = "1980-04-06T18:35:24.00Z"
+        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")
+    }
+}
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 5c8de4462b1..00000000000
--- 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/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.kt
new file mode 100644
index 00000000000..dd1572fde67
--- /dev/null
+++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/Factory.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 io.element.android.tests.testutils.InstrumentationStringProvider
+import kotlinx.datetime.Instant
+import kotlinx.datetime.TimeZone
+import java.util.Locale
+
+/**
+ * Create DefaultDateFormatter and set current time to the provided date.
+ */
+fun createFormatter(currentDate: String): DefaultDateFormatter {
+    val clock = FakeClock().apply { givenInstant(Instant.parse(currentDate)) }
+    val localDateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.UTC }
+    val dateFormatters = DateFormatters(
+        localeChangeObserver = {},
+        clock = clock,
+        timeZoneProvider = { TimeZone.UTC },
+        locale = Locale.getDefault(),
+    )
+    val stringProvider = InstrumentationStringProvider()
+    val dateFormatterDay = DefaultDateFormatterDay(
+        localDateTimeProvider = localDateTimeProvider,
+        dateFormatters = dateFormatters,
+    )
+    return DefaultDateFormatter(
+        dateFormatterFull = DateFormatterFull(
+            stringProvider = stringProvider,
+            localDateTimeProvider = localDateTimeProvider,
+            dateFormatters = dateFormatters,
+            dateFormatterDay = dateFormatterDay,
+        ),
+        dateFormatterMonth = DateFormatterMonth(
+            stringProvider = stringProvider,
+            localDateTimeProvider = localDateTimeProvider,
+            dateFormatters = dateFormatters,
+        ),
+        dateFormatterDay = dateFormatterDay,
+        dateFormatterTime = DateFormatterTime(
+            localDateTimeProvider = localDateTimeProvider,
+            dateFormatters = dateFormatters,
+        ),
+        dateFormatterTimeOnly = DateFormatterTimeOnly(
+            localDateTimeProvider = localDateTimeProvider,
+            dateFormatters = dateFormatters,
+        ),
+    )
+}
diff --git a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeClock.kt b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.kt
similarity index 88%
rename from libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeClock.kt
rename to libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.kt
index 79e0eda10ff..c6bdbec73f4 100644
--- a/libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeClock.kt
+++ b/libraries/dateformatter/impl/src/test/kotlin/io/element/android/libraries/dateformatter/impl/FakeClock.kt
@@ -5,7 +5,7 @@
  * Please see LICENSE in the repository root for full details.
  */

-package io.element.android.libraries.dateformatter.test
+package io.element.android.libraries.dateformatter.impl

 import kotlinx.datetime.Clock
 import kotlinx.datetime.Instant
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 00000000000..722e43f2c90
--- /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 529d8848098..00000000000
--- 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 7edcf321cbe..00000000000
--- 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/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 3dde8176b44..36b767e870b 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 4aeada87010..8eeaef976c6 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 f60c43572e4..d1f0f745f80 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 c41435afc0f..39014f90cbf 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 ce9698ab3e3..7d9fa12efcf 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 00000000000..fa60e497cdc
--- /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/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png
index 0e0342e192a..223d7869b6f 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_10_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:e67a540966100272311381e87011149cdb15c8191a6f2bbc40d1febff999c431
-size 24067
+oid sha256:dbef9e8e887fa493ca9b875e64dc164bf35dd84ad25b84a1ad9b6fd523b26c38
+size 27280
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png
index f500ff60dd2..cc268a914fe 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_11_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:65023f7233f112547ccd0850c321b2d6610cfb6a1371c494f68722e75874f871
-size 45915
+oid sha256:c1a0bc9c83b7e01f9492443433781509c0d899b397652fc7e9b7a539d8ce0412
+size 48219
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png
index c80b8639e0b..d2eec8525c3 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_12_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:e2c2e01bd133c7b84e381141b6baf22783cb585cb3af9f790785dbf8aeaeeed6
-size 47644
+oid sha256:3a624587095825c971186288a0ad5a40ddfcadc8f4b9f92b1102d8ae20ca3bda
+size 49821
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png
index 4088be4e1e2..392c35230c9 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_2_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:9bd6fb63059cebc7dc7b850b5aa73549ab9846eda68b1b6d9a4f8e0716d42c3c
-size 40419
+oid sha256:4cdcc38bfae43298654c3e09d7ab1ca1e0d2fd32157a464539a360f3943f4f75
+size 42839
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png
index b3dbf77d1d2..89efb1c866a 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_3_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:6b4e4a075bcb4b95455c27ce2e5f9f1bdac4a1aca22ad944dd53fdbaeb6ca970
-size 44550
+oid sha256:a56b3e488ceecd0bfb763d60b6827f7d8c460fa198d380a502ff0d97b13c9bc0
+size 45962
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png
index 87c07faab3c..496cf3053bc 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_4_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:1a2bc7e9098301d17e72a61a7baf01e52aa11566e1da4ffe3b43a66fa37652b2
-size 42103
+oid sha256:600f28097c6ef8c54fe136d9bb05b8b800d693bff23f238a4e3e0216ee9918ff
+size 44404
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png
index 25da7b8bf1f..834430dfb36 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_5_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:f097af2773ff2ecf70a69e4292dc118b4c8616f1c8f979e7d121e86a16ef3072
-size 38506
+oid sha256:054cfe9290f91cf0f5d17e8f7c49e71eb50a2bb17067b1edc3c29f89410f76e8
+size 40806
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png
index 2bf210d5907..f082532ffd9 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_6_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:8f49870a5333cbaf1c6c3ac5ddcb85290d04efc79ffec93d8dc39b59f9712820
-size 42391
+oid sha256:814bb524af65d077e3610e8cb920e5207aa808111a530357e8687831bd9c0413
+size 44687
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png
index 6de43e00eed..6db122607cc 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_7_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:883f160afa3fc3d2c0be5098205ea616875084ee0f122ebf994d0fee53a8ecd1
-size 39847
+oid sha256:4da93eb0b9fcea861ae80d6e14bc852acaacea52402b1b41e8c982b9c06b2b63
+size 42105
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png
index cf03f3c12f4..358a9d9a81a 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_8_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:7790c892daeee8910ce17caac2c957d09fffec0d41a51b3adc1d5bef8dcee1a1
-size 42261
+oid sha256:4886407cfe23d5d16aadeef4226698957dad99dfc4d9ac5c184fe64347b3ee41
+size 44524
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png
index 63da19cb016..4de11299786 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Day_9_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:bc7b733680a0d86ee1231345e58641fb3a208da21de62a50f624d9ae04c6e140
-size 31661
+oid sha256:3c0d8491dbf0b6f50ae6be5c7aaf0d77b9eaa3a31e7763f120d6e08993c1c74b
+size 34028
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png
index 7e43e760b28..65a49cb609a 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_10_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:20504466241e36817557812f6b378aceab9c2e271a596bd3d037c6be41af7c54
-size 23624
+oid sha256:64a6d1a6af14176c933387a7a36de0d7392b1cd31cb04fcc5c6a282317874aac
+size 26597
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png
index 1667fe5ff02..89e23b04441 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_11_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:48028e4c3ca7e9b871683165f029430bcd4e0fc2411e4ffc83a93abb641d96a2
-size 45132
+oid sha256:f92ebc24de4dd34b84fcbbcc656a369d4a79bca160666c2fd4d32b5516c2f8fb
+size 47245
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png
index e44f3240152..6e575e21c5e 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_12_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:b040fe47521f5d9d76d7892412bb2b5cf7e2b93b951f5de5f772503797fb6b24
-size 46548
+oid sha256:2a4a3730a941a4c6863c116b401ea7e4d5c2cef3fe4098e353954c4f93aa660a
+size 48771
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png
index 5797dc53458..053b3255570 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_2_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:6db046a97f14f2df3db2415d71a02ffd6e706fe73def67c21f0d844be279da59
-size 39617
+oid sha256:77521de94c04228ca0fb5d09cb7809527cf0dc0acc52a271a8fa4738a752aa66
+size 42028
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png
index 9b837817e6f..a3c9b39e278 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_3_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:7ee53243923d7b5adcc2094d3fe6bb87be00a179cae11966dcb230f2c3e8246e
-size 43681
+oid sha256:171ef67bdc5a1b6d921ecbdb13e3c79dfab073d7289862181a69515e6063c4c5
+size 45246
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png
index 6d0c2a905bf..1374a97dd7b 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_4_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:b88e54ba4743e1e245b5a4c7d6b4045f816241f6cfc8716b8cede0b62666222b
-size 41286
+oid sha256:1a8328afeab339104a83db16d7b4b894c060ff7e80c2b5c4d6ec64d3bc4cdc3a
+size 43613
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png
index c479ebef749..6f45705e5ee 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_5_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:d88b4d82bb0d416aff7b32647479181ac9702c36c08370e7671d6e5ef80687c7
-size 37825
+oid sha256:d575d08141bf446de510162f5dc1e04fccf4831649495bdf94945bd8957768a6
+size 40201
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png
index 885d6afaedd..59e4356eb22 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_6_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:b3d4dd1766a3f46acd86bdd5ec920c56a5e5f897c81c1477df10ba59dcd3d5b7
-size 41554
+oid sha256:606379fcb517afa4583ac03fcb2af4f5649a7fc064df841f442c19f5a71f629b
+size 43863
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png
index 13b4a76016a..8225529b528 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_7_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:842fbea74e53c8804fd0a3e9fe96dcb21b5c91a099e1de33d8ec983fd9a8a80a
-size 39112
+oid sha256:d63dec242ea479beb9a7bf0a58c6f31e2bba84df0a7db51fb07ea46612944d9a
+size 41355
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png
index 18e022f292c..26f908facc0 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_8_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:1311ec5e008b44d81e6556164f780c57dea3e4721cc47caccdbf337dd4027eb7
-size 41402
+oid sha256:f5421b7b5ce8441213b1323b4cd88c2f5aff6689ecd091b5be581fe5690cd611
+size 43685
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png
index 99c6f765a55..0e328d07400 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.actionlist_ActionListViewContent_Night_9_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:d2cf27d2f053f33c67d944a8f45e0206165b2db2f3b959eb9e0bb943f84fe6a1
-size 30657
+oid sha256:e1f4600552aebd3a567251a85105a423882942b23e96f7958238a9b5ba0bb8fc
+size 33137
diff --git a/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_0_en.png
new file mode 100644
index 00000000000..7c2a059e7ca
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d0506879a20bd64cb3a4ea41c93dfa78da1ca3b0c2728ce3044caa56f6648584
+size 105611
diff --git a/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_1_en.png b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_1_en.png
new file mode 100644
index 00000000000..160c9d66fd9
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2e78aa3521464a53c000298dbd4ef51d4d0ea1d3c75b7bb8dcbd933701bab36b
+size 84060
diff --git a/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_2_en.png b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_2_en.png
new file mode 100644
index 00000000000..36a6b1bf7ac
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f9eec4fc7e72588f957cd7d741e29b8eaed71555fc0d66c43e6ae69cc64b924c
+size 87650
diff --git a/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_3_en.png b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_3_en.png
new file mode 100644
index 00000000000..ef7b68df390
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d4ae4608296b5bb24c128572cc6b80379a9e3cd12ec89db9693c301e427f6ae5
+size 82330
diff --git a/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_4_en.png b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_4_en.png
new file mode 100644
index 00000000000..7b48600104a
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/libraries.dateformatter.impl.previews_DateFormatterModeView_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e125730c22fcbc843fc0445e0af06d48b7dc31896053b5a7341b6dde84526eb8
+size 82540
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png
index 55e944524e6..ceac4aade29 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Day_0_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:4f3557280a4010e7ffdb6f22c11561573780c0b24c27e264073d3a3899169014
-size 29659
+oid sha256:2c7ba307cf21056623bf35c8558809fdbc6deaacf4e9365a99ffcf829e8d9188
+size 34477
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png
index 33c24c33417..8a0b19ace2d 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.details_MediaDetailsBottomSheet_Night_0_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:63efb9744640b71cfde672821c7c1ea8b33f9c3c2e2ad4d5f41149b9749b31c2
-size 28016
+oid sha256:724bff130c547e7f0065ccb4c1b3319162ded2e6c1c1db666f4e08e01289a5a0
+size 32776
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png
index 03b462a778c..319ab0eca48 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Day_8_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:983e505a211c92ae92e091f9ba7cc43a655d5f3ce6d6bcf70971d43984507326
-size 36153
+oid sha256:25ebb7460550b31bf2cebfca7bdab32e5f89e327b68f6687bf490e5d14cb9220
+size 40950
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png
index 85ef965bae6..8e10c8a8aa9 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.gallery_MediaGalleryView_Night_8_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:4e7a43195bd617ee952ea084e6b17a7e08e8b3634e8f3ce7df6e98f067ab08dc
-size 34296
+oid sha256:39ecc9a50285df492417f5f22adcc391be2dcad0cc388efb756274c83aba077d
+size 39030
diff --git a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png
index 99618a6ea00..a8777362d7e 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.mediaviewer.impl.viewer_MediaViewerView_11_en.png
@@ -1,3 +1,3 @@
 version https://git-lfs.github.com/spec/v1
-oid sha256:8a6916d2441316cb3ef55ee4c6a3b3ba9134d246d72c27aa3871408a6b9e59fc
-size 30716
+oid sha256:2d5f183f53f9e8d0dbbae473f2f853f4372dbff15b1d6ea17e78b4770781fa34
+size 35468