Create MediaGalleryDataSource and extract logic from MediaGalleryPresenter.

This commit is contained in:
Benoit Marty 2025-01-15 11:59:15 +01:00 committed by Benoit Marty
parent 925db81ca5
commit c8ca4d7425
3 changed files with 105 additions and 107 deletions

View file

@ -0,0 +1,86 @@
/*
* 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.mediaviewer.impl.gallery
import io.element.android.libraries.architecture.AsyncData
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.MatrixRoom
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
@SingleIn(RoomScope::class)
class MediaGalleryDataSource @Inject constructor(
private val room: MatrixRoom,
private val timelineMediaItemsFactory: TimelineMediaItemsFactory,
private val mediaItemsPostProcessor: MediaItemsPostProcessor,
) {
private var timeline: Timeline? = null
private val groupedMediaItemsFlow = MutableSharedFlow<AsyncData<GroupedMediaItems>>(replay = 1)
fun groupedMediaItemsFlow(): Flow<AsyncData<GroupedMediaItems>> = groupedMediaItemsFlow
private val isStarted = AtomicBoolean(false)
@OptIn(ExperimentalCoroutinesApi::class)
fun start() {
if (!isStarted.compareAndSet(false, true)) {
return
}
flow {
groupedMediaItemsFlow.emit(AsyncData.Loading())
room.mediaTimeline().fold(
{
timeline = it
emit(it)
},
{ groupedMediaItemsFlow.emit(AsyncData.Failure(it)) },
)
}.flatMapLatest { timeline ->
timeline.timelineItems.onEach {
timelineMediaItemsFactory.replaceWith(
timelineItems = it,
)
}
}.flatMapLatest {
timelineMediaItemsFactory.timelineItems
}.map { timelineItems ->
mediaItemsPostProcessor.process(mediaItems = timelineItems)
}.onEach { groupedMediaItems ->
groupedMediaItemsFlow.emit(AsyncData.Success(groupedMediaItems))
}
.onCompletion {
timeline?.close()
}
.launchIn(room.roomCoroutineScope)
}
suspend fun loadMore(direction: Timeline.PaginationDirection) {
timeline?.paginate(direction)
}
suspend fun deleteItem(eventId: EventId) {
timeline?.redactEvent(
eventOrTransactionId = eventId.toEventOrTransactionId(),
reason = null,
)
}
}

View file

@ -9,15 +9,12 @@ package io.element.android.libraries.mediaviewer.impl.gallery
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
@ -33,30 +30,21 @@ import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOwn
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.matrix.api.timeline.item.event.toEventOrTransactionId
import io.element.android.libraries.mediaviewer.api.local.LocalMedia import io.element.android.libraries.mediaviewer.api.local.LocalMedia
import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory import io.element.android.libraries.mediaviewer.api.local.LocalMediaFactory
import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState import io.element.android.libraries.mediaviewer.impl.details.MediaBottomSheetState
import io.element.android.libraries.mediaviewer.impl.local.LocalMediaActions import io.element.android.libraries.mediaviewer.impl.local.LocalMediaActions
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class MediaGalleryPresenter @AssistedInject constructor( class MediaGalleryPresenter @AssistedInject constructor(
@Assisted private val navigator: MediaGalleryNavigator, @Assisted private val navigator: MediaGalleryNavigator,
private val room: MatrixRoom, private val room: MatrixRoom,
private val timelineMediaItemsFactory: TimelineMediaItemsFactory, private val mediaGalleryDataSource: MediaGalleryDataSource,
private val localMediaFactory: LocalMediaFactory, private val localMediaFactory: LocalMediaFactory,
private val mediaLoader: MatrixMediaLoader, private val mediaLoader: MatrixMediaLoader,
private val localMediaActions: LocalMediaActions, private val localMediaActions: LocalMediaActions,
private val snackbarDispatcher: SnackbarDispatcher, private val snackbarDispatcher: SnackbarDispatcher,
private val mediaItemsPostProcessor: MediaItemsPostProcessor,
) : Presenter<MediaGalleryState> { ) : Presenter<MediaGalleryState> {
@AssistedFactory @AssistedFactory
interface Factory { interface Factory {
@ -74,56 +62,36 @@ class MediaGalleryPresenter @AssistedInject constructor(
var mediaBottomSheetState by remember { mutableStateOf<MediaBottomSheetState>(MediaBottomSheetState.Hidden) } var mediaBottomSheetState by remember { mutableStateOf<MediaBottomSheetState>(MediaBottomSheetState.Hidden) }
var mediaItems by remember {
mutableStateOf<AsyncData<ImmutableList<MediaItem>>>(AsyncData.Uninitialized)
}
val groupedMediaItems by remember { val groupedMediaItems by remember {
derivedStateOf { mediaGalleryDataSource.groupedMediaItemsFlow()
mediaItemsPostProcessor.process(
mediaItems = mediaItems,
)
}
} }
.collectAsState(AsyncData.Uninitialized)
LaunchedEffect(Unit) {
mediaGalleryDataSource.start()
}
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState() val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
localMediaActions.Configure() localMediaActions.Configure()
var timeline by remember { mutableStateOf<AsyncData<Timeline>>(AsyncData.Uninitialized) }
LaunchedEffect(Unit) {
room.mediaTimeline()
.fold(
{ timeline = AsyncData.Success(it) },
{ timeline = AsyncData.Failure(it) },
)
}
DisposableEffect(Unit) {
onDispose {
timeline.dataOrNull()?.close()
}
}
MediaListEffect(
timeline = timeline,
onItemsChange = { newItems ->
mediaItems = newItems
}
)
fun handleEvents(event: MediaGalleryEvents) { fun handleEvents(event: MediaGalleryEvents) {
when (event) { when (event) {
is MediaGalleryEvents.ChangeMode -> { is MediaGalleryEvents.ChangeMode -> {
mode = event.mode mode = event.mode
} }
is MediaGalleryEvents.LoadMore -> coroutineScope.launch { is MediaGalleryEvents.LoadMore -> coroutineScope.launch {
timeline.dataOrNull()?.paginate(event.direction) mediaGalleryDataSource.loadMore(event.direction)
}
is MediaGalleryEvents.Delete -> coroutineScope.launch {
mediaGalleryDataSource.deleteItem(event.eventId)
} }
is MediaGalleryEvents.Delete -> coroutineScope.delete(timeline, event.eventId)
is MediaGalleryEvents.SaveOnDisk -> coroutineScope.launch { is MediaGalleryEvents.SaveOnDisk -> coroutineScope.launch {
mediaItems.dataOrNull().find(event.eventId)?.let { groupedMediaItems.dataOrNull().find(event.eventId)?.let {
saveOnDisk(it) saveOnDisk(it)
} }
} }
is MediaGalleryEvents.Share -> coroutineScope.launch { is MediaGalleryEvents.Share -> coroutineScope.launch {
mediaItems.dataOrNull().find(event.eventId)?.let { groupedMediaItems.dataOrNull().find(event.eventId)?.let {
share(it) share(it)
} }
} }
@ -172,49 +140,6 @@ class MediaGalleryPresenter @AssistedInject constructor(
) )
} }
@Composable
private fun MediaListEffect(
timeline: AsyncData<Timeline>,
onItemsChange: (AsyncData<ImmutableList<MediaItem>>) -> Unit,
) {
val updatedOnItemsChange by rememberUpdatedState(onItemsChange)
LaunchedEffect(timeline) {
when (timeline) {
AsyncData.Uninitialized -> flowOf(AsyncData.Uninitialized)
is AsyncData.Failure -> flowOf(AsyncData.Failure(timeline.error))
is AsyncData.Loading -> flowOf(AsyncData.Loading())
is AsyncData.Success -> {
timeline.data.timelineItems
.onEach { items ->
timelineMediaItemsFactory.replaceWith(
timelineItems = items,
)
}
.launchIn(this)
timelineMediaItemsFactory.timelineItems.map { timelineItems ->
AsyncData.Success(timelineItems)
}
}
}
.onEach { items ->
updatedOnItemsChange(items)
}
.launchIn(this)
}
}
private fun CoroutineScope.delete(
timeline: AsyncData<Timeline>,
eventId: EventId,
) = launch {
timeline.dataOrNull()?.redactEvent(
eventOrTransactionId = eventId.toEventOrTransactionId(),
reason = null,
)
}
private suspend fun downloadMedia(mediaItem: MediaItem.Event): Result<LocalMedia> { private suspend fun downloadMedia(mediaItem: MediaItem.Event): Result<LocalMedia> {
return mediaLoader.downloadMediaFile( return mediaLoader.downloadMediaFile(
source = mediaItem.mediaSource(), source = mediaItem.mediaSource(),
@ -264,10 +189,10 @@ class MediaGalleryPresenter @AssistedInject constructor(
} }
} }
private fun List<MediaItem>?.find(eventId: EventId?): MediaItem.Event? { private fun GroupedMediaItems?.find(eventId: EventId?): MediaItem.Event? {
if (this == null || eventId == null) { if (this == null || eventId == null) {
return null return null
} }
return filterIsInstance<MediaItem.Event>() return (imageAndVideoItems + fileItems).filterIsInstance<MediaItem.Event>()
.firstOrNull { it.eventId() == eventId } .firstOrNull { it.eventId() == eventId }
} }

View file

@ -7,32 +7,19 @@
package io.element.android.libraries.mediaviewer.impl.gallery package io.element.android.libraries.mediaviewer.impl.gallery
import io.element.android.libraries.architecture.AsyncData
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import javax.inject.Inject import javax.inject.Inject
class MediaItemsPostProcessor @Inject constructor() { class MediaItemsPostProcessor @Inject constructor() {
fun process( fun process(
mediaItems: AsyncData<ImmutableList<MediaItem>>, mediaItems: List<MediaItem>,
): AsyncData<GroupedMediaItems> { ): GroupedMediaItems {
return when (mediaItems) {
is AsyncData.Uninitialized -> AsyncData.Uninitialized
is AsyncData.Loading -> AsyncData.Loading()
is AsyncData.Failure -> AsyncData.Failure(mediaItems.error)
is AsyncData.Success -> AsyncData.Success(
mediaItems.data.process()
)
}
}
private fun List<MediaItem>.process(): GroupedMediaItems {
val imageAndVideoItems = mutableListOf<MediaItem>() val imageAndVideoItems = mutableListOf<MediaItem>()
val fileItems = mutableListOf<MediaItem>() val fileItems = mutableListOf<MediaItem>()
val imageAndVideoItemsSubList = mutableListOf<MediaItem.Event>() val imageAndVideoItemsSubList = mutableListOf<MediaItem.Event>()
val fileItemsSublist = mutableListOf<MediaItem.Event>() val fileItemsSublist = mutableListOf<MediaItem.Event>()
forEach { item -> mediaItems.forEach { item ->
when (item) { when (item) {
is MediaItem.DateSeparator -> { is MediaItem.DateSeparator -> {
if (imageAndVideoItemsSubList.isNotEmpty()) { if (imageAndVideoItemsSubList.isNotEmpty()) {