Merge pull request #4274 from element-hq/feature/bma/mediaTimelineImprovment

Update Matrix Room API and allow media swipe on pinned event only.
This commit is contained in:
Benoit Marty
2025-02-19 09:41:27 +01:00
committed by GitHub
31 changed files with 296 additions and 185 deletions

View File

@@ -71,6 +71,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.alias.matches
import io.element.android.libraries.matrix.api.room.joinedRoomMembers
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.ui.messages.LocalRoomMemberProfilesCache
import io.element.android.libraries.matrix.ui.messages.RoomMemberProfilesCache
@@ -187,8 +188,11 @@ class MessagesFlowNode @AssistedInject constructor(
callbacks.forEach { it.onRoomDetailsClick() }
}
override fun onEventClick(event: TimelineItem.Event): Boolean {
return processEventClick(event)
override fun onEventClick(isLive: Boolean, event: TimelineItem.Event): Boolean {
return processEventClick(
timelineMode = if (isLive) Timeline.Mode.LIVE else Timeline.Mode.FOCUSED_ON_EVENT,
event = event,
)
}
override fun onPreviewAttachments(attachments: ImmutableList<Attachment>) {
@@ -316,7 +320,10 @@ class MessagesFlowNode @AssistedInject constructor(
NavTarget.PinnedMessagesList -> {
val callback = object : PinnedMessagesListNode.Callback {
override fun onEventClick(event: TimelineItem.Event) {
processEventClick(event)
processEventClick(
timelineMode = Timeline.Mode.PINNED_EVENTS,
event = event,
)
}
override fun onUserDataClick(userId: UserId) {
@@ -358,11 +365,14 @@ class MessagesFlowNode @AssistedInject constructor(
callbacks.forEach { it.onPermalinkClick(permalinkData, pushToBackstack = false) }
}
private fun processEventClick(event: TimelineItem.Event): Boolean {
private fun processEventClick(
timelineMode: Timeline.Mode,
event: TimelineItem.Event,
): Boolean {
val navTarget = when (event.content) {
is TimelineItemImageContent -> {
buildMediaViewerNavTarget(
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos,
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(timelineMode),
event = event,
content = event.content,
mediaSource = event.content.mediaSource,
@@ -374,7 +384,7 @@ class MessagesFlowNode @AssistedInject constructor(
if encrypted on certain bridges */
event.content.preferredMediaSource?.let { preferredMediaSource ->
buildMediaViewerNavTarget(
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos,
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(timelineMode),
event = event,
content = event.content,
mediaSource = preferredMediaSource,
@@ -384,7 +394,7 @@ class MessagesFlowNode @AssistedInject constructor(
}
is TimelineItemVideoContent -> {
buildMediaViewerNavTarget(
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos,
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(timelineMode),
event = event,
content = event.content,
mediaSource = event.content.mediaSource,
@@ -393,7 +403,7 @@ class MessagesFlowNode @AssistedInject constructor(
}
is TimelineItemFileContent -> {
buildMediaViewerNavTarget(
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios,
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios(timelineMode),
event = event,
content = event.content,
mediaSource = event.content.mediaSource,
@@ -402,7 +412,7 @@ class MessagesFlowNode @AssistedInject constructor(
}
is TimelineItemAudioContent -> {
buildMediaViewerNavTarget(
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios,
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios(timelineMode),
event = event,
content = event.content,
mediaSource = event.content.mediaSource,

View File

@@ -89,7 +89,7 @@ class MessagesNode @AssistedInject constructor(
interface Callback : Plugin {
fun onRoomDetailsClick()
fun onEventClick(event: TimelineItem.Event): Boolean
fun onEventClick(isLive: Boolean, event: TimelineItem.Event): Boolean
fun onPreviewAttachments(attachments: ImmutableList<Attachment>)
fun onUserDataClick(userId: UserId)
fun onPermalinkClick(data: PermalinkData)
@@ -120,12 +120,12 @@ class MessagesNode @AssistedInject constructor(
callbacks.forEach { it.onRoomDetailsClick() }
}
private fun onEventClick(event: TimelineItem.Event): Boolean {
private fun onEventClick(isLive: Boolean, event: TimelineItem.Event): Boolean {
// Note: cannot use `callbacks.all { it.onEventClick(event) }` because:
// - if callbacks is empty, it will return true and we want to return false.
// - if a callback returns false, the other callback will not be invoked.
return callbacks.takeIf { it.isNotEmpty() }
?.map { it.onEventClick(event) }
?.map { it.onEventClick(isLive, event) }
?.all { it }
.orFalse()
}

View File

@@ -111,7 +111,7 @@ fun MessagesView(
state: MessagesState,
onBackClick: () -> Unit,
onRoomDetailsClick: () -> Unit,
onEventContentClick: (event: TimelineItem.Event) -> Boolean,
onEventContentClick: (isLive: Boolean, event: TimelineItem.Event) -> Boolean,
onUserDataClick: (UserId) -> Unit,
onLinkClick: (String, Boolean) -> Unit,
onSendLocationClick: () -> Unit,
@@ -142,7 +142,7 @@ fun MessagesView(
fun onContentClick(event: TimelineItem.Event) {
Timber.v("onMessageClick= ${event.id}")
val hideKeyboard = onEventContentClick(event)
val hideKeyboard = onEventContentClick(state.timelineState.isLive, event)
if (hideKeyboard) {
localView.hideKeyboard()
}
@@ -544,7 +544,7 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
state = state,
onBackClick = {},
onRoomDetailsClick = {},
onEventContentClick = { false },
onEventContentClick = { _, _ -> false },
onUserDataClick = {},
onLinkClick = { _, _ -> },
onSendLocationClick = {},

View File

@@ -33,7 +33,7 @@ internal fun MessagesViewWithIdentityChangePreview(
),
onBackClick = {},
onRoomDetailsClick = {},
onEventContentClick = { false },
onEventContentClick = { _, _ -> false },
onUserDataClick = {},
onLinkClick = { _, _ -> },
onSendLocationClick = {},

View File

@@ -14,6 +14,7 @@ import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.timeline.Timeline
@@ -104,7 +105,7 @@ class PinnedEventsTimelineProvider @Inject constructor(
is AsyncData.Uninitialized, is AsyncData.Failure -> {
timelineStateFlow.emit(AsyncData.Loading())
withContext(dispatchers.io) {
room.pinnedEventsTimeline()
room.createTimeline(CreateTimelineParams.PinnedOnly)
}
.fold(
{ timelineStateFlow.emit(AsyncData.Success(it)) },

View File

@@ -11,6 +11,7 @@ import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
import io.element.android.libraries.matrix.api.timeline.Timeline
@@ -64,7 +65,7 @@ class TimelineController @Inject constructor(
}
suspend fun focusOnEvent(eventId: EventId): Result<Unit> {
return room.timelineFocusedOnEvent(eventId)
return room.createTimeline(CreateTimelineParams.Focused(eventId))
.onFailure {
if (it is CancellationException) {
throw it

View File

@@ -54,11 +54,11 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.testtags.TestTags
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EnsureCalledOnceWithParam
import io.element.android.tests.testutils.EnsureCalledOnceWithTwoParamsAndResult
import io.element.android.tests.testutils.EnsureNeverCalled
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
import io.element.android.tests.testutils.EnsureNeverCalledWithParamAndResult
import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParams
import io.element.android.tests.testutils.EnsureNeverCalledWithTwoParamsAndResult
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.ensureCalledOnce
@@ -129,8 +129,9 @@ class MessagesViewTest {
eventSink = eventsRecorder
)
val timelineItem = state.timelineState.timelineItems.first()
val callback = EnsureCalledOnceWithParam(
expectedParam = timelineItem,
val callback = EnsureCalledOnceWithTwoParamsAndResult(
expectedParam1 = true,
expectedParam2 = timelineItem,
result = true,
)
rule.setMessagesView(
@@ -513,7 +514,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
state: MessagesState,
onBackClick: () -> Unit = EnsureNeverCalled(),
onRoomDetailsClick: () -> Unit = EnsureNeverCalled(),
onEventClick: (event: TimelineItem.Event) -> Boolean = EnsureNeverCalledWithParamAndResult(),
onEventClick: (isLive: Boolean, event: TimelineItem.Event) -> Boolean = EnsureNeverCalledWithTwoParamsAndResult(),
onUserDataClick: (UserId) -> Unit = EnsureNeverCalledWithParam(),
onLinkClick: (String, Boolean) -> Unit = EnsureNeverCalledWithTwoParams(),
onSendLocationClick: () -> Unit = EnsureNeverCalled(),

View File

@@ -55,7 +55,7 @@ class PinnedMessagesBannerPresenterTest {
@Test
fun `present - loading state`() = runTest {
val room = FakeMatrixRoom(
pinnedEventsTimelineResult = { Result.success(FakeTimeline()) }
createTimelineResult = { Result.success(FakeTimeline()) }
).apply {
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID)))
}
@@ -86,7 +86,7 @@ class PinnedMessagesBannerPresenterTest {
)
)
val room = FakeMatrixRoom(
pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }
createTimelineResult = { Result.success(pinnedEventsTimeline) }
).apply {
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID, AN_EVENT_ID_2)))
}
@@ -125,7 +125,7 @@ class PinnedMessagesBannerPresenterTest {
)
)
val room = FakeMatrixRoom(
pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) }
createTimelineResult = { Result.success(pinnedEventsTimeline) }
).apply {
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID, AN_EVENT_ID_2)))
}
@@ -160,7 +160,7 @@ class PinnedMessagesBannerPresenterTest {
@Test
fun `present - timeline failed`() = runTest {
val room = FakeMatrixRoom(
pinnedEventsTimelineResult = { Result.failure(Exception()) }
createTimelineResult = { Result.failure(Exception()) }
).apply {
givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID)))
}

View File

@@ -83,7 +83,7 @@ class PinnedMessagesListPresenterTest {
@Test
fun `present - timeline failure state`() = runTest {
val room = FakeMatrixRoom(
pinnedEventsTimelineResult = { Result.failure(RuntimeException()) },
createTimelineResult = { Result.failure(RuntimeException()) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserPinUnpinResult = { Result.success(true) },
@@ -102,7 +102,7 @@ class PinnedMessagesListPresenterTest {
@Test
fun `present - empty state`() = runTest {
val room = FakeMatrixRoom(
pinnedEventsTimelineResult = { Result.success(FakeTimeline()) },
createTimelineResult = { Result.success(FakeTimeline()) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserPinUnpinResult = { Result.success(true) },
@@ -122,7 +122,7 @@ class PinnedMessagesListPresenterTest {
fun `present - filled state`() = runTest {
val pinnedEventsTimeline = createPinnedMessagesTimeline()
val room = FakeMatrixRoom(
pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) },
createTimelineResult = { Result.success(pinnedEventsTimeline) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserPinUnpinResult = { Result.success(true) },
@@ -149,7 +149,7 @@ class PinnedMessagesListPresenterTest {
val pinnedEventsTimeline = createPinnedMessagesTimeline()
val analyticsService = FakeAnalyticsService()
val room = FakeMatrixRoom(
pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) },
createTimelineResult = { Result.success(pinnedEventsTimeline) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserPinUnpinResult = { Result.success(true) },
@@ -195,7 +195,7 @@ class PinnedMessagesListPresenterTest {
}
val pinnedEventsTimeline = createPinnedMessagesTimeline()
val room = FakeMatrixRoom(
pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) },
createTimelineResult = { Result.success(pinnedEventsTimeline) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserPinUnpinResult = { Result.success(true) },
@@ -224,7 +224,7 @@ class PinnedMessagesListPresenterTest {
}
val pinnedEventsTimeline = createPinnedMessagesTimeline()
val room = FakeMatrixRoom(
pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) },
createTimelineResult = { Result.success(pinnedEventsTimeline) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserPinUnpinResult = { Result.success(true) },
@@ -253,7 +253,7 @@ class PinnedMessagesListPresenterTest {
}
val pinnedEventsTimeline = createPinnedMessagesTimeline()
val room = FakeMatrixRoom(
pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) },
createTimelineResult = { Result.success(pinnedEventsTimeline) },
canRedactOwnResult = { Result.success(true) },
canRedactOtherResult = { Result.success(true) },
canUserPinUnpinResult = { Result.success(true) },

View File

@@ -31,7 +31,7 @@ class TimelineControllerTest {
val detachedTimeline = FakeTimeline(name = "detached")
val matrixRoom = FakeMatrixRoom(
liveTimeline = liveTimeline,
timelineFocusedOnEventResult = { Result.success(detachedTimeline) }
createTimelineResult = { Result.success(detachedTimeline) }
)
val sut = TimelineController(matrixRoom)
@@ -63,7 +63,7 @@ class TimelineControllerTest {
var callNumber = 0
val matrixRoom = FakeMatrixRoom(
liveTimeline = liveTimeline,
timelineFocusedOnEventResult = {
createTimelineResult = {
callNumber++
when (callNumber) {
1 -> Result.success(detachedTimeline1)
@@ -117,7 +117,7 @@ class TimelineControllerTest {
val detachedTimeline = FakeTimeline(name = "detached")
val matrixRoom = FakeMatrixRoom(
liveTimeline = liveTimeline,
timelineFocusedOnEventResult = { Result.success(detachedTimeline) }
createTimelineResult = { Result.success(detachedTimeline) }
)
val sut = TimelineController(matrixRoom)
sut.activeTimelineFlow().test {
@@ -167,7 +167,7 @@ class TimelineControllerTest {
}
val matrixRoom = FakeMatrixRoom(
liveTimeline = liveTimeline,
timelineFocusedOnEventResult = { Result.success(detachedTimeline) }
createTimelineResult = { Result.success(detachedTimeline) }
)
val sut = TimelineController(matrixRoom)
sut.activeTimelineFlow().test {
@@ -192,7 +192,7 @@ class TimelineControllerTest {
val detachedTimeline = FakeTimeline(name = "detached")
val matrixRoom = FakeMatrixRoom(
liveTimeline = liveTimeline,
timelineFocusedOnEventResult = { Result.success(detachedTimeline) }
createTimelineResult = { Result.success(detachedTimeline) }
)
val sut = TimelineController(matrixRoom)

View File

@@ -483,7 +483,7 @@ import kotlin.time.Duration.Companion.seconds
)
val room = FakeMatrixRoom(
liveTimeline = liveTimeline,
timelineFocusedOnEventResult = { Result.success(detachedTimeline) },
createTimelineResult = { Result.success(detachedTimeline) },
canUserSendMessageResult = { _, _ -> Result.success(true) },
)
val presenter = createTimelinePresenter(
@@ -561,7 +561,7 @@ import kotlin.time.Duration.Companion.seconds
liveTimeline = FakeTimeline(
timelineItems = flowOf(emptyList()),
),
timelineFocusedOnEventResult = { Result.failure(Throwable("An error")) },
createTimelineResult = { Result.failure(Throwable("An error")) },
canUserSendMessageResult = { _, _ -> Result.success(true) },
)
)

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.room
import io.element.android.libraries.matrix.api.core.EventId
sealed interface CreateTimelineParams {
data class Focused(val focusedEventId: EventId) : CreateTimelineParams
data object MediaOnly : CreateTimelineParams
data class MediaOnlyFocused(val focusedEventId: EventId) : CreateTimelineParams
data object PinnedOnly : CreateTimelineParams
}

View File

@@ -109,21 +109,12 @@ interface MatrixRoom : Closeable {
val liveTimeline: Timeline
/**
* Create a new timeline, focused on the provided Event.
* Should not be used directly, see `TimelineController` to manage the various timelines.
* Create a new timeline.
* @param createTimelineParams contains parameters about how to filter the timeline. Will also configure the date separators.
*/
suspend fun timelineFocusedOnEvent(eventId: EventId): Result<Timeline>
/**
* Create a new timeline for the pinned events of the room.
*/
suspend fun pinnedEventsTimeline(): Result<Timeline>
/**
* Create a new timeline for the media events of the room.
* @param eventId The event to focus on, if any.
*/
suspend fun mediaTimeline(eventId: EventId?): Result<Timeline>
suspend fun createTimeline(
createTimelineParams: CreateTimelineParams,
): Result<Timeline>
fun destroy()

View File

@@ -28,6 +28,7 @@ import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.IntentionalMention
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
@@ -214,80 +215,81 @@ class RustMatrixRoom(
override suspend fun subscribeToSync() = roomSyncSubscriber.subscribe(roomId)
override suspend fun timelineFocusedOnEvent(eventId: EventId): Result<Timeline> = withContext(roomDispatcher) {
runCatching {
innerRoom.timelineWithConfiguration(
configuration = TimelineConfiguration(
focus = TimelineFocus.Event(
eventId = eventId.value,
numContextEvents = 50u,
),
allowedMessageTypes = AllowedMessageTypes.All,
internalIdPrefix = "focus_$eventId",
dateDividerMode = DateDividerMode.DAILY,
)
).let { inner ->
createTimeline(inner, mode = Timeline.Mode.FOCUSED_ON_EVENT)
}
}.mapFailure {
it.toFocusEventException()
}.onFailure {
if (it is CancellationException) {
throw it
}
}
}
override suspend fun pinnedEventsTimeline(): Result<Timeline> = withContext(roomDispatcher) {
runCatching {
innerRoom.timelineWithConfiguration(
configuration = TimelineConfiguration(
focus = TimelineFocus.PinnedEvents(
maxEventsToLoad = 100u,
maxConcurrentRequests = 10u,
),
allowedMessageTypes = AllowedMessageTypes.All,
internalIdPrefix = "pinned_events",
dateDividerMode = DateDividerMode.DAILY,
)
).let { inner ->
createTimeline(inner, mode = Timeline.Mode.PINNED_EVENTS)
}
}.onFailure {
if (it is CancellationException) {
throw it
}
}
}
override suspend fun mediaTimeline(
eventId: EventId?,
override suspend fun createTimeline(
createTimelineParams: CreateTimelineParams,
): Result<Timeline> = withContext(roomDispatcher) {
val focus = if (eventId != null) {
TimelineFocus.Event(
eventId = eventId.value,
val focus = when (createTimelineParams) {
is CreateTimelineParams.PinnedOnly -> TimelineFocus.PinnedEvents(
maxEventsToLoad = 100u,
maxConcurrentRequests = 10u,
)
is CreateTimelineParams.MediaOnly -> TimelineFocus.Live
is CreateTimelineParams.Focused -> TimelineFocus.Event(
eventId = createTimelineParams.focusedEventId.value,
numContextEvents = 50u,
)
is CreateTimelineParams.MediaOnlyFocused -> TimelineFocus.Event(
eventId = createTimelineParams.focusedEventId.value,
numContextEvents = 50u,
)
} else {
TimelineFocus.Live
}
val allowedMessageTypes = when (createTimelineParams) {
is CreateTimelineParams.MediaOnly,
is CreateTimelineParams.MediaOnlyFocused -> AllowedMessageTypes.Only(
types = listOf(
RoomMessageEventMessageType.FILE,
RoomMessageEventMessageType.IMAGE,
RoomMessageEventMessageType.VIDEO,
RoomMessageEventMessageType.AUDIO,
)
)
is CreateTimelineParams.Focused,
CreateTimelineParams.PinnedOnly -> AllowedMessageTypes.All
}
val internalIdPrefix = when (createTimelineParams) {
is CreateTimelineParams.PinnedOnly -> "pinned_events"
is CreateTimelineParams.Focused -> "focus_${createTimelineParams.focusedEventId}"
is CreateTimelineParams.MediaOnly -> "MediaGallery_"
is CreateTimelineParams.MediaOnlyFocused -> "MediaGallery_${createTimelineParams.focusedEventId}"
}
// Note that for TimelineFilter.MediaOnlyFocused, the date separator will be filtered out,
// but there is no way to exclude data separator at the moment.
val dateDividerMode = when (createTimelineParams) {
is CreateTimelineParams.MediaOnly,
is CreateTimelineParams.MediaOnlyFocused -> DateDividerMode.MONTHLY
is CreateTimelineParams.Focused,
CreateTimelineParams.PinnedOnly -> DateDividerMode.DAILY
}
runCatching {
innerRoom.timelineWithConfiguration(
configuration = TimelineConfiguration(
focus = focus,
allowedMessageTypes = AllowedMessageTypes.Only(
types = listOf(
RoomMessageEventMessageType.FILE,
RoomMessageEventMessageType.IMAGE,
RoomMessageEventMessageType.VIDEO,
RoomMessageEventMessageType.AUDIO,
)
),
internalIdPrefix = "MediaGallery_",
dateDividerMode = DateDividerMode.MONTHLY,
allowedMessageTypes = allowedMessageTypes,
internalIdPrefix = internalIdPrefix,
dateDividerMode = dateDividerMode,
)
).let { inner ->
createTimeline(inner, mode = if (eventId != null) Timeline.Mode.FOCUSED_ON_EVENT else Timeline.Mode.MEDIA)
val mode = when (createTimelineParams) {
is CreateTimelineParams.Focused -> Timeline.Mode.FOCUSED_ON_EVENT
is CreateTimelineParams.MediaOnly -> Timeline.Mode.MEDIA
is CreateTimelineParams.MediaOnlyFocused -> Timeline.Mode.FOCUSED_ON_EVENT
CreateTimelineParams.PinnedOnly -> Timeline.Mode.PINNED_EVENTS
}
createTimeline(
timeline = inner,
mode = mode,
)
}
}.mapFailure {
when (createTimelineParams) {
is CreateTimelineParams.Focused,
is CreateTimelineParams.MediaOnlyFocused -> it.toFocusEventException()
CreateTimelineParams.MediaOnly,
CreateTimelineParams.PinnedOnly -> it
}
}.onFailure {
if (it is CancellationException) {

View File

@@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.media.MediaUploadHandler
import io.element.android.libraries.matrix.api.media.VideoInfo
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.poll.PollKind
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.IntentionalMention
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
@@ -138,9 +139,7 @@ class FakeMatrixRoom(
private val leaveRoomLambda: () -> Result<Unit> = { lambdaError() },
private val updateMembersResult: () -> Unit = { lambdaError() },
private val getMembersResult: (Int) -> Result<List<RoomMember>> = { lambdaError() },
private val timelineFocusedOnEventResult: (EventId) -> Result<Timeline> = { lambdaError() },
private val pinnedEventsTimelineResult: () -> Result<Timeline> = { lambdaError() },
private val mediaTimelineResult: (EventId?) -> Result<Timeline> = { lambdaError() },
private val createTimelineResult: (CreateTimelineParams) -> Result<Timeline> = { lambdaError() },
private val setSendQueueEnabledLambda: (Boolean) -> Unit = { _: Boolean -> },
private val saveComposerDraftLambda: (ComposerDraft) -> Result<Unit> = { _: ComposerDraft -> Result.success(Unit) },
private val loadComposerDraftLambda: () -> Result<ComposerDraft?> = { Result.success<ComposerDraft?>(null) },
@@ -220,16 +219,10 @@ class FakeMatrixRoom(
_syncUpdateFlow.tryEmit(_syncUpdateFlow.value + 1)
}
override suspend fun timelineFocusedOnEvent(eventId: EventId): Result<Timeline> = simulateLongTask {
timelineFocusedOnEventResult(eventId)
}
override suspend fun pinnedEventsTimeline(): Result<Timeline> = simulateLongTask {
pinnedEventsTimelineResult()
}
override suspend fun mediaTimeline(eventId: EventId?): Result<Timeline> = simulateLongTask {
mediaTimelineResult(eventId)
override suspend fun createTimeline(
createTimelineParams: CreateTimelineParams,
): Result<Timeline> = simulateLongTask {
createTimelineResult(createTimelineParams)
}
override suspend fun subscribeToSync() {

View File

@@ -7,6 +7,7 @@
package io.element.android.libraries.mediaviewer.api
import android.os.Parcelable
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
@@ -14,6 +15,8 @@ import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.Timeline
import kotlinx.parcelize.Parcelize
interface MediaViewerEntryPoint : FeatureEntryPoint {
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
@@ -39,9 +42,14 @@ interface MediaViewerEntryPoint : FeatureEntryPoint {
val canShowInfo: Boolean,
) : NodeInputs
enum class MediaViewerMode {
SingleMedia,
TimelineImagesAndVideos,
TimelineFilesAndAudios,
sealed interface MediaViewerMode : Parcelable {
@Parcelize
data object SingleMedia : MediaViewerMode
@Parcelize
data class TimelineImagesAndVideos(val timelineMode: Timeline.Mode) : MediaViewerMode
@Parcelize
data class TimelineFilesAndAudios(val timelineMode: Timeline.Mode) : MediaViewerMode
}
}

View File

@@ -18,6 +18,7 @@ interface FocusedTimelineMediaGalleryDataSourceFactory {
fun createFor(
eventId: EventId,
mediaItem: MediaItem.Event,
onlyPinnedEvents: Boolean,
): MediaGalleryDataSource
}
@@ -30,6 +31,7 @@ class DefaultFocusedTimelineMediaGalleryDataSourceFactory @Inject constructor(
override fun createFor(
eventId: EventId,
mediaItem: MediaItem.Event,
onlyPinnedEvents: Boolean,
): MediaGalleryDataSource {
return TimelineMediaGalleryDataSource(
room = room,
@@ -37,6 +39,7 @@ class DefaultFocusedTimelineMediaGalleryDataSourceFactory @Inject constructor(
room = room,
eventId = eventId,
initialMediaItem = mediaItem,
onlyPinnedEvents = onlyPinnedEvents,
),
timelineMediaItemsFactory = timelineMediaItemsFactory,
mediaItemsPostProcessor = mediaItemsPostProcessor,

View File

@@ -12,6 +12,7 @@ import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.di.SingleIn
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UniqueId
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaviewer.impl.model.GroupedMediaItems
@@ -44,7 +45,7 @@ class LiveMediaTimeline @Inject constructor(
override suspend fun getTimeline(): Result<Timeline> = mutex.withLock {
val currentTimeline = timeline
if (currentTimeline == null) {
room.mediaTimeline(null)
room.createTimeline(CreateTimelineParams.MediaOnly)
.onSuccess { timeline = it }
} else {
Result.success(currentTimeline)
@@ -58,14 +59,22 @@ class LiveMediaTimeline @Inject constructor(
/**
* A class that will provide a media timeline that is focused on a particular event.
* Optionally, the timeline will only contain the pinned events.
*/
class FocusedMediaTimeline(
private val room: MatrixRoom,
private val eventId: EventId,
private val onlyPinnedEvents: Boolean,
initialMediaItem: MediaItem.Event,
) : MediaTimeline {
override suspend fun getTimeline(): Result<Timeline> {
return room.mediaTimeline(eventId)
return room.createTimeline(
createTimelineParams = if (onlyPinnedEvents) {
CreateTimelineParams.PinnedOnly
} else {
CreateTimelineParams.MediaOnlyFocused(eventId)
},
)
}
override val cache = persistentListOf(

View File

@@ -27,6 +27,7 @@ import io.element.android.libraries.architecture.overlay.operation.show
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaviewer.api.MediaGalleryEntryPoint
import io.element.android.libraries.mediaviewer.api.MediaInfo
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
@@ -96,9 +97,9 @@ class MediaGalleryRootNode @AssistedInject constructor(
val mode = when (item) {
is MediaItem.Audio,
is MediaItem.Voice,
is MediaItem.File -> MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios
is MediaItem.File -> MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios(Timeline.Mode.MEDIA)
is MediaItem.Image,
is MediaItem.Video -> MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos
is MediaItem.Video -> MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(Timeline.Mode.MEDIA)
}
overlay.show(
NavTarget.MediaViewer(

View File

@@ -52,8 +52,8 @@ class MediaViewerDataSource(
private val galleryMode = when (mode) {
MediaViewerMode.SingleMedia,
MediaViewerMode.TimelineImagesAndVideos -> MediaGalleryMode.Images
MediaViewerMode.TimelineFilesAndAudios -> MediaGalleryMode.Files
is MediaViewerMode.TimelineImagesAndVideos -> MediaGalleryMode.Images
is MediaViewerMode.TimelineFilesAndAudios -> MediaGalleryMode.Files
}
// Map of sourceUrl to local media state

View File

@@ -22,6 +22,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
import io.element.android.libraries.mediaviewer.impl.datasource.FocusedTimelineMediaGalleryDataSourceFactory
@@ -69,16 +70,40 @@ class MediaViewerNode @AssistedInject constructor(
// Should not happen
timelineMediaGalleryDataSource
} else {
// Does timelineMediaGalleryDataSource knows the eventId?
val lastData = timelineMediaGalleryDataSource.getLastData().dataOrNull()
val isEventKnown = lastData?.hasEvent(eventId) == true
if (isEventKnown) {
timelineMediaGalleryDataSource
} else {
focusedTimelineMediaGalleryDataSourceFactory.createFor(
eventId = eventId,
mediaItem = inputs.toMediaItem(),
)
// Can we use a specific timeline?
val timelineMode = when (val mode = inputs.mode) {
is MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> mode.timelineMode
is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> mode.timelineMode
else -> null
}
when (timelineMode) {
null -> timelineMediaGalleryDataSource
Timeline.Mode.LIVE -> {
// Even if the timelineMediaGalleryDataSource does not know the eventId, the SDK will create the timeline faster
timelineMediaGalleryDataSource
}
Timeline.Mode.FOCUSED_ON_EVENT -> {
// Does timelineMediaGalleryDataSource knows the eventId?
val lastData = timelineMediaGalleryDataSource.getLastData().dataOrNull()
val isEventKnown = lastData?.hasEvent(eventId) == true
if (isEventKnown) {
timelineMediaGalleryDataSource
} else {
focusedTimelineMediaGalleryDataSourceFactory.createFor(
eventId = eventId,
mediaItem = inputs.toMediaItem(),
onlyPinnedEvents = false,
)
}
}
Timeline.Mode.PINNED_EVENTS -> {
focusedTimelineMediaGalleryDataSourceFactory.createFor(
eventId = eventId,
mediaItem = inputs.toMediaItem(),
onlyPinnedEvents = true,
)
}
Timeline.Mode.MEDIA -> timelineMediaGalleryDataSource
}
}
}

View File

@@ -204,8 +204,8 @@ class MediaViewerPresenter @AssistedInject constructor(
private fun showNoMoreItemsSnackbar() {
val messageResId = when (inputs.mode) {
MediaViewerEntryPoint.MediaViewerMode.SingleMedia,
MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> R.string.screen_media_details_no_more_media_to_show
MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> R.string.screen_media_details_no_more_files_to_show
is MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos -> R.string.screen_media_details_no_more_media_to_show
is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios -> R.string.screen_media_details_no_more_files_to_show
}
val message = SnackbarMessage(messageResId)
snackbarDispatcher.post(message)

View File

@@ -25,6 +25,7 @@ class DefaultFocusedTimelineMediaGalleryDataSourceFactoryTest {
val result = sut.createFor(
eventId = AN_EVENT_ID,
mediaItem = aMediaItemImage(),
onlyPinnedEvents = false,
)
assertThat(result).isInstanceOf(TimelineMediaGalleryDataSource::class.java)
}

View File

@@ -9,6 +9,7 @@ package io.element.android.libraries.mediaviewer.impl.datasource
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
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
@@ -75,11 +76,11 @@ class FocusedMediaTimelineTest {
@Test
fun `getTimeline returns the timeline provided by the room`() = runTest {
val mediaTimelineResult = lambdaRecorder<EventId?, Result<Timeline>> {
val createTimelineResult = lambdaRecorder<CreateTimelineParams, Result<Timeline>> {
Result.success(FakeTimeline())
}
val room = FakeMatrixRoom(
mediaTimelineResult = mediaTimelineResult,
createTimelineResult = createTimelineResult,
)
val sut = createFocusedMediaTimeline(
room = room,
@@ -87,16 +88,36 @@ class FocusedMediaTimelineTest {
)
val timeline = sut.getTimeline()
assertThat(timeline.isSuccess).isTrue()
mediaTimelineResult.assertions().isCalledOnce().with(value(AN_EVENT_ID))
createTimelineResult.assertions().isCalledOnce().with(value(CreateTimelineParams.MediaOnlyFocused(AN_EVENT_ID)))
}
@Test
fun `getTimeline returns the timeline provided by the room for pinned Events`() = runTest {
val createTimelineResult = lambdaRecorder<CreateTimelineParams, Result<Timeline>> {
Result.success(FakeTimeline())
}
val room = FakeMatrixRoom(
createTimelineResult = createTimelineResult,
)
val sut = createFocusedMediaTimeline(
room = room,
eventId = AN_EVENT_ID,
onlyPinnedEvent = true,
)
val timeline = sut.getTimeline()
assertThat(timeline.isSuccess).isTrue()
createTimelineResult.assertions().isCalledOnce().with(value(CreateTimelineParams.PinnedOnly))
}
private fun createFocusedMediaTimeline(
room: MatrixRoom = FakeMatrixRoom(),
eventId: EventId = AN_EVENT_ID,
initialMediaItem: MediaItem.Event = aMediaItemImage(),
onlyPinnedEvent: Boolean = false,
) = FocusedMediaTimeline(
room = room,
eventId = eventId,
initialMediaItem = initialMediaItem,
onlyPinnedEvents = onlyPinnedEvent,
)
}

View File

@@ -8,7 +8,7 @@
package io.element.android.libraries.mediaviewer.impl.datasource
import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.CreateTimelineParams
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.room.FakeMatrixRoom
@@ -28,22 +28,22 @@ class LiveMediaTimelineTest {
@Test
fun `getTimeline returns the timeline provided by the room, then from cache`() = runTest {
val mediaTimelineResult = lambdaRecorder<EventId?, Result<Timeline>> {
val createTimelineResult = lambdaRecorder<CreateTimelineParams, Result<Timeline>> {
Result.success(FakeTimeline())
}
val room = FakeMatrixRoom(
mediaTimelineResult = mediaTimelineResult,
createTimelineResult = createTimelineResult,
)
val sut = createLiveMediaTimeline(
room = room,
)
val timeline = sut.getTimeline()
assertThat(timeline.isSuccess).isTrue()
mediaTimelineResult.assertions().isCalledOnce().with(value(null))
createTimelineResult.assertions().isCalledOnce().with(value(CreateTimelineParams.MediaOnly))
val timeline2 = sut.getTimeline()
assertThat(timeline2.isSuccess).isTrue()
// No called another time
mediaTimelineResult.assertions().isCalledOnce()
createTimelineResult.assertions().isCalledOnce()
}
private fun createLiveMediaTimeline(

View File

@@ -57,7 +57,7 @@ class TimelineMediaGalleryDataSourceTest {
val fakeTimeline = FakeTimeline()
val sut = createTimelineMediaGalleryDataSource(
room = FakeMatrixRoom(
mediaTimelineResult = { Result.success(fakeTimeline) },
createTimelineResult = { Result.success(fakeTimeline) },
roomCoroutineScope = backgroundScope,
)
)
@@ -75,7 +75,7 @@ class TimelineMediaGalleryDataSourceTest {
runTest {
val sut = createTimelineMediaGalleryDataSource(
room = FakeMatrixRoom(
mediaTimelineResult = { Result.success(fakeTimeline) },
createTimelineResult = { Result.success(fakeTimeline) },
roomCoroutineScope = backgroundScope,
)
)
@@ -112,7 +112,7 @@ class TimelineMediaGalleryDataSourceTest {
}
val sut = createTimelineMediaGalleryDataSource(
room = FakeMatrixRoom(
mediaTimelineResult = { Result.success(fakeTimeline) },
createTimelineResult = { Result.success(fakeTimeline) },
roomCoroutineScope = backgroundScope,
)
)
@@ -135,7 +135,7 @@ class TimelineMediaGalleryDataSourceTest {
}
val sut = createTimelineMediaGalleryDataSource(
room = FakeMatrixRoom(
mediaTimelineResult = { Result.success(fakeTimeline) },
createTimelineResult = { Result.success(fakeTimeline) },
roomCoroutineScope = backgroundScope,
)
)
@@ -154,7 +154,7 @@ class TimelineMediaGalleryDataSourceTest {
fun `test - failing to load timeline should emit an error`() = runTest {
val sut = createTimelineMediaGalleryDataSource(
room = FakeMatrixRoom(
mediaTimelineResult = { Result.failure(AN_EXCEPTION) },
createTimelineResult = { Result.failure(AN_EXCEPTION) },
roomCoroutineScope = backgroundScope,
)
)
@@ -176,7 +176,7 @@ class TimelineMediaGalleryDataSourceTest {
)
val sut = createTimelineMediaGalleryDataSource(
room = FakeMatrixRoom(
mediaTimelineResult = { Result.success(fakeTimeline) },
createTimelineResult = { Result.success(fakeTimeline) },
roomCoroutineScope = backgroundScope,
)
)

View File

@@ -52,7 +52,7 @@ class MediaGalleryPresenterTest {
),
room = FakeMatrixRoom(
displayName = A_ROOM_NAME,
mediaTimelineResult = { Result.success(FakeTimeline()) },
createTimelineResult = { Result.success(FakeTimeline()) },
)
)
presenter.test {
@@ -71,7 +71,7 @@ class MediaGalleryPresenterTest {
val presenter = createMediaGalleryPresenter(
room = FakeMatrixRoom(
displayName = A_ROOM_NAME,
mediaTimelineResult = { Result.success(FakeTimeline()) },
createTimelineResult = { Result.success(FakeTimeline()) },
)
)
presenter.test {
@@ -101,7 +101,7 @@ class MediaGalleryPresenterTest {
room = FakeMatrixRoom(
sessionId = A_USER_ID,
displayName = A_ROOM_NAME,
mediaTimelineResult = { Result.success(FakeTimeline()) },
createTimelineResult = { Result.success(FakeTimeline()) },
canRedactOwnResult = { Result.success(canDeleteOwn) }
)
)
@@ -144,7 +144,7 @@ class MediaGalleryPresenterTest {
room = FakeMatrixRoom(
sessionId = A_USER_ID,
displayName = A_ROOM_NAME,
mediaTimelineResult = { Result.success(FakeTimeline()) },
createTimelineResult = { Result.success(FakeTimeline()) },
canRedactOtherResult = { Result.success(canDeleteOther) }
)
)
@@ -177,7 +177,7 @@ class MediaGalleryPresenterTest {
val presenter = createMediaGalleryPresenter(
room = FakeMatrixRoom(
displayName = A_ROOM_NAME,
mediaTimelineResult = { Result.success(FakeTimeline()) },
createTimelineResult = { Result.success(FakeTimeline()) },
)
)
presenter.test {
@@ -244,7 +244,7 @@ class MediaGalleryPresenterTest {
)
val presenter = createMediaGalleryPresenter(
room = FakeMatrixRoom(
mediaTimelineResult = { Result.success(FakeTimeline()) },
createTimelineResult = { Result.success(FakeTimeline()) },
),
navigator = navigator,
)

View File

@@ -137,7 +137,7 @@ class MediaViewerDataSourceTest {
fun `test dataFlow with data galleryMode image`() = runTest {
val galleryDataSource = FakeMediaGalleryDataSource()
val sut = createMediaViewerDataSource(
mode = MediaViewerMode.TimelineImagesAndVideos,
mode = MediaViewerMode.TimelineImagesAndVideos(timelineMode = Timeline.Mode.MEDIA),
galleryDataSource = galleryDataSource,
)
sut.dataFlow().test {
@@ -159,7 +159,7 @@ class MediaViewerDataSourceTest {
fun `test dataFlow with data galleryMode files`() = runTest {
val galleryDataSource = FakeMediaGalleryDataSource()
val sut = createMediaViewerDataSource(
mode = MediaViewerMode.TimelineFilesAndAudios,
mode = MediaViewerMode.TimelineFilesAndAudios(timelineMode = Timeline.Mode.MEDIA),
galleryDataSource = galleryDataSource,
)
sut.dataFlow().test {
@@ -265,7 +265,7 @@ class MediaViewerDataSourceTest {
}
private fun TestScope.createMediaViewerDataSource(
mode: MediaViewerMode = MediaViewerMode.TimelineImagesAndVideos,
mode: MediaViewerMode = MediaViewerMode.TimelineImagesAndVideos(timelineMode = Timeline.Mode.MEDIA),
galleryDataSource: MediaGalleryDataSource = FakeMediaGalleryDataSource(),
mediaLoader: MatrixMediaLoader = FakeMatrixMediaLoader(),
localMediaFactory: LocalMediaFactory = FakeLocalMediaFactory(mockMediaUrl),

View File

@@ -519,7 +519,7 @@ class MediaViewerPresenterTest {
@Test
fun `present - snackbar displayed when there is no more items forward images and videos`() {
`present - snackbar displayed when there is no more items forward`(
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos,
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(timelineMode = Timeline.Mode.MEDIA),
expectedSnackbarResId = R.string.screen_media_details_no_more_media_to_show,
)
}
@@ -527,7 +527,7 @@ class MediaViewerPresenterTest {
@Test
fun `present - snackbar displayed when there is no more items forward files and audio`() {
`present - snackbar displayed when there is no more items forward`(
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios,
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios(timelineMode = Timeline.Mode.MEDIA),
expectedSnackbarResId = R.string.screen_media_details_no_more_files_to_show,
)
}
@@ -547,7 +547,7 @@ class MediaViewerPresenterTest {
awaitFirstItem()
mediaGalleryDataSource.emitGroupedMediaItems(
AsyncData.Success(
if (mode == MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) {
if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) {
GroupedMediaItems(
imageAndVideoItems = persistentListOf(),
fileItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator),
@@ -568,7 +568,7 @@ class MediaViewerPresenterTest {
// data source claims that there is no more items to load forward
mediaGalleryDataSource.emitGroupedMediaItems(
AsyncData.Success(
if (mode == MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) {
if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) {
GroupedMediaItems(
imageAndVideoItems = persistentListOf(),
fileItems = persistentListOf(anImage, aBackwardLoadingIndicator),
@@ -590,7 +590,7 @@ class MediaViewerPresenterTest {
@Test
fun `present - snackbar displayed when there is no more items backward images and videos`() {
`present - snackbar displayed when there is no more items backward`(
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos,
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineImagesAndVideos(timelineMode = Timeline.Mode.MEDIA),
expectedSnackbarResId = R.string.screen_media_details_no_more_media_to_show,
)
}
@@ -598,7 +598,7 @@ class MediaViewerPresenterTest {
@Test
fun `present - snackbar displayed when there is no more items backward files and audio`() {
`present - snackbar displayed when there is no more items backward`(
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios,
mode = MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios(timelineMode = Timeline.Mode.MEDIA),
expectedSnackbarResId = R.string.screen_media_details_no_more_files_to_show,
)
}
@@ -618,7 +618,7 @@ class MediaViewerPresenterTest {
awaitFirstItem()
mediaGalleryDataSource.emitGroupedMediaItems(
AsyncData.Success(
if (mode == MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) {
if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) {
GroupedMediaItems(
imageAndVideoItems = persistentListOf(),
fileItems = persistentListOf(aForwardLoadingIndicator, anImage, aBackwardLoadingIndicator),
@@ -640,7 +640,7 @@ class MediaViewerPresenterTest {
// data source claims that there is no more items to load backward
mediaGalleryDataSource.emitGroupedMediaItems(
AsyncData.Success(
if (mode == MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) {
if (mode is MediaViewerEntryPoint.MediaViewerMode.TimelineFilesAndAudios) {
GroupedMediaItems(
imageAndVideoItems = persistentListOf(),
fileItems = persistentListOf(aForwardLoadingIndicator, anImage),

View File

@@ -84,6 +84,27 @@ class EnsureCalledOnceWithTwoParams<T, U>(
}
}
class EnsureCalledOnceWithTwoParamsAndResult<T, U, R>(
private val expectedParam1: T,
private val expectedParam2: U,
private val result: R,
) : (T, U) -> R {
private var counter = 0
override fun invoke(p1: T, p2: U): R {
if (p1 != expectedParam1 || p2 != expectedParam2) {
throw AssertionError("Expected to be called with $expectedParam1 and $expectedParam2, but was called with $p1 and $p2")
}
counter++
return result
}
fun assertSuccess() {
if (counter != 1) {
throw AssertionError("Expected to be called once, but was called $counter times")
}
}
}
/**
* Shortcut for [<T, R> ensureCalledOnceWithParam] with Unit result.
*/

View File

@@ -32,3 +32,9 @@ class EnsureNeverCalledWithTwoParams<T, U> : (T, U) -> Unit {
lambdaError("Should not be called and is called with $p1 and $p2")
}
}
class EnsureNeverCalledWithTwoParamsAndResult<T, U, R> : (T, U) -> R {
override fun invoke(p1: T, p2: U): R {
lambdaError("Should not be called and is called with $p1 and $p2")
}
}