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 d5b3eea824
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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
- tapOn: "Back"
- scroll
- scroll
- tapOn: "Leave room"
- 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.operation.hide
import io.element.android.libraries.architecture.overlay.operation.show
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
@ -97,6 +99,7 @@ class MessagesFlowNode @AssistedInject constructor(
private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider,
private val timelineController: TimelineController,
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
private val dateFormatter: DateFormatter,
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
backstack = BackStack(
initialElement = plugins.filterIsInstance<MessagesEntryPoint.Params>().first().initialTarget.toNavTarget(),
@ -436,7 +439,14 @@ class MessagesFlowNode @AssistedInject constructor(
senderId = event.senderId,
senderName = event.safeSenderName,
senderAvatar = event.senderAvatar.url,
dateSent = event.sentTime,
dateSent = dateFormatter.format(
event.sentTimeMillis,
mode = DateFormatterMode.Day,
),
dateSentFull = dateFormatter.format(
timestamp = event.sentTimeMillis,
mode = DateFormatterMode.Full,
),
),
mediaSource = mediaSource,
thumbnailSource = thumbnailSource,

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

View file

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

View file

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

View file

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

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

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.TimelineItemVirtualModel
import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import javax.inject.Inject
class TimelineItemDaySeparatorFactory @Inject constructor(private val daySeparatorFormatter: DaySeparatorFormatter) {
class TimelineItemDaySeparatorFactory @Inject constructor(
private val dateFormatter: DateFormatter,
) {
fun create(virtualItem: VirtualTimelineItem.DayDivider): TimelineItemVirtualModel {
val formattedDate = daySeparatorFormatter.format(virtualItem.timestamp)
val formattedDate = dateFormatter.format(
timestamp = virtualItem.timestamp,
mode = DateFormatterMode.Day,
useRelative = true,
)
return TimelineItemDaySeparatorModel(
formattedDate = formattedDate
)

View file

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

View file

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

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

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

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

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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
* Please see LICENSE in the repository root for full details.
@ -7,18 +7,16 @@
package io.element.android.libraries.dateformatter.impl
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultLastMessageTimestampFormatter @Inject constructor(
class DateFormatterTime @Inject constructor(
private val localDateTimeProvider: LocalDateTimeProvider,
private val dateFormatters: DateFormatters,
) : LastMessageTimestampFormatter {
override fun format(timestamp: Long?): String {
if (timestamp == null) return ""
) {
fun format(
timestamp: Long,
useRelative: Boolean,
): String {
val currentDate = localDateTimeProvider.providesNow()
val dateToFormat = localDateTimeProvider.providesFromTimestamp(timestamp)
val isSameDay = currentDate.date == dateToFormat.date
@ -30,7 +28,7 @@ class DefaultLastMessageTimestampFormatter @Inject constructor(
dateFormatters.formatDate(
dateToFormat = dateToFormat,
currentDate = currentDate,
useRelative = true
useRelative = useRelative,
)
}
}

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

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.
*/
package io.element.android.libraries.dateformatter.test
package io.element.android.libraries.dateformatter.impl
import kotlinx.datetime.Clock
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.AUDIO,
),
dateDividerMode = DateDividerMode.DAILY,
dateDividerMode = DateDividerMode.MONTHLY,
).let { inner ->
createTimeline(inner, mode = Timeline.Mode.MEDIA)
}

View file

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

View file

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

View file

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

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.mediaviewer.api.anImageMediaInfo
fun aMediaDetailsBottomSheetState(): MediaBottomSheetState.MediaDetailsBottomSheetState {
fun aMediaDetailsBottomSheetState(
dateSentFull: String = "December 6, 2024 at 12:59",
): MediaBottomSheetState.MediaDetailsBottomSheetState {
return MediaBottomSheetState.MediaDetailsBottomSheetState(
eventId = EventId("\$eventId"),
canDelete = true,
mediaInfo = anImageMediaInfo(
senderName = "Alice",
dateSentFull = dateSentFull,
),
thumbnailSource = null,
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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
oid sha256:e67a540966100272311381e87011149cdb15c8191a6f2bbc40d1febff999c431
size 24067
oid sha256:dbef9e8e887fa493ca9b875e64dc164bf35dd84ad25b84a1ad9b6fd523b26c38
size 27280

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d2cf27d2f053f33c67d944a8f45e0206165b2db2f3b959eb9e0bb943f84fe6a1
size 30657
oid sha256:e1f4600552aebd3a567251a85105a423882942b23e96f7958238a9b5ba0bb8fc
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
oid sha256:4f3557280a4010e7ffdb6f22c11561573780c0b24c27e264073d3a3899169014
size 29659
oid sha256:2c7ba307cf21056623bf35c8558809fdbc6deaacf4e9365a99ffcf829e8d9188
size 34477

View file

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

View file

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

View file

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

View file

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

View file

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