Let the Presenter use real classes.

This commit is contained in:
Benoit Marty
2024-12-10 15:56:54 +01:00
parent 7a3bd38c96
commit 57fc19d545
11 changed files with 45 additions and 145 deletions

View File

@@ -51,6 +51,7 @@ dependencies {
implementation(projects.libraries.di)
implementation(projects.libraries.matrix.api)
testImplementation(projects.libraries.dateformatter.test)
testImplementation(projects.libraries.featureflag.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.mediaviewer.test)

View File

@@ -7,10 +7,8 @@
package io.element.android.libraries.mediaviewer.impl.gallery
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.androidutils.filesize.FileSizeFormatter
import io.element.android.libraries.dateformatter.api.toHumanReadableDuration
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType
import io.element.android.libraries.matrix.api.timeline.item.event.CallNotifyContent
@@ -45,18 +43,13 @@ import java.text.DateFormat
import java.util.Date
import javax.inject.Inject
interface EventItemFactory {
fun create(currentTimelineItem: MatrixTimelineItem.Event): MediaItem.Event?
}
@ContributesBinding(RoomScope::class)
class DefaultEventItemFactory @Inject constructor(
class EventItemFactory @Inject constructor(
private val fileSizeFormatter: FileSizeFormatter,
private val fileExtensionExtractor: FileExtensionExtractor,
) : EventItemFactory {
) {
private val timeFormatter = DateFormat.getDateInstance()
override fun create(
fun create(
currentTimelineItem: MatrixTimelineItem.Event,
): MediaItem.Event? {
val event = currentTimelineItem.event

View File

@@ -7,23 +7,14 @@
package io.element.android.libraries.mediaviewer.impl.gallery
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.di.RoomScope
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import javax.inject.Inject
interface MediaItemsPostProcessor {
class MediaItemsPostProcessor @Inject constructor() {
fun process(
mediaItems: AsyncData<ImmutableList<MediaItem>>,
): AsyncData<GroupedMediaItems>
}
@ContributesBinding(RoomScope::class)
class DefaultMediaItemsPostProcessor @Inject constructor() : MediaItemsPostProcessor {
override fun process(
mediaItems: AsyncData<ImmutableList<MediaItem>>,
): AsyncData<GroupedMediaItems> {
return when (mediaItems) {
is AsyncData.Uninitialized -> AsyncData.Uninitialized

View File

@@ -7,12 +7,10 @@
package io.element.android.libraries.mediaviewer.impl.gallery
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.androidutils.diff.DefaultDiffCacheInvalidator
import io.element.android.libraries.androidutils.diff.DiffCacheUpdater
import io.element.android.libraries.androidutils.diff.MutableListDiffCache
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
@@ -24,17 +22,11 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import javax.inject.Inject
interface TimelineMediaItemsFactory {
val timelineItems: Flow<ImmutableList<MediaItem>>
suspend fun replaceWith(timelineItems: List<MatrixTimelineItem>)
}
@ContributesBinding(RoomScope::class)
class DefaultTimelineMediaItemsFactory @Inject constructor(
class TimelineMediaItemsFactory @Inject constructor(
private val dispatchers: CoroutineDispatchers,
private val virtualItemFactory: VirtualItemFactory,
private val eventItemFactory: EventItemFactory,
) : TimelineMediaItemsFactory {
) {
private val _timelineItems = MutableSharedFlow<ImmutableList<MediaItem>>(replay = 1)
private val lock = Mutex()
private val diffCache = MutableListDiffCache<MediaItem>()
@@ -50,9 +42,9 @@ class DefaultTimelineMediaItemsFactory @Inject constructor(
}
}
override val timelineItems: Flow<ImmutableList<MediaItem>> = _timelineItems.distinctUntilChanged()
val timelineItems: Flow<ImmutableList<MediaItem>> = _timelineItems.distinctUntilChanged()
override suspend fun replaceWith(
suspend fun replaceWith(
timelineItems: List<MatrixTimelineItem>,
) = withContext(dispatchers.computation) {
lock.withLock {

View File

@@ -7,22 +7,15 @@
package io.element.android.libraries.mediaviewer.impl.gallery
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
import javax.inject.Inject
interface VirtualItemFactory {
fun create(timelineItem: MatrixTimelineItem.Virtual): MediaItem?
}
@ContributesBinding(RoomScope::class)
class DefaultVirtualItemFactory @Inject constructor(
class VirtualItemFactory @Inject constructor(
private val daySeparatorFormatter: DaySeparatorFormatter,
) : VirtualItemFactory {
override fun create(timelineItem: MatrixTimelineItem.Virtual): MediaItem? {
) {
fun create(timelineItem: MatrixTimelineItem.Virtual): MediaItem? {
return when (val virtual = timelineItem.virtual) {
is VirtualTimelineItem.DayDivider -> MediaItem.DateSeparator(
id = timelineItem.uniqueId,

View File

@@ -55,7 +55,7 @@ import kotlin.time.Duration.Companion.seconds
class DefaultEventItemFactoryTest {
@Test
fun `create check all null cases`() {
val factory = createDefaultEventItemFactory()
val factory = createEventItemFactory()
val contents = listOf(
CallNotifyContent,
FailedToParseMessageLikeContent("", ""),
@@ -100,7 +100,7 @@ class DefaultEventItemFactoryTest {
@Test
fun `create MessageContent check all null cases`() {
val factory = createDefaultEventItemFactory()
val factory = createEventItemFactory()
val messageTypes = listOf(
EmoteMessageType("", null),
NoticeMessageType("", null),
@@ -125,7 +125,7 @@ class DefaultEventItemFactoryTest {
@Test
fun `create for FileMessageType`() {
val factory = createDefaultEventItemFactory()
val factory = createEventItemFactory()
val result = factory.create(
MatrixTimelineItem.Event(
uniqueId = A_UNIQUE_ID,
@@ -169,7 +169,7 @@ class DefaultEventItemFactoryTest {
@Test
fun `create for ImageMessageType`() {
val factory = createDefaultEventItemFactory()
val factory = createEventItemFactory()
val result = factory.create(
MatrixTimelineItem.Event(
uniqueId = A_UNIQUE_ID,
@@ -217,7 +217,7 @@ class DefaultEventItemFactoryTest {
@Test
fun `create for AudioMessageType`() {
val factory = createDefaultEventItemFactory()
val factory = createEventItemFactory()
val result = factory.create(
MatrixTimelineItem.Event(
uniqueId = A_UNIQUE_ID,
@@ -260,7 +260,7 @@ class DefaultEventItemFactoryTest {
@Test
fun `create for VideoMessageType`() {
val factory = createDefaultEventItemFactory()
val factory = createEventItemFactory()
val result = factory.create(
MatrixTimelineItem.Event(
uniqueId = A_UNIQUE_ID,
@@ -310,7 +310,7 @@ class DefaultEventItemFactoryTest {
@Test
fun `create for VoiceMessageType`() {
val factory = createDefaultEventItemFactory()
val factory = createEventItemFactory()
val result = factory.create(
MatrixTimelineItem.Event(
uniqueId = A_UNIQUE_ID,
@@ -357,7 +357,7 @@ class DefaultEventItemFactoryTest {
@Test
fun `create for StickerMessageType`() {
val factory = createDefaultEventItemFactory()
val factory = createEventItemFactory()
val result = factory.create(
MatrixTimelineItem.Event(
uniqueId = A_UNIQUE_ID,
@@ -404,7 +404,7 @@ class DefaultEventItemFactoryTest {
}
}
private fun createDefaultEventItemFactory() = DefaultEventItemFactory(
private fun createEventItemFactory() = EventItemFactory(
fileSizeFormatter = FakeFileSizeFormatter(),
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
)

View File

@@ -1,16 +0,0 @@
/*
* 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.mediaviewer.impl.gallery
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
class FakeEventItemFactory : EventItemFactory {
override fun create(currentTimelineItem: MatrixTimelineItem.Event): MediaItem.Event? {
return null
}
}

View File

@@ -1,23 +0,0 @@
/*
* 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.mediaviewer.impl.gallery
import io.element.android.libraries.architecture.AsyncData
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
class FakeMediaItemsPostProcessor : MediaItemsPostProcessor {
override fun process(mediaItems: AsyncData<ImmutableList<MediaItem>>): AsyncData<GroupedMediaItems> {
return AsyncData.Success(
GroupedMediaItems(
imageAndVideoItems = persistentListOf(),
fileItems = persistentListOf()
)
)
}
}

View File

@@ -1,26 +0,0 @@
/*
* 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.mediaviewer.impl.gallery
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.tests.testutils.lambda.lambdaError
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
class FakeTimelineMediaItemsFactory(
private val replaceWithLambda: (List<MatrixTimelineItem>) -> Unit = { lambdaError() },
) : TimelineMediaItemsFactory {
override val timelineItems: Flow<ImmutableList<MediaItem>>
get() = flowOf(emptyList<MediaItem>().toImmutableList())
override suspend fun replaceWith(timelineItems: List<MatrixTimelineItem>) {
replaceWithLambda(timelineItems)
}
}

View File

@@ -1,16 +0,0 @@
/*
* 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.mediaviewer.impl.gallery
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
class FakeVirtualItemFactory : VirtualItemFactory {
override fun create(timelineItem: MatrixTimelineItem.Virtual): MediaItem? {
return null
}
}

View File

@@ -9,10 +9,11 @@ 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.FakeDaySeparatorFormatter
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
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_USER_ID
@@ -24,12 +25,15 @@ import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetSta
import io.element.android.libraries.mediaviewer.impl.gallery.ui.aMediaItemImage
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaActions
import io.element.android.libraries.mediaviewer.test.FakeLocalMediaFactory
import io.element.android.libraries.mediaviewer.test.util.FileExtensionExtractorWithoutValidation
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import io.element.android.tests.testutils.testCoroutineDispatchers
import io.mockk.mockk
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@@ -55,7 +59,7 @@ class MediaGalleryPresenterTest {
)
)
presenter.test {
skipItems(1)
skipItems(2)
val initialState = awaitItem()
assertThat(initialState.mode).isEqualTo(MediaGalleryMode.Images)
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
@@ -84,7 +88,7 @@ class MediaGalleryPresenterTest {
)
)
presenter.test {
skipItems(1)
skipItems(2)
val initialState = awaitItem()
assertThat(initialState.mode).isEqualTo(MediaGalleryMode.Images)
initialState.eventSink(MediaGalleryEvents.ChangeMode(MediaGalleryMode.Files))
@@ -106,7 +110,7 @@ class MediaGalleryPresenterTest {
`present - bottom sheet state - own message`(canDeleteOwn = false)
}
private suspend fun `present - bottom sheet state - own message`(canDeleteOwn: Boolean) {
private suspend fun TestScope.`present - bottom sheet state - own message`(canDeleteOwn: Boolean) {
val presenter = createMediaGalleryPresenter(
room = FakeMatrixRoom(
sessionId = A_USER_ID,
@@ -116,7 +120,7 @@ class MediaGalleryPresenterTest {
)
)
presenter.test {
skipItems(1)
skipItems(2)
val initialState = awaitItem()
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
val item = aMediaItemImage(
@@ -150,7 +154,7 @@ class MediaGalleryPresenterTest {
`present - bottom sheet state - other message`(canDeleteOther = false)
}
private suspend fun `present - bottom sheet state - other message`(canDeleteOther: Boolean) {
private suspend fun TestScope.`present - bottom sheet state - other message`(canDeleteOther: Boolean) {
val presenter = createMediaGalleryPresenter(
room = FakeMatrixRoom(
sessionId = A_USER_ID,
@@ -160,7 +164,7 @@ class MediaGalleryPresenterTest {
)
)
presenter.test {
skipItems(1)
skipItems(2)
val initialState = awaitItem()
assertThat(initialState.mediaBottomSheetState).isEqualTo(MediaBottomSheetState.Hidden)
val item = aMediaItemImage(
@@ -193,7 +197,7 @@ class MediaGalleryPresenterTest {
)
)
presenter.test {
skipItems(1)
skipItems(2)
val initialState = awaitItem()
// Delete bottom sheet
val item = aMediaItemImage()
@@ -226,14 +230,14 @@ class MediaGalleryPresenterTest {
navigator = navigator,
)
presenter.test {
skipItems(1)
skipItems(2)
val initialState = awaitItem()
initialState.eventSink(MediaGalleryEvents.ViewInTimeline(AN_EVENT_ID))
onViewInTimelineClickLambda.assertions().isCalledOnce().with(value(AN_EVENT_ID))
}
}
private fun createMediaGalleryPresenter(
private fun TestScope.createMediaGalleryPresenter(
matrixMediaLoader: FakeMatrixMediaLoader = FakeMatrixMediaLoader(),
localMediaActions: FakeLocalMediaActions = FakeLocalMediaActions(),
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
@@ -245,14 +249,21 @@ class MediaGalleryPresenterTest {
return MediaGalleryPresenter(
navigator = navigator,
room = room,
timelineMediaItemsFactory = FakeTimelineMediaItemsFactory(
replaceWithLambda = lambdaRecorder<List<MatrixTimelineItem>, Unit> { _ -> },
timelineMediaItemsFactory = TimelineMediaItemsFactory(
dispatchers = testCoroutineDispatchers(),
virtualItemFactory = VirtualItemFactory(
daySeparatorFormatter = FakeDaySeparatorFormatter(),
),
eventItemFactory = EventItemFactory(
fileSizeFormatter = FakeFileSizeFormatter(),
fileExtensionExtractor = FileExtensionExtractorWithoutValidation(),
),
),
localMediaFactory = localMediaFactory,
mediaLoader = matrixMediaLoader,
localMediaActions = localMediaActions,
snackbarDispatcher = snackbarDispatcher,
mediaItemsPostProcessor = FakeMediaItemsPostProcessor(),
mediaItemsPostProcessor = MediaItemsPostProcessor(),
)
}
}