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

Implement month separator for the Gallery, and improve date rendering.
This commit is contained in:
Benoit Marty 2024-12-12 17:48:17 +01:00 committed by GitHub
commit cf4bc53ef3
100 changed files with 1704 additions and 403 deletions

View file

@ -30,5 +30,6 @@ appId: ${MAESTRO_APP_ID}
# assert there's 1 member and 2 invitees # assert there's 1 member and 2 invitees
- tapOn: "Back" - tapOn: "Back"
- scroll - scroll
- scroll
- tapOn: "Leave room" - tapOn: "Leave room"
- tapOn: "Leave" - tapOn: "Leave"

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ import extension.setupAnvil
*/ */
plugins { plugins {
id("io.element.android-library") id("io.element.android-compose-library")
} }
setupAnvil() setupAnvil()
@ -16,15 +16,30 @@ 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.designsystem)
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,64 @@
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,
) { locale: Locale,
private val onlyTimeFormatter: DateTimeFormatter by lazy { ) : LocaleChangeListener {
DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(locale) init {
localeChangeObserver.addListener(this)
} }
private val dateWithMonthFormatter: DateTimeFormatter by lazy { private var dateTimeFormatters: DateTimeFormatters = DateTimeFormatters(locale)
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 +82,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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -235,7 +235,7 @@ class RustMatrixRoom(
RoomMessageEventMessageType.VIDEO, RoomMessageEventMessageType.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

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e67a540966100272311381e87011149cdb15c8191a6f2bbc40d1febff999c431 oid sha256:dbef9e8e887fa493ca9b875e64dc164bf35dd84ad25b84a1ad9b6fd523b26c38
size 24067 size 27280

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:65023f7233f112547ccd0850c321b2d6610cfb6a1371c494f68722e75874f871 oid sha256:c1a0bc9c83b7e01f9492443433781509c0d899b397652fc7e9b7a539d8ce0412
size 45915 size 48219

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e2c2e01bd133c7b84e381141b6baf22783cb585cb3af9f790785dbf8aeaeeed6 oid sha256:3a624587095825c971186288a0ad5a40ddfcadc8f4b9f92b1102d8ae20ca3bda
size 47644 size 49821

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9bd6fb63059cebc7dc7b850b5aa73549ab9846eda68b1b6d9a4f8e0716d42c3c oid sha256:4cdcc38bfae43298654c3e09d7ab1ca1e0d2fd32157a464539a360f3943f4f75
size 40419 size 42839

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6b4e4a075bcb4b95455c27ce2e5f9f1bdac4a1aca22ad944dd53fdbaeb6ca970 oid sha256:a56b3e488ceecd0bfb763d60b6827f7d8c460fa198d380a502ff0d97b13c9bc0
size 44550 size 45962

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1a2bc7e9098301d17e72a61a7baf01e52aa11566e1da4ffe3b43a66fa37652b2 oid sha256:600f28097c6ef8c54fe136d9bb05b8b800d693bff23f238a4e3e0216ee9918ff
size 42103 size 44404

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f097af2773ff2ecf70a69e4292dc118b4c8616f1c8f979e7d121e86a16ef3072 oid sha256:054cfe9290f91cf0f5d17e8f7c49e71eb50a2bb17067b1edc3c29f89410f76e8
size 38506 size 40806

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8f49870a5333cbaf1c6c3ac5ddcb85290d04efc79ffec93d8dc39b59f9712820 oid sha256:814bb524af65d077e3610e8cb920e5207aa808111a530357e8687831bd9c0413
size 42391 size 44687

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:883f160afa3fc3d2c0be5098205ea616875084ee0f122ebf994d0fee53a8ecd1 oid sha256:4da93eb0b9fcea861ae80d6e14bc852acaacea52402b1b41e8c982b9c06b2b63
size 39847 size 42105

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7790c892daeee8910ce17caac2c957d09fffec0d41a51b3adc1d5bef8dcee1a1 oid sha256:4886407cfe23d5d16aadeef4226698957dad99dfc4d9ac5c184fe64347b3ee41
size 42261 size 44524

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:bc7b733680a0d86ee1231345e58641fb3a208da21de62a50f624d9ae04c6e140 oid sha256:3c0d8491dbf0b6f50ae6be5c7aaf0d77b9eaa3a31e7763f120d6e08993c1c74b
size 31661 size 34028

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:20504466241e36817557812f6b378aceab9c2e271a596bd3d037c6be41af7c54 oid sha256:64a6d1a6af14176c933387a7a36de0d7392b1cd31cb04fcc5c6a282317874aac
size 23624 size 26597

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:48028e4c3ca7e9b871683165f029430bcd4e0fc2411e4ffc83a93abb641d96a2 oid sha256:f92ebc24de4dd34b84fcbbcc656a369d4a79bca160666c2fd4d32b5516c2f8fb
size 45132 size 47245

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b040fe47521f5d9d76d7892412bb2b5cf7e2b93b951f5de5f772503797fb6b24 oid sha256:2a4a3730a941a4c6863c116b401ea7e4d5c2cef3fe4098e353954c4f93aa660a
size 46548 size 48771

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:6db046a97f14f2df3db2415d71a02ffd6e706fe73def67c21f0d844be279da59 oid sha256:77521de94c04228ca0fb5d09cb7809527cf0dc0acc52a271a8fa4738a752aa66
size 39617 size 42028

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:7ee53243923d7b5adcc2094d3fe6bb87be00a179cae11966dcb230f2c3e8246e oid sha256:171ef67bdc5a1b6d921ecbdb13e3c79dfab073d7289862181a69515e6063c4c5
size 43681 size 45246

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b88e54ba4743e1e245b5a4c7d6b4045f816241f6cfc8716b8cede0b62666222b oid sha256:1a8328afeab339104a83db16d7b4b894c060ff7e80c2b5c4d6ec64d3bc4cdc3a
size 41286 size 43613

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d88b4d82bb0d416aff7b32647479181ac9702c36c08370e7671d6e5ef80687c7 oid sha256:d575d08141bf446de510162f5dc1e04fccf4831649495bdf94945bd8957768a6
size 37825 size 40201

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:b3d4dd1766a3f46acd86bdd5ec920c56a5e5f897c81c1477df10ba59dcd3d5b7 oid sha256:606379fcb517afa4583ac03fcb2af4f5649a7fc064df841f442c19f5a71f629b
size 41554 size 43863

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:842fbea74e53c8804fd0a3e9fe96dcb21b5c91a099e1de33d8ec983fd9a8a80a oid sha256:d63dec242ea479beb9a7bf0a58c6f31e2bba84df0a7db51fb07ea46612944d9a
size 39112 size 41355

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:1311ec5e008b44d81e6556164f780c57dea3e4721cc47caccdbf337dd4027eb7 oid sha256:f5421b7b5ce8441213b1323b4cd88c2f5aff6689ecd091b5be581fe5690cd611
size 41402 size 43685

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d2cf27d2f053f33c67d944a8f45e0206165b2db2f3b959eb9e0bb943f84fe6a1 oid sha256:e1f4600552aebd3a567251a85105a423882942b23e96f7958238a9b5ba0bb8fc
size 30657 size 33137

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d0506879a20bd64cb3a4ea41c93dfa78da1ca3b0c2728ce3044caa56f6648584
size 105611

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2e78aa3521464a53c000298dbd4ef51d4d0ea1d3c75b7bb8dcbd933701bab36b
size 84060

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f9eec4fc7e72588f957cd7d741e29b8eaed71555fc0d66c43e6ae69cc64b924c
size 87650

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d4ae4608296b5bb24c128572cc6b80379a9e3cd12ec89db9693c301e427f6ae5
size 82330

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e125730c22fcbc843fc0445e0af06d48b7dc31896053b5a7341b6dde84526eb8
size 82540

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4f3557280a4010e7ffdb6f22c11561573780c0b24c27e264073d3a3899169014 oid sha256:2c7ba307cf21056623bf35c8558809fdbc6deaacf4e9365a99ffcf829e8d9188
size 29659 size 34477

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:63efb9744640b71cfde672821c7c1ea8b33f9c3c2e2ad4d5f41149b9749b31c2 oid sha256:724bff130c547e7f0065ccb4c1b3319162ded2e6c1c1db666f4e08e01289a5a0
size 28016 size 32776

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:983e505a211c92ae92e091f9ba7cc43a655d5f3ce6d6bcf70971d43984507326 oid sha256:25ebb7460550b31bf2cebfca7bdab32e5f89e327b68f6687bf490e5d14cb9220
size 36153 size 40950

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4e7a43195bd617ee952ea084e6b17a7e08e8b3634e8f3ce7df6e98f067ab08dc oid sha256:39ecc9a50285df492417f5f22adcc391be2dcad0cc388efb756274c83aba077d
size 34296 size 39030

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8a6916d2441316cb3ef55ee4c6a3b3ba9134d246d72c27aa3871408a6b9e59fc oid sha256:2d5f183f53f9e8d0dbbae473f2f853f4372dbff15b1d6ea17e78b4770781fa34
size 30716 size 35468

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" : [