From f8b23581e1577a15562a29ba16c88659a8acef13 Mon Sep 17 00:00:00 2001
From: Taras Smakula <tarassmakula@gmail.com>
Date: Wed, 6 Dec 2023 15:04:14 +0200
Subject: [PATCH] Change timeline building to flow

---
 .../timeline/TimelineDialogFragment.kt        | 18 ++-----
 core/build.gradle                             |  2 +-
 .../gallery/media/PickMediaItemViewModel.kt   |  2 +-
 .../select_users/SelectUsersDataSource.kt     |  5 --
 .../feature/timeline/BaseTimelineViewModel.kt |  9 ++--
 .../timeline/builder/BaseTimelineBuilder.kt   |  2 +-
 .../data_source/BaseTimelineDataSource.kt     | 48 +++++++++++++------
 .../data_source/MultiTimelinesDataSource.kt   | 12 ++---
 .../data_source/SingleTimelineDataSource.kt   | 12 ++---
 .../feature/gallery/grid/GalleryViewModel.kt  | 10 ++--
 10 files changed, 60 insertions(+), 60 deletions(-)

diff --git a/app/src/main/java/org/futo/circles/feature/timeline/TimelineDialogFragment.kt b/app/src/main/java/org/futo/circles/feature/timeline/TimelineDialogFragment.kt
index e0e6a2c13..f2a19432b 100644
--- a/app/src/main/java/org/futo/circles/feature/timeline/TimelineDialogFragment.kt
+++ b/app/src/main/java/org/futo/circles/feature/timeline/TimelineDialogFragment.kt
@@ -26,7 +26,6 @@ import org.futo.circles.core.extensions.withConfirmation
 import org.futo.circles.core.feature.share.ShareProvider
 import org.futo.circles.core.model.CircleRoomTypeArg
 import org.futo.circles.core.model.CreatePollContent
-import org.futo.circles.core.model.Post
 import org.futo.circles.core.model.PostContent
 import org.futo.circles.core.model.PostContentType
 import org.futo.circles.core.utils.debounce
@@ -66,18 +65,7 @@ class TimelineDialogFragment : BaseFullscreenDialogFragment(DialogFragmentTimeli
         debounce<Unit>(
             scope = lifecycleScope,
             destinationFunction = {
-                if (viewModel.loadMore()) binding.rvTimeline.setIsPageLoading(true)
-            }
-        )
-    }
-
-    private val submitDataDebounce by lazy {
-        debounce<List<Post>>(
-            scope = lifecycleScope,
-            destinationFunction = {
-                listAdapter.submitList(it)
-                binding.rvTimeline.setIsPageLoading(false)
-                viewModel.markTimelineAsRead(args.roomId, isGroupMode)
+                binding.rvTimeline.setIsPageLoading(viewModel.loadMore())
             }
         )
     }
