Implement month separator for the Gallery.

Improve day separator rendering in the timeline.
Use Today, Yesterday, and the name of the day if less than 7 days and do not render the year for the current year.
Improve date format for the media viewer.
Rework how date and time are computed.
ActionListView: Time can take more space, so update the layout.
This commit is contained in:
Benoit Marty 2024-12-11 23:43:20 +01:00 committed by Benoit Marty
parent 5515c938d4
commit da272ddb07
60 changed files with 1271 additions and 351 deletions

View file

@ -55,6 +55,8 @@ import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.overlay.Overlay
import io.element.android.libraries.architecture.overlay.operation.hide
import io.element.android.libraries.architecture.overlay.operation.show
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId
@ -97,6 +99,7 @@ class MessagesFlowNode @AssistedInject constructor(
private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider,
private val timelineController: TimelineController,
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
private val dateFormatter: DateFormatter,
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
backstack = BackStack(
initialElement = plugins.filterIsInstance<MessagesEntryPoint.Params>().first().initialTarget.toNavTarget(),
@ -436,7 +439,14 @@ class MessagesFlowNode @AssistedInject constructor(
senderId = event.senderId,
senderName = event.safeSenderName,
senderAvatar = event.senderAvatar.url,
dateSent = event.sentTime,
dateSent = dateFormatter.format(
event.sentTimeMillis,
mode = DateFormatterMode.Day,
),
dateSentFull = dateFormatter.format(
timestamp = event.sentTimeMillis,
mode = DateFormatterMode.Full,
),
),
mediaSource = mediaSource,
thumbnailSource = thumbnailSource,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -9,13 +9,20 @@ package io.element.android.features.messages.impl.timeline.factories.virtual
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
import io.element.android.libraries.dateformatter.api.DateFormatter
import io.element.android.libraries.dateformatter.api.DateFormatterMode
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import javax.inject.Inject
class TimelineItemDaySeparatorFactory @Inject constructor(private val daySeparatorFormatter: DaySeparatorFormatter) {
class TimelineItemDaySeparatorFactory @Inject constructor(
private val dateFormatter: DateFormatter,
) {
fun create(virtualItem: VirtualTimelineItem.DayDivider): TimelineItemVirtualModel {
val formattedDate = daySeparatorFormatter.format(virtualItem.timestamp)
val formattedDate = dateFormatter.format(
timestamp = virtualItem.timestamp,
mode = DateFormatterMode.Day,
useRelative = true,
)
return TimelineItemDaySeparatorModel(
formattedDate = formattedDate
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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