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

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

View file

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

View file

@ -10,12 +10,15 @@ package io.element.android.libraries.mediaviewer.impl.details
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.mediaviewer.api.anImageMediaInfo
fun aMediaDetailsBottomSheetState(): MediaBottomSheetState.MediaDetailsBottomSheetState {
fun aMediaDetailsBottomSheetState(
dateSentFull: String = "December 6, 2024 at 12:59",
): MediaBottomSheetState.MediaDetailsBottomSheetState {
return MediaBottomSheetState.MediaDetailsBottomSheetState(
eventId = EventId("\$eventId"),
canDelete = true,
mediaInfo = anImageMediaInfo(
senderName = "Alice",
dateSentFull = dateSentFull,
),
thumbnailSource = null,
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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