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.
This commit is contained in:
Benoit Marty 2024-12-11 23:43:20 +01:00 committed by Benoit Marty
parent 91444aee67
commit 4188d58b56
60 changed files with 1271 additions and 351 deletions

View file

@ -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.Overlay
import io.element.android.libraries.architecture.overlay.operation.hide import io.element.android.libraries.architecture.overlay.operation.hide
import io.element.android.libraries.architecture.overlay.operation.show 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.di.RoomScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
@ -97,6 +99,7 @@ class MessagesFlowNode @AssistedInject constructor(
private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider, private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider,
private val timelineController: TimelineController, private val timelineController: TimelineController,
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint, private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
private val dateFormatter: DateFormatter,
) : BaseFlowNode<MessagesFlowNode.NavTarget>( ) : BaseFlowNode<MessagesFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = plugins.filterIsInstance<MessagesEntryPoint.Params>().first().initialTarget.toNavTarget(), initialElement = plugins.filterIsInstance<MessagesEntryPoint.Params>().first().initialTarget.toNavTarget(),
@ -436,7 +439,14 @@ class MessagesFlowNode @AssistedInject constructor(
senderId = event.senderId, senderId = event.senderId,
senderName = event.safeSenderName, senderName = event.safeSenderName,
senderAvatar = event.senderAvatar.url, 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, mediaSource = mediaSource,
thumbnailSource = thumbnailSource, thumbnailSource = thumbnailSource,

View file

@ -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.canBeForwarded
import io.element.android.features.messages.impl.timeline.model.event.canReact import io.element.android.features.messages.impl.timeline.model.event.canReact
import io.element.android.libraries.architecture.Presenter 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.di.RoomScope
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
@ -64,6 +66,7 @@ class DefaultActionListPresenter @AssistedInject constructor(
private val room: MatrixRoom, private val room: MatrixRoom,
private val userSendFailureFactory: VerifiedUserSendFailureFactory, private val userSendFailureFactory: VerifiedUserSendFailureFactory,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val dateFormatter: DateFormatter,
) : ActionListPresenter { ) : ActionListPresenter {
@AssistedFactory @AssistedFactory
@ContributesBinding(RoomScope::class) @ContributesBinding(RoomScope::class)
@ -131,6 +134,11 @@ class DefaultActionListPresenter @AssistedInject constructor(
if (actions.isNotEmpty() || displayEmojiReactions || verifiedUserSendFailure != VerifiedUserSendFailure.None) { if (actions.isNotEmpty() || displayEmojiReactions || verifiedUserSendFailure != VerifiedUserSendFailure.None) {
target.value = ActionListState.Target.Success( target.value = ActionListState.Target.Success(
event = timelineItem, event = timelineItem,
sentTimeFull = dateFormatter.format(
timelineItem.sentTimeMillis,
DateFormatterMode.Full,
useRelative = true,
),
displayEmojiReactions = displayEmojiReactions, displayEmojiReactions = displayEmojiReactions,
verifiedUserSendFailure = verifiedUserSendFailure, verifiedUserSendFailure = verifiedUserSendFailure,
actions = actions.toImmutableList() actions = actions.toImmutableList()

View file

@ -24,6 +24,7 @@ data class ActionListState(
data class Loading(val event: TimelineItem.Event) : Target data class Loading(val event: TimelineItem.Event) : Target
data class Success( data class Success(
val event: TimelineItem.Event, val event: TimelineItem.Event,
val sentTimeFull: String,
val displayEmojiReactions: Boolean, val displayEmojiReactions: Boolean,
val verifiedUserSendFailure: VerifiedUserSendFailure, val verifiedUserSendFailure: VerifiedUserSendFailure,
val actions: ImmutableList<TimelineItemAction>, val actions: ImmutableList<TimelineItemAction>,

View file

@ -37,6 +37,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
event = aTimelineItemEvent( event = aTimelineItemEvent(
timelineItemReactions = reactionsState timelineItemReactions = reactionsState
), ),
sentTimeFull = "January 1, 1970 at 12:00AM",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
@ -49,6 +50,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
displayNameAmbiguous = true, displayNameAmbiguous = true,
timelineItemReactions = reactionsState, timelineItemReactions = reactionsState,
), ),
sentTimeFull = "January 1, 1970 at 12:00AM",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList( actions = aTimelineItemActionList(
@ -62,6 +64,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
content = aTimelineItemVideoContent(), content = aTimelineItemVideoContent(),
timelineItemReactions = reactionsState timelineItemReactions = reactionsState
), ),
sentTimeFull = "January 1, 1970 at 12:00AM",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList( actions = aTimelineItemActionList(
@ -75,6 +78,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
content = aTimelineItemFileContent(), content = aTimelineItemFileContent(),
timelineItemReactions = reactionsState timelineItemReactions = reactionsState
), ),
sentTimeFull = "January 1, 1970 at 12:00AM",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList( actions = aTimelineItemActionList(
@ -88,6 +92,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
content = aTimelineItemAudioContent(), content = aTimelineItemAudioContent(),
timelineItemReactions = reactionsState timelineItemReactions = reactionsState
), ),
sentTimeFull = "January 1, 1970 at 12:00AM",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList( actions = aTimelineItemActionList(
@ -101,6 +106,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
content = aTimelineItemVoiceContent(caption = null), content = aTimelineItemVoiceContent(caption = null),
timelineItemReactions = reactionsState timelineItemReactions = reactionsState
), ),
sentTimeFull = "January 1, 1970 at 12:00AM",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList( actions = aTimelineItemActionList(
@ -114,6 +120,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
content = aTimelineItemLocationContent(), content = aTimelineItemLocationContent(),
timelineItemReactions = reactionsState timelineItemReactions = reactionsState
), ),
sentTimeFull = "January 1, 1970 at 12:00AM",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
@ -125,6 +132,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
content = aTimelineItemLocationContent(), content = aTimelineItemLocationContent(),
timelineItemReactions = reactionsState timelineItemReactions = reactionsState
), ),
sentTimeFull = "January 1, 1970 at 12:00AM",
displayEmojiReactions = false, displayEmojiReactions = false,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
@ -136,6 +144,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
content = aTimelineItemPollContent(), content = aTimelineItemPollContent(),
timelineItemReactions = reactionsState timelineItemReactions = reactionsState
), ),
sentTimeFull = "January 1, 1970 at 12:00AM",
displayEmojiReactions = false, displayEmojiReactions = false,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemPollActionList(), actions = aTimelineItemPollActionList(),
@ -147,6 +156,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
timelineItemReactions = reactionsState, timelineItemReactions = reactionsState,
messageShield = MessageShield.UnknownDevice(isCritical = true) messageShield = MessageShield.UnknownDevice(isCritical = true)
), ),
sentTimeFull = "January 1, 1970 at 12:00AM",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),
@ -155,6 +165,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
anActionListState( anActionListState(
target = ActionListState.Target.Success( target = ActionListState.Target.Success(
event = aTimelineItemEvent(), event = aTimelineItemEvent(),
sentTimeFull = "January 1, 1970 at 12:00AM",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = anUnsignedDeviceSendFailure(), verifiedUserSendFailure = anUnsignedDeviceSendFailure(),
actions = aTimelineItemActionList(), actions = aTimelineItemActionList(),

View file

@ -185,6 +185,7 @@ private fun ActionListViewContent(
Column { Column {
MessageSummary( MessageSummary(
event = target.event, event = target.event,
sentTimeFull = target.sentTimeFull,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
@ -245,7 +246,11 @@ private fun ActionListViewContent(
@Suppress("MultipleEmitters") // False positive @Suppress("MultipleEmitters") // False positive
@Composable @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 content: @Composable () -> Unit
val icon: @Composable () -> Unit = { Avatar(avatarData = event.senderAvatar.copy(size = AvatarSize.MessageActionSender)) } val icon: @Composable () -> Unit = { Avatar(avatarData = event.senderAvatar.copy(size = AvatarSize.MessageActionSender)) }
val contentStyle = ElementTheme.typography.fontBodyMdRegular.copy(color = MaterialTheme.colorScheme.secondary) val contentStyle = ElementTheme.typography.fontBodyMdRegular.copy(color = MaterialTheme.colorScheme.secondary)
@ -300,20 +305,23 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
icon() icon()
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {
SenderName( Row {
senderId = event.senderId, SenderName(
senderProfile = event.senderProfile, modifier = Modifier.weight(1f),
senderNameMode = SenderNameMode.ActionList, 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() content()
} }
Spacer(modifier = Modifier.width(16.dp))
Text(
event.sentTime,
style = ElementTheme.typography.fontBodyXsRegular,
color = MaterialTheme.colorScheme.secondary,
textAlign = TextAlign.End,
)
} }
} }

View file

@ -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.TimelineItemReactions
import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts
import io.element.android.libraries.core.bool.orTrue 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.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.MatrixClient 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 io.element.android.libraries.matrix.ui.messages.reply.map
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import java.text.DateFormat
import java.util.Date import java.util.Date
class TimelineItemEventFactory @AssistedInject constructor( class TimelineItemEventFactory @AssistedInject constructor(
@Assisted private val config: TimelineItemsFactoryConfig, @Assisted private val config: TimelineItemsFactoryConfig,
private val contentFactory: TimelineItemContentFactory, private val contentFactory: TimelineItemContentFactory,
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter, private val dateFormatter: DateFormatter,
private val permalinkParser: PermalinkParser, private val permalinkParser: PermalinkParser,
) { ) {
@AssistedFactory @AssistedFactory
@ -57,9 +57,10 @@ class TimelineItemEventFactory @AssistedInject constructor(
val groupPosition = val groupPosition =
computeGroupPosition(currentTimelineItem, timelineItems, index) computeGroupPosition(currentTimelineItem, timelineItems, index)
val senderProfile = currentTimelineItem.event.senderProfile val senderProfile = currentTimelineItem.event.senderProfile
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT) val sentTime = dateFormatter.format(
val sentTime = timeFormatter.format(Date(currentTimelineItem.event.timestamp)) timestamp = currentTimelineItem.event.timestamp,
mode = DateFormatterMode.TimeOnly,
)
val senderAvatarData = AvatarData( val senderAvatarData = AvatarData(
id = currentSender.value, id = currentSender.value,
name = senderProfile.getDisambiguatedDisplayName(currentSender), name = senderProfile.getDisambiguatedDisplayName(currentSender),
@ -78,6 +79,7 @@ class TimelineItemEventFactory @AssistedInject constructor(
isMine = currentTimelineItem.event.isOwn, isMine = currentTimelineItem.event.isOwn,
isEditable = currentTimelineItem.event.isEditable, isEditable = currentTimelineItem.event.isEditable,
canBeRepliedTo = currentTimelineItem.event.canBeRepliedTo, canBeRepliedTo = currentTimelineItem.event.canBeRepliedTo,
sentTimeMillis = currentTimelineItem.event.timestamp,
sentTime = sentTime, sentTime = sentTime,
groupPosition = groupPosition, groupPosition = groupPosition,
reactionsState = currentTimelineItem.computeReactionsState(), reactionsState = currentTimelineItem.computeReactionsState(),
@ -106,7 +108,6 @@ class TimelineItemEventFactory @AssistedInject constructor(
if (!config.computeReactions) { if (!config.computeReactions) {
return TimelineItemReactions(reactions = persistentListOf()) return TimelineItemReactions(reactions = persistentListOf())
} }
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
var aggregatedReactions = this.event.reactions.map { reaction -> var aggregatedReactions = this.event.reactions.map { reaction ->
// Sort reactions within an aggregation by timestamp descending. // Sort reactions within an aggregation by timestamp descending.
// This puts the most recent at the top, useful in cases like the // This puts the most recent at the top, useful in cases like the
@ -121,7 +122,10 @@ class TimelineItemEventFactory @AssistedInject constructor(
AggregatedReactionSender( AggregatedReactionSender(
senderId = it.senderId, senderId = it.senderId,
timestamp = date, timestamp = date,
sentTime = timeFormatter.format(date), sentTime = dateFormatter.format(
it.timestamp,
DateFormatterMode.TimeOrDate,
),
) )
} }
.toImmutableList() .toImmutableList()
@ -157,7 +161,10 @@ class TimelineItemEventFactory @AssistedInject constructor(
url = roomMember?.avatarUrl, url = roomMember?.avatarUrl,
size = AvatarSize.TimelineReadReceipt, size = AvatarSize.TimelineReadReceipt,
), ),
formattedDate = lastMessageTimestampFormatter.format(receipt.timestamp) formattedDate = dateFormatter.format(
receipt.timestamp,
mode = DateFormatterMode.TimeOrDate,
)
) )
} }
.toImmutableList() .toImmutableList()

View file

@ -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.TimelineItemDaySeparatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel 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 io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import javax.inject.Inject 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 { 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( return TimelineItemDaySeparatorModel(
formattedDate = formattedDate formattedDate = formattedDate
) )

View file

@ -71,6 +71,7 @@ sealed interface TimelineItem {
val senderProfile: ProfileTimelineDetails, val senderProfile: ProfileTimelineDetails,
val senderAvatar: AvatarData, val senderAvatar: AvatarData,
val content: TimelineItemEventContent, val content: TimelineItemEventContent,
val sentTimeMillis: Long = 0L,
val sentTime: String = "", val sentTime: String = "",
val isMine: Boolean = false, val isMine: Boolean = false,
val isEditable: Boolean, val isEditable: Boolean,

View file

@ -327,6 +327,7 @@ class MessagesViewTest {
actionListState = anActionListState( actionListState = anActionListState(
target = ActionListState.Target.Success( target = ActionListState.Target.Success(
event = timelineItem, event = timelineItem,
sentTimeFull = "",
displayEmojiReactions = true, displayEmojiReactions = true,
actions = persistentListOf(TimelineItemAction.Edit), actions = persistentListOf(TimelineItemAction.Edit),
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
@ -399,6 +400,7 @@ class MessagesViewTest {
actionListState = anActionListState( actionListState = anActionListState(
target = ActionListState.Target.Success( target = ActionListState.Target.Success(
event = timelineItem, event = timelineItem,
sentTimeFull = "",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf(TimelineItemAction.Edit), actions = persistentListOf(TimelineItemAction.Edit),
@ -427,6 +429,7 @@ class MessagesViewTest {
actionListState = anActionListState( actionListState = anActionListState(
target = ActionListState.Target.Success( target = ActionListState.Target.Success(
event = timelineItem, event = timelineItem,
sentTimeFull = "",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = aChangedIdentitySendFailure(), verifiedUserSendFailure = aChangedIdentitySendFailure(),
actions = persistentListOf(), actions = persistentListOf(),

View file

@ -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.aTimelineItemStateEventContent
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemVoiceContent
import io.element.android.features.poll.api.pollcontent.aPollAnswerItemList 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.api.FeatureFlags
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
@ -86,6 +87,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = false, displayEmojiReactions = false,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -128,6 +130,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = false, displayEmojiReactions = false,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -170,6 +173,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -215,6 +219,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -263,6 +268,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -308,6 +314,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -355,6 +362,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = false, displayEmojiReactions = false,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -403,6 +411,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -448,6 +457,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -496,6 +506,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -542,6 +553,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -592,6 +604,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -641,6 +654,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -691,6 +705,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -738,6 +753,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = stateEvent, event = stateEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = false, displayEmojiReactions = false,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -808,6 +824,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -855,6 +872,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -909,6 +927,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -1006,6 +1025,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -1046,6 +1066,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -1089,6 +1110,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -1131,6 +1153,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -1174,6 +1197,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = true, displayEmojiReactions = true,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -1214,6 +1238,7 @@ class ActionListPresenterTest {
assertThat(successState.target).isEqualTo( assertThat(successState.target).isEqualTo(
ActionListState.Target.Success( ActionListState.Target.Success(
event = messageEvent, event = messageEvent,
sentTimeFull = "0 Full true",
displayEmojiReactions = false, displayEmojiReactions = false,
verifiedUserSendFailure = VerifiedUserSendFailure.None, verifiedUserSendFailure = VerifiedUserSendFailure.None,
actions = persistentListOf( actions = persistentListOf(
@ -1268,6 +1293,7 @@ private fun createActionListPresenter(
initialState = mapOf( initialState = mapOf(
FeatureFlags.MediaCaptionCreation.key to allowCaption, FeatureFlags.MediaCaptionCreation.key to allowCaption,
), ),
) ),
dateFormatter = FakeDateFormatter(),
) )
} }

View file

@ -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.messages.test.timeline.FakeHtmlConverterProvider
import io.element.android.features.poll.test.pollcontent.FakePollContentStateFactory import io.element.android.features.poll.test.pollcontent.FakePollContentStateFactory
import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter import io.element.android.libraries.androidutils.filesize.FakeFileSizeFormatter
import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
import io.element.android.libraries.eventformatter.api.TimelineEventFormatter import io.element.android.libraries.eventformatter.api.TimelineEventFormatter
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
@ -80,7 +79,7 @@ internal fun TestScope.aTimelineItemsFactory(
failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(), failedToParseStateFactory = TimelineItemContentFailedToParseStateFactory(),
), ),
matrixClient = matrixClient, matrixClient = matrixClient,
lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(), dateFormatter = FakeDateFormatter(),
permalinkParser = FakePermalinkParser(), permalinkParser = FakePermalinkParser(),
config = config config = config
) )
@ -88,7 +87,7 @@ internal fun TestScope.aTimelineItemsFactory(
}, },
virtualItemFactory = TimelineItemVirtualFactory( virtualItemFactory = TimelineItemVirtualFactory(
daySeparatorFactory = TimelineItemDaySeparatorFactory( daySeparatorFactory = TimelineItemDaySeparatorFactory(
FakeDaySeparatorFormatter() FakeDateFormatter()
), ),
), ),
timelineItemGrouper = TimelineItemGrouper(), timelineItemGrouper = TimelineItemGrouper(),

View file

@ -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.features.poll.api.pollcontent.PollContentStateFactory
import io.element.android.libraries.core.coroutine.CoroutineDispatchers 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.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.PollContent import io.element.android.libraries.matrix.api.timeline.item.event.PollContent
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
@ -18,7 +19,7 @@ import javax.inject.Inject
class PollHistoryItemsFactory @Inject constructor( class PollHistoryItemsFactory @Inject constructor(
private val pollContentStateFactory: PollContentStateFactory, private val pollContentStateFactory: PollContentStateFactory,
private val daySeparatorFormatter: DaySeparatorFormatter, private val dateFormatter: DateFormatter,
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
) { ) {
suspend fun create(timelineItems: List<MatrixTimelineItem>): PollHistoryItems = withContext(dispatchers.computation) { 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 pollContent = timelineItem.event.content as? PollContent ?: return null
val pollContentState = pollContentStateFactory.create(timelineItem.event, pollContent) val pollContentState = pollContentStateFactory.create(timelineItem.event, pollContent)
PollHistoryItem( PollHistoryItem(
formattedDate = daySeparatorFormatter.format(timelineItem.event.timestamp), formattedDate = dateFormatter.format(
timestamp = timelineItem.event.timestamp,
mode = DateFormatterMode.Day,
useRelative = true
),
state = pollContentState state = pollContentState
) )
} }

View file

@ -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.impl.model.DefaultPollContentStateFactory
import io.element.android.features.poll.test.actions.FakeEndPollAction import io.element.android.features.poll.test.actions.FakeEndPollAction
import io.element.android.features.poll.test.actions.FakeSendPollResponseAction 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.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
@ -161,7 +161,7 @@ class PollHistoryPresenterTest {
sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(), sendPollResponseAction: SendPollResponseAction = FakeSendPollResponseAction(),
pollHistoryItemFactory: PollHistoryItemsFactory = PollHistoryItemsFactory( pollHistoryItemFactory: PollHistoryItemsFactory = PollHistoryItemsFactory(
pollContentStateFactory = DefaultPollContentStateFactory(FakeMatrixClient()), pollContentStateFactory = DefaultPollContentStateFactory(FakeMatrixClient()),
daySeparatorFormatter = FakeDaySeparatorFormatter(), dateFormatter = FakeDateFormatter(),
dispatchers = testCoroutineDispatchers(), dispatchers = testCoroutineDispatchers(),
), ),
): PollHistoryPresenter { ): PollHistoryPresenter {

View file

@ -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.RoomListRoomSummary
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
import io.element.android.libraries.core.extensions.orEmpty 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.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.CurrentUserMembership
@ -22,7 +23,7 @@ import kotlinx.collections.immutable.toImmutableList
import javax.inject.Inject import javax.inject.Inject
class RoomListRoomSummaryFactory @Inject constructor( class RoomListRoomSummaryFactory @Inject constructor(
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter, private val dateFormatter: DateFormatter,
private val roomLastMessageFormatter: RoomLastMessageFormatter, private val roomLastMessageFormatter: RoomLastMessageFormatter,
) { ) {
fun create(roomSummary: RoomSummary): RoomListRoomSummary { fun create(roomSummary: RoomSummary): RoomListRoomSummary {
@ -36,7 +37,11 @@ class RoomListRoomSummaryFactory @Inject constructor(
numberOfUnreadMentions = roomInfo.numUnreadMentions, numberOfUnreadMentions = roomInfo.numUnreadMentions,
numberOfUnreadNotifications = roomInfo.numUnreadNotifications, numberOfUnreadNotifications = roomInfo.numUnreadNotifications,
isMarkedUnread = roomInfo.isMarkedUnread, isMarkedUnread = roomInfo.isMarkedUnread,
timestamp = lastMessageTimestampFormatter.format(roomSummary.lastMessageTimestamp), timestamp = dateFormatter.format(
timestamp = roomSummary.lastMessageTimestamp,
mode = DateFormatterMode.TimeOrDate,
useRelative = true,
),
lastMessage = roomSummary.lastMessage?.let { message -> lastMessage = roomSummary.lastMessage?.let { message ->
roomLastMessageFormatter.format(message.event, roomInfo.isDm) roomLastMessageFormatter.format(message.event, roomInfo.isDm)
}.orEmpty(), }.orEmpty(),

View file

@ -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.features.roomlist.impl.search.aRoomListSearchState
import io.element.android.libraries.androidutils.system.DateTimeObserver import io.element.android.libraries.androidutils.system.DateTimeObserver
import io.element.android.libraries.architecture.Presenter 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.test.A_FORMATTED_DATE import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter
@ -188,6 +187,7 @@ class RoomListPresenterTest {
createRoomListRoomSummary( createRoomListRoomSummary(
numberOfUnreadMentions = 1, numberOfUnreadMentions = 1,
numberOfUnreadMessages = 2, numberOfUnreadMessages = 2,
timestamp = "0 TimeOrDate true",
) )
) )
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
@ -633,9 +633,7 @@ class RoomListPresenterTest {
networkMonitor: NetworkMonitor = FakeNetworkMonitor(), networkMonitor: NetworkMonitor = FakeNetworkMonitor(),
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
leaveRoomState: LeaveRoomState = aLeaveRoomState(), leaveRoomState: LeaveRoomState = aLeaveRoomState(),
lastMessageTimestampFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter().apply { dateFormatter: DateFormatter = FakeDateFormatter(),
givenFormat(A_FORMATTED_DATE)
},
roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(), roomLastMessageFormatter: RoomLastMessageFormatter = FakeRoomLastMessageFormatter(),
sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(), sessionPreferencesStore: SessionPreferencesStore = InMemorySessionPreferencesStore(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(), featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
@ -652,7 +650,7 @@ class RoomListPresenterTest {
roomListDataSource = RoomListDataSource( roomListDataSource = RoomListDataSource(
roomListService = client.roomListService, roomListService = client.roomListService,
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory( roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
lastMessageTimestampFormatter = lastMessageTimestampFormatter, dateFormatter = dateFormatter,
roomLastMessageFormatter = roomLastMessageFormatter, roomLastMessageFormatter = roomLastMessageFormatter,
), ),
coroutineDispatchers = testCoroutineDispatchers(), coroutineDispatchers = testCoroutineDispatchers(),

View file

@ -11,7 +11,7 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.roomlist.impl.FakeDateTimeObserver import io.element.android.features.roomlist.impl.FakeDateTimeObserver
import io.element.android.libraries.androidutils.system.DateTimeObserver 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.api.roomlist.RoomListService
import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService import io.element.android.libraries.matrix.test.notificationsettings.FakeNotificationSettingsService
import io.element.android.libraries.matrix.test.room.aRoomSummary import io.element.android.libraries.matrix.test.room.aRoomSummary
@ -30,12 +30,12 @@ class RoomListDataSourceTest {
postAllRooms(listOf(aRoomSummary())) postAllRooms(listOf(aRoomSummary()))
} }
val dateTimeObserver = FakeDateTimeObserver() val dateTimeObserver = FakeDateTimeObserver()
val lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter() var dateFormatterResult = "Today"
lastMessageTimestampFormatter.givenFormat("Today") val dateFormatter = FakeDateFormatter({ _, _, _ -> dateFormatterResult })
val roomListDataSource = createRoomListDataSource( val roomListDataSource = createRoomListDataSource(
roomListService = roomListService, roomListService = roomListService,
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory( roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
lastMessageTimestampFormatter = lastMessageTimestampFormatter, dateFormatter = dateFormatter,
), ),
dateTimeObserver = dateTimeObserver, dateTimeObserver = dateTimeObserver,
) )
@ -47,7 +47,7 @@ class RoomListDataSourceTest {
val initialRoomList = awaitItem() val initialRoomList = awaitItem()
assertThat(initialRoomList).isNotEmpty() assertThat(initialRoomList).isNotEmpty()
assertThat(initialRoomList.first().timestamp).isEqualTo("Today") assertThat(initialRoomList.first().timestamp).isEqualTo("Today")
lastMessageTimestampFormatter.givenFormat("Yesterday") dateFormatterResult = "Yesterday"
// Trigger a date change // Trigger a date change
dateTimeObserver.given(DateTimeObserver.Event.DateChanged(Instant.MIN, Instant.now())) 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 // 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())) postAllRooms(listOf(aRoomSummary()))
} }
val dateTimeObserver = FakeDateTimeObserver() val dateTimeObserver = FakeDateTimeObserver()
val lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter() var dateFormatterResult = "Today"
lastMessageTimestampFormatter.givenFormat("Today") val dateFormatter = FakeDateFormatter({ _, _, _ -> dateFormatterResult })
val roomListDataSource = createRoomListDataSource( val roomListDataSource = createRoomListDataSource(
roomListService = roomListService, roomListService = roomListService,
roomListRoomSummaryFactory = aRoomListRoomSummaryFactory( roomListRoomSummaryFactory = aRoomListRoomSummaryFactory(
lastMessageTimestampFormatter = lastMessageTimestampFormatter, dateFormatter = dateFormatter,
), ),
dateTimeObserver = dateTimeObserver, dateTimeObserver = dateTimeObserver,
) )
@ -80,7 +80,7 @@ class RoomListDataSourceTest {
val initialRoomList = awaitItem() val initialRoomList = awaitItem()
assertThat(initialRoomList).isNotEmpty() assertThat(initialRoomList).isNotEmpty()
assertThat(initialRoomList.first().timestamp).isEqualTo("Today") assertThat(initialRoomList.first().timestamp).isEqualTo("Today")
lastMessageTimestampFormatter.givenFormat("Yesterday") dateFormatterResult = "Yesterday"
// Trigger a timezone change // Trigger a timezone change
dateTimeObserver.given(DateTimeObserver.Event.TimeZoneChanged) dateTimeObserver.given(DateTimeObserver.Event.TimeZoneChanged)
// Check there is a new list and it's not the same as the previous one // Check there is a new list and it's not the same as the previous one

View file

@ -7,13 +7,14 @@
package io.element.android.features.roomlist.impl.datasource 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 import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
fun aRoomListRoomSummaryFactory( fun aRoomListRoomSummaryFactory(
lastMessageTimestampFormatter: LastMessageTimestampFormatter = LastMessageTimestampFormatter { _ -> "Today" }, dateFormatter: DateFormatter = FakeDateFormatter { _, _, _ -> "Today" },
roomLastMessageFormatter: RoomLastMessageFormatter = RoomLastMessageFormatter { _, _ -> "Hey" } roomLastMessageFormatter: RoomLastMessageFormatter = RoomLastMessageFormatter { _, _ -> "Hey" }
) = RoomListRoomSummaryFactory( ) = RoomListRoomSummaryFactory(
lastMessageTimestampFormatter = lastMessageTimestampFormatter, dateFormatter = dateFormatter,
roomLastMessageFormatter = roomLastMessageFormatter roomLastMessageFormatter = roomLastMessageFormatter
) )

View file

@ -8,7 +8,6 @@
package io.element.android.features.roomlist.impl.model package io.element.android.features.roomlist.impl.model
import com.google.common.truth.Truth.assertThat 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.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.room.RoomNotificationMode
@ -84,6 +83,7 @@ internal fun createRoomListRoomSummary(
isFavorite: Boolean = false, isFavorite: Boolean = false,
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM, displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
heroes: List<AvatarData> = emptyList(), heroes: List<AvatarData> = emptyList(),
timestamp: String? = null,
) = RoomListRoomSummary( ) = RoomListRoomSummary(
id = A_ROOM_ID.value, id = A_ROOM_ID.value,
roomId = A_ROOM_ID, roomId = A_ROOM_ID,
@ -92,7 +92,7 @@ internal fun createRoomListRoomSummary(
numberOfUnreadMessages = numberOfUnreadMessages, numberOfUnreadMessages = numberOfUnreadMessages,
numberOfUnreadNotifications = numberOfUnreadNotifications, numberOfUnreadNotifications = numberOfUnreadNotifications,
isMarkedUnread = isMarkedUnread, isMarkedUnread = isMarkedUnread,
timestamp = A_FORMATTED_DATE, timestamp = timestamp,
lastMessage = "", lastMessage = "",
avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem), avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem),
displayType = displayType, displayType = displayType,

View file

@ -12,7 +12,7 @@ import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.roomlist.impl.datasource.aRoomListRoomSummaryFactory 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.eventformatter.test.FakeRoomLastMessageFormatter
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
@ -143,7 +143,7 @@ fun TestScope.createRoomListSearchPresenter(
dataSource = RoomListSearchDataSource( dataSource = RoomListSearchDataSource(
roomListService = roomListService, roomListService = roomListService,
roomSummaryFactory = aRoomListRoomSummaryFactory( roomSummaryFactory = aRoomListRoomSummaryFactory(
lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(), dateFormatter = FakeDateFormatter(),
roomLastMessageFormatter = FakeRoomLastMessageFormatter(), roomLastMessageFormatter = FakeRoomLastMessageFormatter(),
), ),
coroutineDispatchers = testCoroutineDispatchers(), coroutineDispatchers = testCoroutineDispatchers(),

View file

@ -20,7 +20,8 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.features.verifysession.impl.incoming.IncomingVerificationState.Step import io.element.android.features.verifysession.impl.incoming.IncomingVerificationState.Step
import io.element.android.libraries.architecture.Presenter 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.SessionVerificationRequestDetails
import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.matrix.api.verification.VerificationFlowState
@ -37,7 +38,7 @@ class IncomingVerificationPresenter @AssistedInject constructor(
@Assisted private val navigator: IncomingVerificationNavigator, @Assisted private val navigator: IncomingVerificationNavigator,
private val sessionVerificationService: SessionVerificationService, private val sessionVerificationService: SessionVerificationService,
private val stateMachine: IncomingVerificationStateMachine, private val stateMachine: IncomingVerificationStateMachine,
private val dateFormatter: LastMessageTimestampFormatter, private val dateFormatter: DateFormatter,
) : Presenter<IncomingVerificationState> { ) : Presenter<IncomingVerificationState> {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
@ -59,7 +60,10 @@ class IncomingVerificationPresenter @AssistedInject constructor(
} }
val stateAndDispatch = stateMachine.rememberStateAndDispatch() val stateAndDispatch = stateMachine.rememberStateAndDispatch()
val formattedSignInTime = remember { val formattedSignInTime = remember {
dateFormatter.format(sessionVerificationRequestDetails.firstSeenTimestamp) dateFormatter.format(
timestamp = sessionVerificationRequestDetails.firstSeenTimestamp,
mode = DateFormatterMode.TimeOrDate,
)
} }
val step by remember { val step by remember {
derivedStateOf { derivedStateOf {

View file

@ -9,9 +9,8 @@ package io.element.android.features.verifysession.impl.incoming
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE import io.element.android.libraries.dateformatter.test.FakeDateFormatter
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
import io.element.android.libraries.matrix.api.core.FlowId 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.SessionVerificationRequestDetails
import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@ -56,7 +55,7 @@ class IncomingVerificationPresenterTest {
IncomingVerificationState.Step.Initial( IncomingVerificationState.Step.Initial(
deviceDisplayName = "a device name", deviceDisplayName = "a device name",
deviceId = A_DEVICE_ID, deviceId = A_DEVICE_ID,
formattedSignInTime = A_FORMATTED_DATE, formattedSignInTime = "567 TimeOrDate false",
isWaiting = false, isWaiting = false,
) )
) )
@ -119,7 +118,7 @@ class IncomingVerificationPresenterTest {
IncomingVerificationState.Step.Initial( IncomingVerificationState.Step.Initial(
deviceDisplayName = "a device name", deviceDisplayName = "a device name",
deviceId = A_DEVICE_ID, deviceId = A_DEVICE_ID,
formattedSignInTime = A_FORMATTED_DATE, formattedSignInTime = "567 TimeOrDate false",
isWaiting = false, isWaiting = false,
) )
) )
@ -178,7 +177,7 @@ class IncomingVerificationPresenterTest {
IncomingVerificationState.Step.Initial( IncomingVerificationState.Step.Initial(
deviceDisplayName = "a device name", deviceDisplayName = "a device name",
deviceId = A_DEVICE_ID, deviceId = A_DEVICE_ID,
formattedSignInTime = A_FORMATTED_DATE, formattedSignInTime = "567 TimeOrDate false",
isWaiting = false, isWaiting = false,
) )
) )
@ -210,7 +209,7 @@ class IncomingVerificationPresenterTest {
IncomingVerificationState.Step.Initial( IncomingVerificationState.Step.Initial(
deviceDisplayName = "a device name", deviceDisplayName = "a device name",
deviceId = A_DEVICE_ID, deviceId = A_DEVICE_ID,
formattedSignInTime = A_FORMATTED_DATE, formattedSignInTime = "567 TimeOrDate false",
isWaiting = false, isWaiting = false,
) )
) )
@ -281,7 +280,7 @@ class IncomingVerificationPresenterTest {
sessionVerificationRequestDetails: SessionVerificationRequestDetails = aSessionVerificationRequestDetails, sessionVerificationRequestDetails: SessionVerificationRequestDetails = aSessionVerificationRequestDetails,
navigator: IncomingVerificationNavigator = IncomingVerificationNavigator { lambdaError() }, navigator: IncomingVerificationNavigator = IncomingVerificationNavigator { lambdaError() },
service: SessionVerificationService = FakeSessionVerificationService(), service: SessionVerificationService = FakeSessionVerificationService(),
dateFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(A_FORMATTED_DATE), dateFormatter: DateFormatter = FakeDateFormatter(),
) = IncomingVerificationPresenter( ) = IncomingVerificationPresenter(
sessionVerificationRequestDetails = sessionVerificationRequestDetails, sessionVerificationRequestDetails = sessionVerificationRequestDetails,
navigator = navigator, navigator = navigator,

View file

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

View file

@ -0,0 +1,26 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.api
interface DateFormatter {
fun format(
timestamp: Long?,
mode: DateFormatterMode = DateFormatterMode.Full,
useRelative: Boolean = false,
): String
}
enum class DateFormatterMode {
Full,
Month,
Day,
// Time if same day, else date
TimeOrDate,
// Only time whatever the day
TimeOnly,
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,25 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultDaySeparatorFormatter @Inject constructor(
private val localDateTimeProvider: LocalDateTimeProvider,
private val dateFormatters: DateFormatters,
) : DaySeparatorFormatter {
override fun format(timestamp: Long): String {
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
// TODO use relative formatting once iOS uses it too
return dateFormatters.formatDateWithFullFormat(dateToFormat)
}
}

View file

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

View file

@ -14,7 +14,6 @@ import io.element.android.libraries.dateformatter.impl.TimezoneProvider
import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.AppScope
import kotlinx.datetime.Clock import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import java.util.Locale
@Module @Module
@ContributesTo(AppScope::class) @ContributesTo(AppScope::class)
@ -22,9 +21,6 @@ object DateFormatterModule {
@Provides @Provides
fun providesClock(): Clock = Clock.System fun providesClock(): Clock = Clock.System
@Provides
fun providesLocale(): Locale = Locale.getDefault()
@Provides @Provides
fun providesTimezone(): TimezoneProvider = TimezoneProvider { TimeZone.currentSystemDefault() } fun providesTimezone(): TimezoneProvider = TimezoneProvider { TimeZone.currentSystemDefault() }
} }

View file

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

View file

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

View file

@ -0,0 +1,277 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.dateformatter.test.FakeClock
import io.element.android.tests.testutils.InstrumentationStringProvider
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
@Config(qualifiers = "fr")
class DefaultDateFormatterFrTest {
@Test
fun `test null`() {
val now = "1980-04-06T18:35:24.00Z"
val ts: Long? = null
val formatter = createFormatter(now)
assertThat(formatter.format(ts)).isEmpty()
}
@Test
fun `test epoch`() {
val now = "1980-04-06T18:35:24.00Z"
val ts = 0L
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("1 janvier 1970 à 00:00")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Janvier 1970")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("1 janvier 1970")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("01.01.1970")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("00:00")
}
@Test
fun `test epoch relative`() {
val now = "1980-04-06T18:35:24.00Z"
val ts = 0L
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("1 janvier 1970 à 00:00")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Janvier 1970")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("1 janvier 1970")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("01.01.1970")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("00:00")
}
@Test
fun `test now`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1980 à 18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Dimanche 6 avril")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("18:35")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
}
@Test
fun `test now relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Aujourdhui")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("18:35")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
}
@Test
fun `test one second before`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:35:23.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1980 à 18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Dimanche 6 avril")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("18:35")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
}
@Test
fun `test one second before relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:35:23.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Aujourdhui")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("18:35")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
}
@Test
fun `test one minute before`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:34:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1980 à 18:34")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Dimanche 6 avril")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("18:34")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:34")
}
@Test
fun `test one minute before relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:34:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("18:34")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Aujourdhui")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("18:34")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:34")
}
@Test
fun `test one hour before`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T17:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1980 à 17:35")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Dimanche 6 avril")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("17:35")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("17:35")
}
@Test
fun `test one hour before relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T17:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("17:35")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Aujourdhui")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("17:35")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("17:35")
}
@Test
fun `test one day before same time`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-05T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("5 avril 1980 à 18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Samedi 5 avril")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("5 avr.")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
}
@Test
fun `test one day before same time relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-05T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Hier à 18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Hier")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("Hier")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
}
@Test
fun `test one month before same time`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-03-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 mars 1980 à 18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Mars 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Jeudi 6 mars")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6 mars")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
}
@Test
fun `test one month before same time relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-03-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Jeudi 6 mars à 18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Mars 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Jeudi 6 mars")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6 mars")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
}
@Test
fun `test one year before same time`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1979-04-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1979 à 18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1979")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("6 avril 1979")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("06.04.1979")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
}
@Test
fun `test one year before same time relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1979-04-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("6 avril 1979 à 18:35")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Avril 1979")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("6 avril 1979")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("06.04.1979")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
}
/**
* Create DefaultLastMessageFormatter and set current time to the provided date.
*/
private fun createFormatter(@Suppress("SameParameterValue") currentDate: String): DefaultDateFormatter {
val clock = FakeClock().apply { givenInstant(Instant.parse(currentDate)) }
val localDateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.UTC }
val dateFormatters = DateFormatters(
localeChangeObserver = {},
clock = clock,
timeZoneProvider = { TimeZone.UTC },
)
val stringProvider = InstrumentationStringProvider()
val dateFormatterDay = DefaultDateFormatterDay(
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
)
return DefaultDateFormatter(
dateFormatterFull = DateFormatterFull(
stringProvider = stringProvider,
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
dateFormatterDay = dateFormatterDay,
),
dateFormatterMonth = DateFormatterMonth(
stringProvider = stringProvider,
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
),
dateFormatterDay = dateFormatterDay,
dateFormatterTime = DateFormatterTime(
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
),
dateFormatterTimeOnly = DateFormatterTimeOnly(
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
),
)
}
}

View file

@ -0,0 +1,277 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.dateformatter.impl
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.dateformatter.test.FakeClock
import io.element.android.tests.testutils.InstrumentationStringProvider
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
@RunWith(AndroidJUnit4::class)
@Config(qualifiers = "en")
class DefaultDateFormatterTest {
@Test
fun `test null`() {
val now = "1980-04-06T18:35:24.00Z"
val ts: Long? = null
val formatter = createFormatter(now)
assertThat(formatter.format(ts)).isEmpty()
}
@Test
fun `test epoch`() {
val now = "1980-04-06T18:35:24.00Z"
val ts = 0L
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("January 1, 1970 at 12:00AM")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("January 1970")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("January 1, 1970")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("01.01.1970")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("12:00AM")
}
@Test
fun `test epoch relative`() {
val now = "1980-04-06T18:35:24.00Z"
val ts = 0L
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("January 1, 1970 at 12:00AM")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("January 1970")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("January 1, 1970")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("01.01.1970")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("12:00AM")
}
@Test
fun `test now`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35PM")
}
@Test
fun `test now relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Today")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35PM")
}
@Test
fun `test one second before`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:35:23.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35PM")
}
@Test
fun `test one second before relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:35:23.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Today")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35PM")
}
@Test
fun `test one minute before`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:34:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 6:34PM")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6:34PM")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:34PM")
}
@Test
fun `test one minute before relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T18:34:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("6:34PM")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Today")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6:34PM")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:34PM")
}
@Test
fun `test one hour before`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T17:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 5:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("5:35PM")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("5:35PM")
}
@Test
fun `test one hour before relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-06T17:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("5:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Today")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("5:35PM")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("5:35PM")
}
@Test
fun `test one day before same time`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-05T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 5, 1980 at 6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Saturday 5 April")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("5 Apr")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35PM")
}
@Test
fun `test one day before same time relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-04-05T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Yesterday at 6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Yesterday")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("Yesterday")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35PM")
}
@Test
fun `test one month before same time`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-03-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("March 6, 1980 at 6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("March 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Thursday 6 March")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6 Mar")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35PM")
}
@Test
fun `test one month before same time relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1980-03-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Thursday 6 March at 6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("March 1980")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Thursday 6 March")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6 Mar")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35PM")
}
@Test
fun `test one year before same time`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1979-04-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1979 at 6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1979")
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("April 6, 1979")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("06.04.1979")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35PM")
}
@Test
fun `test one year before same time relative`() {
val now = "1980-04-06T18:35:24.00Z"
val dat = "1979-04-06T18:35:24.00Z"
val ts = Instant.parse(dat).toEpochMilliseconds()
val formatter = createFormatter(now)
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("April 6, 1979 at 6:35PM")
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("April 1979")
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("April 6, 1979")
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("06.04.1979")
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35PM")
}
/**
* Create DefaultLastMessageFormatter and set current time to the provided date.
*/
private fun createFormatter(@Suppress("SameParameterValue") currentDate: String): DefaultDateFormatter {
val clock = FakeClock().apply { givenInstant(Instant.parse(currentDate)) }
val localDateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.UTC }
val dateFormatters = DateFormatters(
localeChangeObserver = {},
clock = clock,
timeZoneProvider = { TimeZone.UTC },
)
val stringProvider = InstrumentationStringProvider()
val dateFormatterDay = DefaultDateFormatterDay(
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
)
return DefaultDateFormatter(
dateFormatterFull = DateFormatterFull(
stringProvider = stringProvider,
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
dateFormatterDay = dateFormatterDay,
),
dateFormatterMonth = DateFormatterMonth(
stringProvider = stringProvider,
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
),
dateFormatterDay = dateFormatterDay,
dateFormatterTime = DateFormatterTime(
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
),
dateFormatterTimeOnly = DateFormatterTimeOnly(
localDateTimeProvider = localDateTimeProvider,
dateFormatters = dateFormatters,
),
)
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,12 +10,15 @@ package io.element.android.libraries.mediaviewer.impl.details
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo 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( return MediaBottomSheetState.MediaDetailsBottomSheetState(
eventId = EventId("\$eventId"), eventId = EventId("\$eventId"),
canDelete = true, canDelete = true,
mediaInfo = anImageMediaInfo( mediaInfo = anImageMediaInfo(
senderName = "Alice", senderName = "Alice",
dateSentFull = dateSentFull,
), ),
thumbnailSource = null, thumbnailSource = null,
) )

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,6 +24,7 @@ dependencies {
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.services.toolbox.api)
implementation(libs.test.turbine) implementation(libs.test.turbine)
implementation(libs.molecule.runtime) implementation(libs.molecule.runtime)
implementation(libs.androidx.compose.ui.test.junit) implementation(libs.androidx.compose.ui.test.junit)

View file

@ -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)
}
}

View file

@ -80,6 +80,12 @@
".*voice_message_tooltip" ".*voice_message_tooltip"
] ]
}, },
{
"name" : ":libraries:dateformatter:impl",
"includeRegex" : [
"common\\.date\\..*"
]
},
{ {
"name" : ":libraries:permissions:api", "name" : ":libraries:permissions:api",
"includeRegex" : [ "includeRegex" : [