Remember flows (#4533)

* Add Konsist test to ensure that the result of a function returning a flow is remembered.

* Remember flows before they are collected by state.

* Fix compilation issue

* Make isOnline a val.

* Make selectedUsers() a val.

* Make flow() a val.

* Make getUserConsent(), didAskUserConsent() and getAnalyticsId() some val.

* Remove Timeline.paginationStatus() and replace by direct access to the underlined flow.

* Simplify test

* userConsentFlow must be initialized before because it's used in observeUserConsent

* Fix test compilation
This commit is contained in:
Benoit Marty 2025-04-04 16:50:43 +02:00 committed by GitHub
parent e557ee2c77
commit a230b83e99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
52 changed files with 221 additions and 172 deletions

View file

@ -7,7 +7,6 @@
package io.element.android.libraries.matrix.api.sync
import io.element.android.libraries.core.coroutine.mapState
import kotlinx.coroutines.flow.StateFlow
interface SyncService {
@ -25,6 +24,6 @@ interface SyncService {
* Flow of [SyncState]. Will be updated as soon as the current [SyncState] changes.
*/
val syncState: StateFlow<SyncState>
}
fun SyncService.isOnline(): StateFlow<Boolean> = syncState.mapState { it != SyncState.Offline }
val isOnline: StateFlow<Boolean>
}

View file

@ -49,7 +49,10 @@ interface Timeline : AutoCloseable {
val membershipChangeEventReceived: Flow<Unit>
suspend fun sendReadReceipt(eventId: EventId, receiptType: ReceiptType): Result<Unit>
suspend fun paginate(direction: PaginationDirection): Result<Boolean>
fun paginationStatus(direction: PaginationDirection): StateFlow<PaginationStatus>
val backwardPaginationStatus: StateFlow<PaginationStatus>
val forwardPaginationStatus: StateFlow<PaginationStatus>
val timelineItems: Flow<List<MatrixTimelineItem>>
suspend fun sendMessage(
@ -105,7 +108,7 @@ interface Timeline : AutoCloseable {
caption: String?,
formattedCaption: String?,
progressCallback: ProgressCallback?,
): Result<MediaUploadHandler>
): Result<MediaUploadHandler>
suspend fun sendFile(
file: File,

View file

@ -7,6 +7,7 @@
package io.element.android.libraries.matrix.impl.sync
import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState
import kotlinx.coroutines.CoroutineDispatcher
@ -73,4 +74,6 @@ class RustSyncService(
}
.distinctUntilChanged()
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, SyncState.Idle)
override val isOnline: StateFlow<Boolean> = syncState.mapState { it != SyncState.Offline }
}

View file

@ -54,7 +54,6 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
@ -127,11 +126,11 @@ class RustTimeline(
private val lastForwardIndicatorsPostProcessor = LastForwardIndicatorsPostProcessor(mode)
private val typingNotificationPostProcessor = TypingNotificationPostProcessor(mode)
private val backPaginationStatus = MutableStateFlow(
override val backwardPaginationStatus = MutableStateFlow(
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode != Timeline.Mode.PINNED_EVENTS)
)
private val forwardPaginationStatus = MutableStateFlow(
override val forwardPaginationStatus = MutableStateFlow(
Timeline.PaginationStatus(isPaginating = false, hasMoreToLoad = mode == Timeline.Mode.FOCUSED_ON_EVENT)
)
@ -167,7 +166,7 @@ class RustTimeline(
private fun updatePaginationStatus(direction: Timeline.PaginationDirection, update: (Timeline.PaginationStatus) -> Timeline.PaginationStatus) {
when (direction) {
Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus.getAndUpdate(update)
Timeline.PaginationDirection.BACKWARDS -> backwardPaginationStatus.getAndUpdate(update)
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus.getAndUpdate(update)
}
}
@ -185,7 +184,7 @@ class RustTimeline(
}
}.onFailure { error ->
if (error is TimelineException.CannotPaginate) {
Timber.d("Can't paginate $direction on room ${matrixRoom.roomId} with paginationStatus: ${backPaginationStatus.value}")
Timber.d("Can't paginate $direction on room ${matrixRoom.roomId} with paginationStatus: ${backwardPaginationStatus.value}")
} else {
updatePaginationStatus(direction) { it.copy(isPaginating = false) }
Timber.e(error, "Error paginating $direction on room ${matrixRoom.roomId}")
@ -199,21 +198,14 @@ class RustTimeline(
private fun canPaginate(direction: Timeline.PaginationDirection): Boolean {
if (!isTimelineInitialized.value) return false
return when (direction) {
Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus.value.canPaginate
Timeline.PaginationDirection.BACKWARDS -> backwardPaginationStatus.value.canPaginate
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus.value.canPaginate
}
}
override fun paginationStatus(direction: Timeline.PaginationDirection): StateFlow<Timeline.PaginationStatus> {
return when (direction) {
Timeline.PaginationDirection.BACKWARDS -> backPaginationStatus
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus
}
}
override val timelineItems: Flow<List<MatrixTimelineItem>> = combine(
_timelineItems,
backPaginationStatus,
backwardPaginationStatus,
forwardPaginationStatus,
matrixRoom.roomInfoFlow.map { it.creator to it.isDm }.distinctUntilChanged(),
isTimelineInitialized,

View file

@ -29,7 +29,7 @@ class DefaultCallWidgetSettingsProvider @Inject constructor(
private val analyticsService: AnalyticsService,
) : CallWidgetSettingsProvider {
override suspend fun provide(baseUrl: String, widgetId: String, encrypted: Boolean): MatrixWidgetSettings {
val isAnalyticsEnabled = analyticsService.getUserConsent().first()
val isAnalyticsEnabled = analyticsService.userConsentFlow.first()
val options = VirtualElementCallWidgetOptions(
elementCallUrl = baseUrl,
widgetId = widgetId,

View file

@ -7,6 +7,7 @@
package io.element.android.libraries.matrix.test.sync
import io.element.android.libraries.core.coroutine.mapState
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState
import kotlinx.coroutines.flow.MutableStateFlow
@ -29,6 +30,8 @@ class FakeSyncService(
override val syncState: StateFlow<SyncState> = syncStateFlow
override val isOnline: StateFlow<Boolean> = syncState.mapState { it != SyncState.Offline }
suspend fun emitSyncState(syncState: SyncState) {
syncStateFlow.emit(syncState)
}

View file

@ -28,19 +28,18 @@ import io.element.android.tests.testutils.lambda.lambdaError
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import java.io.File
class FakeTimeline(
private val name: String = "FakeTimeline",
override val timelineItems: Flow<List<MatrixTimelineItem>> = MutableStateFlow(emptyList()),
private val backwardPaginationStatus: MutableStateFlow<Timeline.PaginationStatus> = MutableStateFlow(
override val backwardPaginationStatus: MutableStateFlow<Timeline.PaginationStatus> = MutableStateFlow(
Timeline.PaginationStatus(
isPaginating = false,
hasMoreToLoad = true
)
),
private val forwardPaginationStatus: MutableStateFlow<Timeline.PaginationStatus> = MutableStateFlow(
override val forwardPaginationStatus: MutableStateFlow<Timeline.PaginationStatus> = MutableStateFlow(
Timeline.PaginationStatus(
isPaginating = false,
hasMoreToLoad = false
@ -377,13 +376,6 @@ class FakeTimeline(
override suspend fun paginate(direction: Timeline.PaginationDirection): Result<Boolean> = paginateLambda(direction)
override fun paginationStatus(direction: Timeline.PaginationDirection): StateFlow<Timeline.PaginationStatus> {
return when (direction) {
Timeline.PaginationDirection.BACKWARDS -> backwardPaginationStatus
Timeline.PaginationDirection.FORWARDS -> forwardPaginationStatus
}
}
var loadReplyDetailsLambda: (eventId: EventId) -> InReplyTo = {
InReplyTo.NotLoaded(it)
}

View file

@ -13,6 +13,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@ -56,13 +57,13 @@ class DefaultPermissionsPresenter @AssistedInject constructor(
// To reset the store: ResetStore()
val isAlreadyDenied: Boolean by permissionsStore
.isPermissionDenied(permission)
.collectAsState(initial = false)
val isAlreadyDenied: Boolean by remember {
permissionsStore.isPermissionDenied(permission)
}.collectAsState(initial = false)
val isAlreadyAsked: Boolean by permissionsStore
.isPermissionAsked(permission)
.collectAsState(initial = false)
val isAlreadyAsked: Boolean by remember {
permissionsStore.isPermissionAsked(permission)
}.collectAsState(initial = false)
var permissionState: PermissionState? = null