@@ -168,7 +156,9 @@ class TimelineDialogFragment : BaseFullscreenDialogFragment(DialogFragmentTimeli
             binding.toolbar.title = title
         }
         viewModel.timelineEventsLiveData.observeData(this) {
-            submitDataDebounce(it)
+            listAdapter.submitList(it)
+            binding.rvTimeline.setIsPageLoading(false)
+            viewModel.markTimelineAsRead(args.roomId, isGroupMode)
         }
         viewModel.notificationsStateLiveData.observeData(this) {
             binding.toolbar.subtitle =
diff --git a/core/build.gradle b/core/build.gradle
index 35b4eb51d..f21acd386 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -69,7 +69,7 @@ dependencies {
     kapt "com.google.dagger:hilt-compiler:$rootProject.ext.hilt_version"
 
     //Matrix
-    api 'org.matrix.android:matrix-sdk-android-rustCrypto:1.5.30.36'
+    api 'org.matrix.android:matrix-sdk-android-rustCrypto:1.5.30.37'
 
     //Retrofit2
     def retrofit_version = '2.9.0'
diff --git a/core/src/main/java/org/futo/circles/core/feature/picker/gallery/media/PickMediaItemViewModel.kt b/core/src/main/java/org/futo/circles/core/feature/picker/gallery/media/PickMediaItemViewModel.kt
index b187f428b..3d52fe475 100644
--- a/core/src/main/java/org/futo/circles/core/feature/picker/gallery/media/PickMediaItemViewModel.kt
+++ b/core/src/main/java/org/futo/circles/core/feature/picker/gallery/media/PickMediaItemViewModel.kt
@@ -26,7 +26,7 @@ class PickMediaItemViewModel @Inject constructor(
     private val selectedItemsFlow = MutableStateFlow<List<GalleryContentListItem>>(emptyList())
 
     val galleryItemsLiveData = combine(
-        timelineDataSource.timelineEventsLiveData.asFlow(),
+        timelineDataSource.getTimelineEventFlow(),
         selectedItemsFlow
     ) { items, selectedIds ->
         items.mapNotNull { post ->
diff --git a/core/src/main/java/org/futo/circles/core/feature/select_users/SelectUsersDataSource.kt b/core/src/main/java/org/futo/circles/core/feature/select_users/SelectUsersDataSource.kt
index d276949d3..bc477494e 100644
--- a/core/src/main/java/org/futo/circles/core/feature/select_users/SelectUsersDataSource.kt
+++ b/core/src/main/java/org/futo/circles/core/feature/select_users/SelectUsersDataSource.kt
@@ -1,15 +1,12 @@
 package org.futo.circles.core.feature.select_users
 
 import androidx.lifecycle.SavedStateHandle
-import androidx.lifecycle.asFlow
 import dagger.hilt.android.scopes.ViewModelScoped
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.mapLatest
-import org.futo.circles.core.extensions.getOrFetchUser
 import org.futo.circles.core.extensions.getUserIdsToExclude
 import org.futo.circles.core.mapping.toUserListItem
 import org.futo.circles.core.model.CirclesUserSummary
@@ -20,11 +17,9 @@ import org.futo.circles.core.model.UserListItem
 import org.futo.circles.core.provider.MatrixSessionProvider
 import org.futo.circles.core.utils.UserUtils
 import org.matrix.android.sdk.api.MatrixPatterns
-import org.matrix.android.sdk.api.session.Session
 import org.matrix.android.sdk.api.session.getRoom
 import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
 import org.matrix.android.sdk.api.session.room.model.Membership
-import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
 import org.matrix.android.sdk.api.session.user.model.User
 import javax.inject.Inject
 
diff --git a/core/src/main/java/org/futo/circles/core/feature/timeline/BaseTimelineViewModel.kt b/core/src/main/java/org/futo/circles/core/feature/timeline/BaseTimelineViewModel.kt
index d302d53ff..60b44419b 100644
--- a/core/src/main/java/org/futo/circles/core/feature/timeline/BaseTimelineViewModel.kt
+++ b/core/src/main/java/org/futo/circles/core/feature/timeline/BaseTimelineViewModel.kt
@@ -1,9 +1,10 @@
 package org.futo.circles.core.feature.timeline
 
 import androidx.lifecycle.ViewModel
+import androidx.lifecycle.asLiveData
 import androidx.lifecycle.map
-import org.futo.circles.core.mapping.nameOrId
 import org.futo.circles.core.feature.timeline.data_source.BaseTimelineDataSource
+import org.futo.circles.core.mapping.nameOrId
 
 abstract class BaseTimelineViewModel(
     private val baseTimelineDataSource: BaseTimelineDataSource
@@ -12,11 +13,7 @@ abstract class BaseTimelineViewModel(
     val titleLiveData =
         baseTimelineDataSource.room.getRoomSummaryLive().map { it.getOrNull()?.nameOrId() ?: "" }
 
-    val timelineEventsLiveData = baseTimelineDataSource.timelineEventsLiveData
-
-    init {
-        baseTimelineDataSource.startTimeline()
-    }
+    val timelineEventsLiveData = baseTimelineDataSource.getTimelineEventFlow().asLiveData()
 
     override fun onCleared() {
         baseTimelineDataSource.clearTimeline()
diff --git a/core/src/main/java/org/futo/circles/core/feature/timeline/builder/BaseTimelineBuilder.kt b/core/src/main/java/org/futo/circles/core/feature/timeline/builder/BaseTimelineBuilder.kt
index 551b588b4..eb4e448d2 100644
--- a/core/src/main/java/org/futo/circles/core/feature/timeline/builder/BaseTimelineBuilder.kt
+++ b/core/src/main/java/org/futo/circles/core/feature/timeline/builder/BaseTimelineBuilder.kt
@@ -17,7 +17,7 @@ abstract class BaseTimelineBuilder {
 
     abstract fun List<TimelineEvent>.processSnapshot(isThread: Boolean): List<Post>
 
-    fun build(snapshot: List<TimelineEvent>, isThread: Boolean): List<Post> = snapshot
+    suspend fun build(snapshot: List<TimelineEvent>, isThread: Boolean): List<Post> = snapshot
         .filterTimelineEvents(isThread)
         .processSnapshot(isThread)
 
diff --git a/core/src/main/java/org/futo/circles/core/feature/timeline/data_source/BaseTimelineDataSource.kt b/core/src/main/java/org/futo/circles/core/feature/timeline/data_source/BaseTimelineDataSource.kt
index c6de328db..43683402b 100644
--- a/core/src/main/java/org/futo/circles/core/feature/timeline/data_source/BaseTimelineDataSource.kt
+++ b/core/src/main/java/org/futo/circles/core/feature/timeline/data_source/BaseTimelineDataSource.kt
@@ -1,7 +1,14 @@
 package org.futo.circles.core.feature.timeline.data_source
 
-import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.SavedStateHandle
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.mapLatest
 import org.futo.circles.core.extensions.getOrThrow
 import org.futo.circles.core.feature.timeline.builder.BaseTimelineBuilder
 import org.futo.circles.core.feature.timeline.builder.MultiTimelineBuilder
@@ -19,7 +26,7 @@ import javax.inject.Inject
 abstract class BaseTimelineDataSource(
     savedStateHandle: SavedStateHandle,
     private val timelineBuilder: BaseTimelineBuilder
-) : Timeline.Listener {
+) {
 
     class Factory @Inject constructor(private val savedStateHandle: SavedStateHandle) {
         fun create(isMultiTimelines: Boolean): BaseTimelineDataSource =
@@ -32,30 +39,45 @@ abstract class BaseTimelineDataSource(
     protected val session = MatrixSessionProvider.getSessionOrThrow()
 
     val room = session.getRoom(roomId) ?: throw IllegalArgumentException("room is not found")
-    val timelineEventsLiveData = MutableLiveData<List<Post>>()
 
     private val isThread: Boolean = threadEventId != null
     private val listDirection =
         if (isThread) Timeline.Direction.FORWARDS else Timeline.Direction.BACKWARDS
 
 
-    abstract fun startTimeline()
+    fun getTimelineEventFlow(): Flow<List<Post>> = callbackFlow {
+        val listener = object : Timeline.Listener {
+            override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
+                if (snapshot.isNotEmpty()) trySend(snapshot)
+            }
+
+            override fun onTimelineFailure(timelineId: String, throwable: Throwable) {
+                onTimelineFailure(timelineId, throwable)
+            }
+        }
+        startTimeline(listener)
+        awaitClose()
+    }.flowOn(Dispatchers.IO)
+        .debounce(150)
+        .mapLatest {
+            timelineBuilder.build(it, isThread)
+        }
+        .distinctUntilChanged()
+
+    protected abstract fun startTimeline(listener: Timeline.Listener)
+
+    protected abstract fun onTimelineFailure(timelineId: String, throwable: Throwable)
     abstract fun clearTimeline()
     abstract fun loadMore(): Boolean
 
-    override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
-        if (snapshot.isNotEmpty())
-            timelineEventsLiveData.value = timelineBuilder.build(snapshot, isThread)
-    }
-
-    protected fun createAndStartNewTimeline(room: Room) =
+    protected fun createAndStartNewTimeline(room: Room, listener: Timeline.Listener) =
         room.timelineService()
             .createTimeline(
                 null,
                 TimelineSettings(initialSize = MESSAGES_PER_PAGE, rootThreadEventId = threadEventId)
             )
             .apply {
-                addListener(this@BaseTimelineDataSource)
+                addListener(listener)
                 start(threadEventId)
             }
 
@@ -70,10 +92,6 @@ abstract class BaseTimelineDataSource(
         return hasMore
     }
 
-    protected fun restartTimelineOnFailure(timeline: Timeline) {
-        if (timeline.getPaginationState(listDirection).inError) timeline.restartWithEventId(null)
-    }
-
     companion object {
         private const val MESSAGES_PER_PAGE = 50
     }
diff --git a/core/src/main/java/org/futo/circles/core/feature/timeline/data_source/MultiTimelinesDataSource.kt b/core/src/main/java/org/futo/circles/core/feature/timeline/data_source/MultiTimelinesDataSource.kt
index 9de0a1caa..11f0fcafa 100644
--- a/core/src/main/java/org/futo/circles/core/feature/timeline/data_source/MultiTimelinesDataSource.kt
+++ b/core/src/main/java/org/futo/circles/core/feature/timeline/data_source/MultiTimelinesDataSource.kt
@@ -16,13 +16,17 @@ class MultiTimelinesDataSource @Inject constructor(
 
     private var timelines: MutableList<Timeline> = mutableListOf()
 
-    override fun startTimeline() {
+    override fun startTimeline(listener: Timeline.Listener) {
         getTimelineRooms().forEach { room ->
-            val timeline = createAndStartNewTimeline(room)
+            val timeline = createAndStartNewTimeline(room, listener)
             timelines.add(timeline)
         }
     }
 
+    override fun onTimelineFailure(timelineId: String, throwable: Throwable) {
+        timelines.firstOrNull { it.timelineID == timelineId }?.restartWithEventId(null)
+    }
+
     override fun clearTimeline() {
         timelines.forEach { timeline -> closeTimeline(timeline) }
         timelines.clear()
@@ -36,10 +40,6 @@ class MultiTimelinesDataSource @Inject constructor(
         return hasMore
     }
 
-    override fun onTimelineFailure(throwable: Throwable) {
-        timelines.forEach { restartTimelineOnFailure(it) }
-    }
-
     private fun getTimelineRooms(): List<Room> = room.roomSummary()?.spaceChildren?.mapNotNull {
         session.getRoom(it.childRoomId)
     } ?: emptyList()
diff --git a/core/src/main/java/org/futo/circles/core/feature/timeline/data_source/SingleTimelineDataSource.kt b/core/src/main/java/org/futo/circles/core/feature/timeline/data_source/SingleTimelineDataSource.kt
index 519c0de26..87a94383d 100644
--- a/core/src/main/java/org/futo/circles/core/feature/timeline/data_source/SingleTimelineDataSource.kt
+++ b/core/src/main/java/org/futo/circles/core/feature/timeline/data_source/SingleTimelineDataSource.kt
@@ -14,8 +14,12 @@ class SingleTimelineDataSource @Inject constructor(
 
     private var timeline: Timeline? = null
 
-    override fun startTimeline() {
-        timeline = createAndStartNewTimeline(room)
+    override fun startTimeline(listener: Timeline.Listener) {
+        timeline = createAndStartNewTimeline(room, listener)
+    }
+
+    override fun onTimelineFailure(timelineId: String, throwable: Throwable) {
+        timeline?.restartWithEventId(null)
     }
 
     override fun clearTimeline() {
@@ -25,8 +29,4 @@ class SingleTimelineDataSource @Inject constructor(
 
     override fun loadMore() = timeline?.let { loadNextPage(it) } ?: false
 
-    override fun onTimelineFailure(throwable: Throwable) {
-        timeline?.let { restartTimelineOnFailure(it) }
-    }
-
 }
\ No newline at end of file
diff --git a/gallery/src/main/java/org/futo/circles/gallery/feature/gallery/grid/GalleryViewModel.kt b/gallery/src/main/java/org/futo/circles/gallery/feature/gallery/grid/GalleryViewModel.kt
index 4dc518ac7..f557aa3fd 100644
--- a/gallery/src/main/java/org/futo/circles/gallery/feature/gallery/grid/GalleryViewModel.kt
+++ b/gallery/src/main/java/org/futo/circles/gallery/feature/gallery/grid/GalleryViewModel.kt
@@ -8,16 +8,16 @@ import dagger.hilt.android.lifecycle.HiltViewModel
 import org.futo.circles.core.base.SingleEventLiveData
 import org.futo.circles.core.extensions.getOrThrow
 import org.futo.circles.core.extensions.launchBg
-import org.futo.circles.core.model.GalleryContentListItem
-import org.futo.circles.core.model.MediaContent
-import org.futo.circles.core.model.MediaType
-import org.futo.circles.core.model.ShareableContent
 import org.futo.circles.core.feature.timeline.BaseTimelineViewModel
 import org.futo.circles.core.feature.timeline.data_source.AccessLevelDataSource
 import org.futo.circles.core.feature.timeline.data_source.SingleTimelineDataSource
 import org.futo.circles.core.feature.timeline.post.PostContentDataSource
 import org.futo.circles.core.feature.timeline.post.PostOptionsDataSource
 import org.futo.circles.core.feature.timeline.post.SendMessageDataSource
+import org.futo.circles.core.model.GalleryContentListItem
+import org.futo.circles.core.model.MediaContent
+import org.futo.circles.core.model.MediaType
+import org.futo.circles.core.model.ShareableContent
 import javax.inject.Inject
 
 @HiltViewModel
@@ -34,7 +34,7 @@ class GalleryViewModel @Inject constructor(
 
     val accessLevelLiveData = accessLevelDataSource.accessLevelFlow.asLiveData()
 
-    val galleryItemsLiveData = timelineDataSource.timelineEventsLiveData.map { list ->
+    val galleryItemsLiveData = timelineDataSource.getTimelineEventFlow().asLiveData().map { list ->
         list.mapNotNull { post ->
             (post.content as? MediaContent)?.let {
                 GalleryContentListItem(post.id, post.postInfo, it)
-- 
GitLab