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:
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -37,6 +37,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||
event = aTimelineItemEvent(
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(),
|
||||
@@ -49,6 +50,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||
displayNameAmbiguous = true,
|
||||
timelineItemReactions = reactionsState,
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(
|
||||
@@ -62,6 +64,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||
content = aTimelineItemVideoContent(),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(
|
||||
@@ -75,6 +78,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||
content = aTimelineItemFileContent(),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(
|
||||
@@ -88,6 +92,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||
content = aTimelineItemAudioContent(),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(
|
||||
@@ -101,6 +106,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||
content = aTimelineItemVoiceContent(caption = null),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(
|
||||
@@ -114,6 +120,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||
content = aTimelineItemLocationContent(),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(),
|
||||
@@ -125,6 +132,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||
content = aTimelineItemLocationContent(),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = false,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(),
|
||||
@@ -136,6 +144,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||
content = aTimelineItemPollContent(),
|
||||
timelineItemReactions = reactionsState
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = false,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemPollActionList(),
|
||||
@@ -147,6 +156,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||
timelineItemReactions = reactionsState,
|
||||
messageShield = MessageShield.UnknownDevice(isCritical = true)
|
||||
),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = VerifiedUserSendFailure.None,
|
||||
actions = aTimelineItemActionList(),
|
||||
@@ -155,6 +165,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
|
||||
anActionListState(
|
||||
target = ActionListState.Target.Success(
|
||||
event = aTimelineItemEvent(),
|
||||
sentTimeFull = "January 1, 1970 at 12:00 AM",
|
||||
displayEmojiReactions = true,
|
||||
verifiedUserSendFailure = anUnsignedDeviceSendFailure(),
|
||||
actions = aTimelineItemActionList(),
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
),
|
||||
)
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
import kotlinx.datetime.Instant
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@Config(qualifiers = "fr")
|
||||
class DefaultDateFormatterFrTest {
|
||||
@Test
|
||||
fun `test null`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val ts: Long? = null
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts)).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test epoch`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val ts = 0L
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("1 janvier 1970 à 00:00")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Janvier 1970")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("1 janvier 1970")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("01.01.1970")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("00:00")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test epoch relative`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val ts = 0L
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("1 janvier 1970 à 00:00")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Janvier 1970")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("1 janvier 1970")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("01.01.1970")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("00:00")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test now`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1980 à 18:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Dimanche 6 avril")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("18:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test now relative`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("18:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Aujourd’hui")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("18:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one second before`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T18:35:23.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1980 à 18:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Dimanche 6 avril")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("18:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one second before relative`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T18:35:23.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("18:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Aujourd’hui")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("18:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one minute before`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T18:34:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1980 à 18:34")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Dimanche 6 avril")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("18:34")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:34")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one minute before relative`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T18:34:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("18:34")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Aujourd’hui")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("18:34")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:34")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one hour before`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T17:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1980 à 17:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Dimanche 6 avril")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("17:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("17:35")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one hour before relative`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T17:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("17:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Aujourd’hui")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("17:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("17:35")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one day before same time`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-05T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("5 avril 1980 à 18:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Samedi 5 avril")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("5 avr.")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one day before same time relative`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-05T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Hier à 18:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Hier")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("Hier")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test two days before same time`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-04T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("4 avril 1980 à 18:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1980")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Vendredi 4 avril")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("4 avr.")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test two days before same time relative`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-04T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Vendredi à 18:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Ce mois-ci")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Vendredi")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("4 avr.")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one month before same time`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-03-06T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 mars 1980 à 18:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Mars 1980")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Jeudi 6 mars")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6 mars")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one month before same time relative`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-03-06T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Jeudi 6 mars à 18:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Mars 1980")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Jeudi 6 mars")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6 mars")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one year before same time`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1979-04-06T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("6 avril 1979 à 18:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("Avril 1979")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("6 avril 1979")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("06.04.1979")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("18:35")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one year before same time relative`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1979-04-06T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("6 avril 1979 à 18:35")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("Avril 1979")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("6 avril 1979")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("06.04.1979")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("18:35")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.dateformatter.api.DateFormatterMode
|
||||
import kotlinx.datetime.Instant
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@Config(qualifiers = "en")
|
||||
class DefaultDateFormatterTest {
|
||||
@Test
|
||||
fun `test null`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val ts: Long? = null
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts)).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test epoch`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val ts = 0L
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("January 1, 1970 at 12:00 AM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("January 1970")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("January 1, 1970")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("01.01.1970")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("12:00 AM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test epoch relative`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val ts = 0L
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("January 1, 1970 at 12:00 AM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("January 1970")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("January 1, 1970")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("01.01.1970")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("12:00 AM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test now`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 6:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test now relative`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("6:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Today")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one second before`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T18:35:23.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 6:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one second before relative`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T18:35:23.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("6:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Today")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one minute before`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T18:34:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 6:34 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6:34 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:34 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one minute before relative`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T18:34:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("6:34 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Today")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6:34 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:34 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one hour before`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T17:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1980 at 5:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Sunday 6 April")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("5:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("5:35 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one hour before relative`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T17:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("5:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Today")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("5:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("5:35 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one day before same time`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-05T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 5, 1980 at 6:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Saturday 5 April")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("5 Apr")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one day before same time relative`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-05T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Yesterday at 6:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Yesterday")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("Yesterday")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test two days before same time`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-04T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 4, 1980 at 6:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1980")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Friday 4 April")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("4 Apr")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test two days before same time relative`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-04T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Friday at 6:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("This month")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Friday")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("4 Apr")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one month before same time`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-03-06T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("March 6, 1980 at 6:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("March 1980")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("Thursday 6 March")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("6 Mar")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one month before same time relative`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-03-06T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("Thursday 6 March at 6:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("March 1980")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("Thursday 6 March")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("6 Mar")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one year before same time`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1979-04-06T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full)).isEqualTo("April 6, 1979 at 6:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month)).isEqualTo("April 1979")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day)).isEqualTo("April 6, 1979")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate)).isEqualTo("06.04.1979")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly)).isEqualTo("6:35 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one year before same time relative`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1979-04-06T18:35:24.00Z"
|
||||
val ts = Instant.parse(dat).toEpochMilliseconds()
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Full, true)).isEqualTo("April 6, 1979 at 6:35 PM")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Month, true)).isEqualTo("April 1979")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.Day, true)).isEqualTo("April 6, 1979")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOrDate, true)).isEqualTo("06.04.1979")
|
||||
assertThat(formatter.format(ts, DateFormatterMode.TimeOnly, true)).isEqualTo("6:35 PM")
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
/*
|
||||
* Copyright 2023, 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.dateformatter.impl
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||
import io.element.android.libraries.dateformatter.test.FakeClock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import org.junit.Test
|
||||
import java.util.Locale
|
||||
|
||||
class DefaultLastMessageTimestampFormatterTest {
|
||||
@Test
|
||||
fun `test null`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(null)).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test epoch`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(0)).isEqualTo("01.01.1970")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test now`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T18:35:24.00Z"
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("6:35 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one second before`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T18:35:23.00Z"
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("6:35 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one minute before`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T18:34:24.00Z"
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("6:34 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one hour before`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-06T17:35:24.00Z"
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("5:35 PM")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one day before same time`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-04-05T18:35:24.00Z"
|
||||
val formatter = createFormatter(now)
|
||||
// TODO DateUtils.getRelativeTimeSpanString returns null.
|
||||
assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one month before same time`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1980-03-06T18:35:24.00Z"
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("6 Mar")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test one year before same time`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1979-04-06T18:35:24.00Z"
|
||||
val formatter = createFormatter(now)
|
||||
assertThat(formatter.format(Instant.parse(dat).toEpochMilliseconds())).isEqualTo("06.04.1979")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test full format`() {
|
||||
val now = "1980-04-06T18:35:24.00Z"
|
||||
val dat = "1979-04-06T18:35:24.00Z"
|
||||
val clock = FakeClock().apply { givenInstant(Instant.parse(now)) }
|
||||
val dateFormatters = DateFormatters(Locale.US, clock) { TimeZone.UTC }
|
||||
assertThat(dateFormatters.formatDateWithFullFormat(Instant.parse(dat).toLocalDateTime(TimeZone.UTC))).isEqualTo("Friday, April 6, 1979")
|
||||
}
|
||||
|
||||
/**
|
||||
* Create DefaultLastMessageFormatter and set current time to the provided date.
|
||||
*/
|
||||
private fun createFormatter(@Suppress("SameParameterValue") currentDate: String): LastMessageTimestampFormatter {
|
||||
val clock = FakeClock().apply { givenInstant(Instant.parse(currentDate)) }
|
||||
val localDateTimeProvider = LocalDateTimeProvider(clock) { TimeZone.UTC }
|
||||
val dateFormatters = DateFormatters(Locale.US, clock) { TimeZone.UTC }
|
||||
return DefaultLastMessageTimestampFormatter(localDateTimeProvider, dateFormatters)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -235,7 +235,7 @@ class RustMatrixRoom(
|
||||
RoomMessageEventMessageType.VIDEO,
|
||||
RoomMessageEventMessageType.AUDIO,
|
||||
),
|
||||
dateDividerMode = DateDividerMode.DAILY,
|
||||
dateDividerMode = DateDividerMode.MONTHLY,
|
||||
).let { inner ->
|
||||
createTimeline(inner, mode = Timeline.Mode.MEDIA)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -53,6 +53,7 @@ class DefaultMediaViewerEntryPoint @Inject constructor() : MediaViewerEntryPoint
|
||||
senderName = null,
|
||||
senderAvatar = null,
|
||||
dateSent = null,
|
||||
dateSentFull = null,
|
||||
),
|
||||
mediaSource = MediaSource(url = avatarUrl),
|
||||
thumbnailSource = null,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ class FakeLocalMediaFactory(
|
||||
senderId = null,
|
||||
senderName = null,
|
||||
senderAvatar = null,
|
||||
dateSent = null
|
||||
dateSent = null,
|
||||
dateSentFull = null,
|
||||
)
|
||||
return aLocalMedia(uri, mediaInfo)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -80,6 +80,12 @@
|
||||
".*voice_message_tooltip"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:dateformatter:impl",
|
||||
"includeRegex" : [
|
||||
"common\\.date\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":libraries:permissions:api",
|
||||
"includeRegex" : [
|
||||
|
||||
Reference in New Issue
Block a user