Create MediaGalleryDataSource and extract logic from MediaGalleryPresenter.
This commit is contained in:
parent
925db81ca5
commit
c8ca4d7425
3 changed files with 105 additions and 107 deletions
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue