Feature/fga/sync states (#1042)

* Change RoomSummaryDataSource to RoomListService to better reflects the rust api

* Better Sync management

* Sync: improve sync spinner rendering

* Sync: make test compiles

* Sync: add more test for sync spinner

* Sync: more clean-up

* Sync: pr review

---------

Co-authored-by: ganfra <francoisg@element.io>
This commit is contained in:
ganfra 2023-08-09 14:37:43 +02:00 committed by GitHub
parent 4e94d4da6b
commit 226a3dbf28
44 changed files with 547 additions and 356 deletions

View file

@ -65,6 +65,8 @@ dependencies {
testImplementation(libs.test.truth) testImplementation(libs.test.truth)
testImplementation(libs.test.turbine) testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.matrix.test)
testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.tests.testutils)
testImplementation(projects.features.rageshake.test) testImplementation(projects.features.rageshake.test)
testImplementation(projects.features.rageshake.impl) testImplementation(projects.features.rageshake.impl)
testImplementation(projects.services.appnavstate.test) testImplementation(projects.services.appnavstate.test)

View file

@ -69,10 +69,13 @@ import io.element.android.libraries.matrix.ui.di.MatrixUIBindings
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.services.appnavstate.api.AppNavigationStateService import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import timber.log.Timber
@ContributesNode(AppScope::class) @ContributesNode(AppScope::class)
class LoggedInFlowNode @AssistedInject constructor( class LoggedInFlowNode @AssistedInject constructor(
@ -123,7 +126,6 @@ class LoggedInFlowNode @AssistedInject constructor(
override fun onBuilt() { override fun onBuilt() {
super.onBuilt() super.onBuilt()
lifecycle.subscribe( lifecycle.subscribe(
onCreate = { onCreate = {
plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.matrixClient) } plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.matrixClient) }
@ -138,12 +140,8 @@ class LoggedInFlowNode @AssistedInject constructor(
backstack.push(NavTarget.Ftue) backstack.push(NavTarget.Ftue)
} }
}, },
onStart = {
lifecycleScope.launch {
syncService.startSync()
}
},
onStop = { onStop = {
//Counterpart startSync is done in observeSyncStateAndNetworkStatus method.
syncService.stopSync() syncService.stopSync()
}, },
onDestroy = { onDestroy = {
@ -153,22 +151,24 @@ class LoggedInFlowNode @AssistedInject constructor(
loggedInFlowProcessor.stopObserving() loggedInFlowProcessor.stopObserving()
} }
) )
observeSyncStateAndNetworkStatus() observeSyncStateAndNetworkStatus()
} }
@OptIn(FlowPreview::class)
private fun observeSyncStateAndNetworkStatus() { private fun observeSyncStateAndNetworkStatus() {
lifecycleScope.launch { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) { repeatOnLifecycle(Lifecycle.State.STARTED) {
combine( combine(
syncService.syncState, // small debounce to avoid spamming startSync when the state is changing quickly in case of error.
syncService.syncState.debounce(100),
networkMonitor.connectivity networkMonitor.connectivity
) { syncState, networkStatus -> ) { syncState, networkStatus ->
syncState == SyncState.Error && networkStatus == NetworkStatus.Online Pair(syncState, networkStatus)
} }
.distinctUntilChanged() .distinctUntilChanged()
.collect { restartSync -> .collect { (syncState, networkStatus) ->
if (restartSync) { Timber.d("Sync state: $syncState, network status: $networkStatus")
if (syncState != SyncState.Running && networkStatus == NetworkStatus.Online) {
syncService.startSync() syncService.startSync()
} }
} }
@ -351,3 +351,4 @@ class LoggedInFlowNode @AssistedInject constructor(
backstack.push(NavTarget.InviteList) backstack.push(NavTarget.InviteList)
} }
} }

View file

@ -21,16 +21,27 @@ import android.os.Build
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.api.PushService
import kotlinx.coroutines.delay
import javax.inject.Inject import javax.inject.Inject
private const val DELAY_BEFORE_SHOWING_SYNC_SPINNER_IN_MILLIS = 1500L
class LoggedInPresenter @Inject constructor( class LoggedInPresenter @Inject constructor(
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val permissionsPresenterFactory: PermissionsPresenter.Factory, private val permissionsPresenterFactory: PermissionsPresenter.Factory,
private val networkMonitor: NetworkMonitor,
private val pushService: PushService, private val pushService: PushService,
) : Presenter<LoggedInState> { ) : Presenter<LoggedInState> {
@ -53,18 +64,25 @@ class LoggedInPresenter @Inject constructor(
pushService.registerWith(matrixClient, pushProvider, distributor) pushService.registerWith(matrixClient, pushProvider, distributor)
} }
val syncState = matrixClient.syncService().syncState.collectAsState() val roomListState by matrixClient.roomListService.state.collectAsState()
val networkStatus by networkMonitor.connectivity.collectAsState()
val permissionsState = postNotificationPermissionsPresenter.present() val permissionsState = postNotificationPermissionsPresenter.present()
var showSyncSpinner by remember {
// fun handleEvents(event: LoggedInEvents) { mutableStateOf(false)
// when (event) { }
// } LaunchedEffect(roomListState, networkStatus) {
// } showSyncSpinner = when {
networkStatus == NetworkStatus.Offline -> false
roomListState == RoomListService.State.Running -> false
else -> {
delay(DELAY_BEFORE_SHOWING_SYNC_SPINNER_IN_MILLIS)
true
}
}
}
return LoggedInState( return LoggedInState(
syncState = syncState.value, showSyncSpinner = showSyncSpinner,
permissionsState = permissionsState, permissionsState = permissionsState,
// eventSink = ::handleEvents
) )
} }
} }

View file

@ -16,11 +16,9 @@
package io.element.android.appnav.loggedin package io.element.android.appnav.loggedin
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.permissions.api.PermissionsState import io.element.android.libraries.permissions.api.PermissionsState
data class LoggedInState( data class LoggedInState(
val syncState: SyncState, val showSyncSpinner: Boolean,
val permissionsState: PermissionsState, val permissionsState: PermissionsState,
// val eventSink: (LoggedInEvents) -> Unit
) )

View file

@ -17,22 +17,20 @@
package io.element.android.appnav.loggedin package io.element.android.appnav.loggedin
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.permissions.api.createDummyPostNotificationPermissionsState import io.element.android.libraries.permissions.api.createDummyPostNotificationPermissionsState
open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> { open class LoggedInStateProvider : PreviewParameterProvider<LoggedInState> {
override val values: Sequence<LoggedInState> override val values: Sequence<LoggedInState>
get() = sequenceOf( get() = sequenceOf(
aLoggedInState(), aLoggedInState(false),
aLoggedInState(syncState = SyncState.Idle), aLoggedInState(true),
// Add other state here // Add other state here
) )
} }
fun aLoggedInState( fun aLoggedInState(
syncState: SyncState = SyncState.Running, showSyncSpinner: Boolean = true,
) = LoggedInState( ) = LoggedInState(
syncState = syncState, showSyncSpinner = showSyncSpinner,
permissionsState = createDummyPostNotificationPermissionsState(), permissionsState = createDummyPostNotificationPermissionsState(),
// eventSink = {}
) )

View file

@ -47,7 +47,7 @@ fun LoggedInView(
modifier = Modifier modifier = Modifier
.padding(top = 8.dp) .padding(top = 8.dp)
.align(Alignment.TopCenter), .align(Alignment.TopCenter),
syncState = state.syncState, isVisible = state.showSyncSpinner,
) )
PermissionsView( PermissionsView(
state = state.permissionsState, state = state.permissionsState,

View file

@ -38,19 +38,18 @@ import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable
fun SyncStateView( fun SyncStateView(
syncState: SyncState, isVisible: Boolean,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val animationSpec = spring<Float>(stiffness = 500F) val animationSpec = spring<Float>(stiffness = 500F)
AnimatedVisibility( AnimatedVisibility(
modifier = modifier, modifier = modifier,
visible = syncState.mustBeVisible(), visible = isVisible,
enter = fadeIn(animationSpec = animationSpec), enter = fadeIn(animationSpec = animationSpec),
exit = fadeOut(animationSpec = animationSpec), exit = fadeOut(animationSpec = animationSpec),
) { ) {
@ -60,15 +59,15 @@ fun SyncStateView(
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.background(color = ElementTheme.colors.bgSubtleSecondary) .background(color = ElementTheme.colors.bgSubtleSecondary)
.padding(horizontal = 24.dp, vertical = 10.dp), .padding(horizontal = 24.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp) horizontalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
CircularProgressIndicator( CircularProgressIndicator(
modifier = Modifier modifier = Modifier
.progressSemantics() .progressSemantics()
.size(12.dp), .size(12.dp),
color = ElementTheme.colors.textPrimary, color = ElementTheme.colors.textPrimary,
strokeWidth = 1.5.dp, strokeWidth = 1.5.dp,
) )
@ -82,20 +81,13 @@ fun SyncStateView(
} }
} }
private fun SyncState.mustBeVisible() = when (this) {
SyncState.Idle -> true /* Cold start of the app */
SyncState.Running -> false
SyncState.Error -> false /* In this case, the network error banner can be displayed */
SyncState.Terminated -> true /* The app is resumed and the sync is started again */
}
@DayNightPreviews @DayNightPreviews
@Composable @Composable
internal fun SyncStateViewPreview() = ElementPreview { internal fun SyncStateViewPreview() = ElementPreview {
// Add a box to see the shadow // Add a box to see the shadow
Box(modifier = Modifier.padding(24.dp)) { Box(modifier = Modifier.padding(24.dp)) {
SyncStateView( SyncStateView(
syncState = SyncState.Idle isVisible = true
) )
} }
} }

View file

@ -20,13 +20,18 @@ import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.api.PermissionsPresenter
import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter
import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.pushproviders.api.Distributor import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider import io.element.android.libraries.pushproviders.api.PushProvider
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -42,14 +47,33 @@ class LoggedInPresenterTest {
} }
} }
private fun createPresenter(): LoggedInPresenter { @Test
fun `present - show sync spinner`() = runTest {
val roomListService = FakeRoomListService()
val presenter = createPresenter(roomListService, NetworkStatus.Online)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.showSyncSpinner).isFalse()
consumeItemsUntilPredicate { it.showSyncSpinner }
roomListService.postState(RoomListService.State.Running)
consumeItemsUntilPredicate { !it.showSyncSpinner }
}
}
private fun createPresenter(
roomListService: RoomListService = FakeRoomListService(),
networkStatus: NetworkStatus = NetworkStatus.Offline
): LoggedInPresenter {
return LoggedInPresenter( return LoggedInPresenter(
matrixClient = FakeMatrixClient(), matrixClient = FakeMatrixClient(roomListService = roomListService),
permissionsPresenterFactory = object : PermissionsPresenter.Factory { permissionsPresenterFactory = object : PermissionsPresenter.Factory {
override fun create(permission: String): PermissionsPresenter { override fun create(permission: String): PermissionsPresenter {
return NoopPermissionsPresenter() return NoopPermissionsPresenter()
} }
}, },
networkMonitor = FakeNetworkMonitor(networkStatus),
pushService = object : PushService { pushService = object : PushService {
override fun notificationStyleChanged() { override fun notificationStyleChanged() {
} }

View file

@ -18,12 +18,12 @@ package io.element.android.appnav.room
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth import com.google.common.truth.Truth
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -47,29 +47,29 @@ class LoadingRoomStateFlowFactoryTest {
@Test @Test
fun `flow should emit Loading and then Loaded when there is a room in cache after SS is loaded`() = runTest { fun `flow should emit Loading and then Loaded when there is a room in cache after SS is loaded`() = runTest {
val room = FakeMatrixRoom(sessionId= A_SESSION_ID, roomId = A_ROOM_ID) val room = FakeMatrixRoom(sessionId= A_SESSION_ID, roomId = A_ROOM_ID)
val roomSummaryDataSource = FakeRoomSummaryDataSource() val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomSummaryDataSource = roomSummaryDataSource) val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
val flowFactory = LoadingRoomStateFlowFactory(matrixClient) val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
flowFactory flowFactory
.create(this, A_ROOM_ID) .create(this, A_ROOM_ID)
.test { .test {
Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading) Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
matrixClient.givenGetRoomResult(A_ROOM_ID, room) matrixClient.givenGetRoomResult(A_ROOM_ID, room)
roomSummaryDataSource.postLoadingState(RoomSummaryDataSource.LoadingState.Loaded(1)) roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room)) Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loaded(room))
} }
} }
@Test @Test
fun `flow should emit Loading and then Error when there is no room in cache after SS is loaded`() = runTest { fun `flow should emit Loading and then Error when there is no room in cache after SS is loaded`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource() val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomSummaryDataSource = roomSummaryDataSource) val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
val flowFactory = LoadingRoomStateFlowFactory(matrixClient) val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
flowFactory flowFactory
.create(this, A_ROOM_ID) .create(this, A_ROOM_ID)
.test { .test {
Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading) Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
roomSummaryDataSource.postLoadingState(RoomSummaryDataSource.LoadingState.Loaded(1)) roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Error) Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Error)
} }
} }

View file

@ -36,7 +36,7 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.extensions.toAnalyticsJoinedRoom import io.element.android.services.analytics.api.extensions.toAnalyticsJoinedRoom
@ -56,8 +56,9 @@ class InviteListPresenter @Inject constructor(
@Composable @Composable
override fun present(): InviteListState { override fun present(): InviteListState {
val invites by client val invites by client
.roomSummaryDataSource .roomListService
.inviteRooms() .invites()
.summaries
.collectAsState() .collectAsState()
var seenInvites by remember { mutableStateOf<Set<RoomId>>(emptySet()) } var seenInvites by remember { mutableStateOf<Set<RoomId>>(emptySet()) }

View file

@ -30,8 +30,8 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_ROOM_ID_2
@ -40,7 +40,7 @@ import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analytics.api.AnalyticsService
@ -51,9 +51,9 @@ class InviteListPresenterTests {
@Test @Test
fun `present - starts empty, adds invites when received`() = runTest { fun `present - starts empty, adds invites when received`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource() val roomListService = FakeRoomListService()
val presenter = createPresenter( val presenter = createPresenter(
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource) FakeMatrixClient(roomListService = roomListService)
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -61,7 +61,7 @@ class InviteListPresenterTests {
val initialState = awaitItem() val initialState = awaitItem()
Truth.assertThat(initialState.inviteList).isEmpty() Truth.assertThat(initialState.inviteList).isEmpty()
roomSummaryDataSource.postInviteRooms(listOf(aRoomSummary())) roomListService.postInviteRooms(listOf(aRoomSummary()))
val withInviteState = awaitItem() val withInviteState = awaitItem()
Truth.assertThat(withInviteState.inviteList.size).isEqualTo(1) Truth.assertThat(withInviteState.inviteList.size).isEqualTo(1)
@ -72,9 +72,9 @@ class InviteListPresenterTests {
@Test @Test
fun `present - uses user ID and avatar for direct invites`() = runTest { fun `present - uses user ID and avatar for direct invites`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withDirectChatInvitation() val roomListService = FakeRoomListService().withDirectChatInvitation()
val presenter = createPresenter( val presenter = createPresenter(
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource) FakeMatrixClient(roomListService = roomListService)
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -98,9 +98,9 @@ class InviteListPresenterTests {
@Test @Test
fun `present - includes sender details for room invites`() = runTest { fun `present - includes sender details for room invites`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val roomListService = FakeRoomListService().withRoomInvitation()
val presenter = createPresenter( val presenter = createPresenter(
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource) FakeMatrixClient(roomListService = roomListService)
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -122,10 +122,10 @@ class InviteListPresenterTests {
@Test @Test
fun `present - shows confirm dialog for declining direct chat invites`() = runTest { fun `present - shows confirm dialog for declining direct chat invites`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withDirectChatInvitation() val roomListService = FakeRoomListService().withDirectChatInvitation()
val presenter = InviteListPresenter( val presenter = InviteListPresenter(
FakeMatrixClient( FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource, roomListService = roomListService,
), ),
FakeSeenInvitesStore(), FakeSeenInvitesStore(),
FakeAnalyticsService(), FakeAnalyticsService(),
@ -148,9 +148,9 @@ class InviteListPresenterTests {
@Test @Test
fun `present - shows confirm dialog for declining room invites`() = runTest { fun `present - shows confirm dialog for declining room invites`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val roomListService = FakeRoomListService().withRoomInvitation()
val presenter = createPresenter( val presenter = createPresenter(
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource) FakeMatrixClient(roomListService = roomListService)
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -169,9 +169,9 @@ class InviteListPresenterTests {
@Test @Test
fun `present - hides confirm dialog when cancelling`() = runTest { fun `present - hides confirm dialog when cancelling`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val roomListService = FakeRoomListService().withRoomInvitation()
val presenter = createPresenter( val presenter = createPresenter(
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource) FakeMatrixClient(roomListService = roomListService)
) )
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -190,10 +190,10 @@ class InviteListPresenterTests {
@Test @Test
fun `present - declines invite after confirming`() = runTest { fun `present - declines invite after confirming`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val roomListService = FakeRoomListService().withRoomInvitation()
val fakeNotificationDrawerManager = FakeNotificationDrawerManager() val fakeNotificationDrawerManager = FakeNotificationDrawerManager()
val client = FakeMatrixClient( val client = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource, roomListService = roomListService,
) )
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager) val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager)
@ -217,9 +217,9 @@ class InviteListPresenterTests {
@Test @Test
fun `present - declines invite after confirming and sets state on error`() = runTest { fun `present - declines invite after confirming and sets state on error`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val roomListService = FakeRoomListService().withRoomInvitation()
val client = FakeMatrixClient( val client = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource, roomListService = roomListService,
) )
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
val presenter = createPresenter(client) val presenter = createPresenter(client)
@ -247,9 +247,9 @@ class InviteListPresenterTests {
@Test @Test
fun `present - dismisses declining error state`() = runTest { fun `present - dismisses declining error state`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val roomListService = FakeRoomListService().withRoomInvitation()
val client = FakeMatrixClient( val client = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource, roomListService = roomListService,
) )
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
val presenter = createPresenter(client) val presenter = createPresenter(client)
@ -279,10 +279,10 @@ class InviteListPresenterTests {
@Test @Test
fun `present - accepts invites and sets state on success`() = runTest { fun `present - accepts invites and sets state on success`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val roomListService = FakeRoomListService().withRoomInvitation()
val fakeNotificationDrawerManager = FakeNotificationDrawerManager() val fakeNotificationDrawerManager = FakeNotificationDrawerManager()
val client = FakeMatrixClient( val client = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource, roomListService = roomListService,
) )
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager) val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager)
@ -303,9 +303,9 @@ class InviteListPresenterTests {
@Test @Test
fun `present - accepts invites and sets state on error`() = runTest { fun `present - accepts invites and sets state on error`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val roomListService = FakeRoomListService().withRoomInvitation()
val client = FakeMatrixClient( val client = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource, roomListService = roomListService,
) )
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
val presenter = createPresenter(client) val presenter = createPresenter(client)
@ -325,9 +325,9 @@ class InviteListPresenterTests {
@Test @Test
fun `present - dismisses accepting error state`() = runTest { fun `present - dismisses accepting error state`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val roomListService = FakeRoomListService().withRoomInvitation()
val client = FakeMatrixClient( val client = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource, roomListService = roomListService,
) )
val room = FakeMatrixRoom() val room = FakeMatrixRoom()
val presenter = createPresenter(client) val presenter = createPresenter(client)
@ -352,11 +352,11 @@ class InviteListPresenterTests {
@Test @Test
fun `present - stores seen invites when received`() = runTest { fun `present - stores seen invites when received`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource() val roomListService = FakeRoomListService()
val store = FakeSeenInvitesStore() val store = FakeSeenInvitesStore()
val presenter = InviteListPresenter( val presenter = InviteListPresenter(
FakeMatrixClient( FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource, roomListService = roomListService,
), ),
store, store,
FakeAnalyticsService(), FakeAnalyticsService(),
@ -368,19 +368,19 @@ class InviteListPresenterTests {
awaitItem() awaitItem()
// When one invite is received, that ID is saved // When one invite is received, that ID is saved
roomSummaryDataSource.postInviteRooms(listOf(aRoomSummary())) roomListService.postInviteRooms(listOf(aRoomSummary()))
awaitItem() awaitItem()
Truth.assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID)) Truth.assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID))
// When a second is added, both are saved // When a second is added, both are saved
roomSummaryDataSource.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2))) roomListService.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
awaitItem() awaitItem()
Truth.assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID, A_ROOM_ID_2)) Truth.assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID, A_ROOM_ID_2))
// When they're both dismissed, an empty set is saved // When they're both dismissed, an empty set is saved
roomSummaryDataSource.postInviteRooms(listOf()) roomListService.postInviteRooms(listOf())
awaitItem() awaitItem()
Truth.assertThat(store.getProvidedRoomIds()).isEmpty() Truth.assertThat(store.getProvidedRoomIds()).isEmpty()
@ -389,12 +389,12 @@ class InviteListPresenterTests {
@Test @Test
fun `present - marks invite as new if they're unseen`() = runTest { fun `present - marks invite as new if they're unseen`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource() val roomListService = FakeRoomListService()
val store = FakeSeenInvitesStore() val store = FakeSeenInvitesStore()
store.publishRoomIds(setOf(A_ROOM_ID)) store.publishRoomIds(setOf(A_ROOM_ID))
val presenter = InviteListPresenter( val presenter = InviteListPresenter(
FakeMatrixClient( FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource, roomListService = roomListService,
), ),
store, store,
FakeAnalyticsService(), FakeAnalyticsService(),
@ -405,7 +405,7 @@ class InviteListPresenterTests {
}.test { }.test {
awaitItem() awaitItem()
roomSummaryDataSource.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2))) roomListService.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
skipItems(1) skipItems(1)
val withInviteState = awaitItem() val withInviteState = awaitItem()
@ -417,7 +417,7 @@ class InviteListPresenterTests {
} }
} }
private suspend fun FakeRoomSummaryDataSource.withRoomInvitation(): FakeRoomSummaryDataSource { private suspend fun FakeRoomListService.withRoomInvitation(): FakeRoomListService {
postInviteRooms( postInviteRooms(
listOf( listOf(
RoomSummary.Filled( RoomSummary.Filled(
@ -446,7 +446,7 @@ class InviteListPresenterTests {
return this return this
} }
private suspend fun FakeRoomSummaryDataSource.withDirectChatInvitation(): FakeRoomSummaryDataSource { private suspend fun FakeRoomListService.withDirectChatInvitation(): FakeRoomListService {
postInviteRooms( postInviteRooms(
listOf( listOf(
RoomSummary.Filled( RoomSummary.Filled(

View file

@ -16,7 +16,7 @@
package io.element.android.features.messages.impl.forward package io.element.android.features.messages.impl.forward
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
sealed interface ForwardMessagesEvents { sealed interface ForwardMessagesEvents {
data class SetSelectedRoom(val room: RoomSummaryDetails) : ForwardMessagesEvents data class SetSelectedRoom(val room: RoomSummaryDetails) : ForwardMessagesEvents

View file

@ -35,8 +35,8 @@ import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
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.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
@ -65,7 +65,7 @@ class ForwardMessagesPresenter @AssistedInject constructor(
var results: SearchBarResultState<ImmutableList<RoomSummaryDetails>> by remember { mutableStateOf(SearchBarResultState.NotSearching()) } var results: SearchBarResultState<ImmutableList<RoomSummaryDetails>> by remember { mutableStateOf(SearchBarResultState.NotSearching()) }
val forwardingActionState: MutableState<Async<ImmutableList<RoomId>>> = remember { mutableStateOf(Async.Uninitialized) } val forwardingActionState: MutableState<Async<ImmutableList<RoomId>>> = remember { mutableStateOf(Async.Uninitialized) }
val summaries by client.roomSummaryDataSource.allRooms().collectAsState() val summaries by client.roomListService.allRooms().summaries.collectAsState()
LaunchedEffect(query, summaries) { LaunchedEffect(query, summaries) {
val filteredSummaries = summaries.filterIsInstance<RoomSummary.Filled>() val filteredSummaries = summaries.filterIsInstance<RoomSummary.Filled>()

View file

@ -18,7 +18,7 @@ package io.element.android.features.messages.impl.forward
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
data class ForwardMessagesState( data class ForwardMessagesState(

View file

@ -20,7 +20,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.matrix.api.room.message.RoomMessage import io.element.android.libraries.matrix.api.room.message.RoomMessage
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf

View file

@ -63,7 +63,7 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.designsystem.theme.roomListRoomMessage import io.element.android.libraries.designsystem.theme.roomListRoomMessage
import io.element.android.libraries.designsystem.theme.roomListRoomName import io.element.android.libraries.designsystem.theme.roomListRoomName
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.matrix.ui.components.SelectedRoom import io.element.android.libraries.matrix.ui.components.SelectedRoom
import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.theme.ElementTheme
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings

View file

@ -24,12 +24,12 @@ import io.element.android.features.messages.impl.forward.ForwardMessagesEvents
import io.element.android.features.messages.impl.forward.ForwardMessagesPresenter import io.element.android.features.messages.impl.forward.ForwardMessagesPresenter
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetail import io.element.android.libraries.matrix.test.room.aRoomSummaryDetail
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -76,10 +76,10 @@ class ForwardMessagesPresenterTests {
@Test @Test
fun `present - update query`() = runTest { fun `present - update query`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().apply { val roomListService = FakeRoomListService().apply {
postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetail()))) postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetail())))
} }
val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource) val client = FakeMatrixClient(roomListService = roomListService)
val presenter = aPresenter(client = client) val presenter = aPresenter(client = client)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
@ -166,11 +166,10 @@ class ForwardMessagesPresenterTests {
} }
} }
private fun CoroutineScope.aPresenter( private fun CoroutineScope.aPresenter(
eventId: EventId = AN_EVENT_ID, eventId: EventId = AN_EVENT_ID,
fakeMatrixRoom: FakeMatrixRoom = FakeMatrixRoom(), fakeMatrixRoom: FakeMatrixRoom = FakeMatrixRoom(),
coroutineScope: CoroutineScope = this, coroutineScope: CoroutineScope = this,
client: FakeMatrixClient = FakeMatrixClient(), client: FakeMatrixClient = FakeMatrixClient(),
) = ForwardMessagesPresenter(eventId.value, fakeMatrixRoom, coroutineScope, client) ) = ForwardMessagesPresenter(eventId.value, fakeMatrixRoom, coroutineScope, client)
} }

View file

@ -135,6 +135,6 @@ class RoomListPresenter @Inject constructor(
// Safe to give bigger size than room list // Safe to give bigger size than room list
val extendedRangeEnd = range.last + midExtendedRangeSize val extendedRangeEnd = range.last + midExtendedRangeSize
val extendedRange = IntRange(extendedRangeStart, extendedRangeEnd) val extendedRange = IntRange(extendedRangeStart, extendedRangeEnd)
client.roomSummaryDataSource.updateAllRoomsVisibleRange(extendedRange) client.roomListService.updateAllRoomsVisibleRange(extendedRange)
} }
} }

View file

@ -30,7 +30,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
@ -44,8 +44,9 @@ class DefaultInviteStateDataSource @Inject constructor(
@Composable @Composable
override fun inviteState(): InvitesState { override fun inviteState(): InvitesState {
val invites by client val invites by client
.roomSummaryDataSource .roomListService
.inviteRooms() .invites()
.summaries
.collectAsState() .collectAsState()
val seenInvites by seenInvitesStore val seenInvites by seenInvitesStore

View file

@ -27,8 +27,8 @@ import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -44,7 +44,7 @@ import kotlinx.coroutines.withContext
import javax.inject.Inject import javax.inject.Inject
class RoomListDataSource @Inject constructor( class RoomListDataSource @Inject constructor(
private val roomSummaryDataSource: RoomSummaryDataSource, private val roomListService: RoomListService,
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter, private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
private val roomLastMessageFormatter: RoomLastMessageFormatter, private val roomLastMessageFormatter: RoomLastMessageFormatter,
private val coroutineDispatchers: CoroutineDispatchers, private val coroutineDispatchers: CoroutineDispatchers,
@ -61,8 +61,9 @@ class RoomListDataSource @Inject constructor(
} }
fun launchIn(coroutineScope: CoroutineScope) { fun launchIn(coroutineScope: CoroutineScope) {
roomSummaryDataSource roomListService
.allRooms() .allRooms()
.summaries
.onEach { roomSummaries -> .onEach { roomSummaries ->
replaceWith(roomSummaries) replaceWith(roomSummaries)
} }

View file

@ -47,8 +47,8 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME
import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID
import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.A_USER_NAME
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
@ -111,9 +111,9 @@ class RoomListPresenterTests {
@Test @Test
fun `present - load 1 room with success`() = runTest { fun `present - load 1 room with success`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource() val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource roomListService = roomListService
) )
val presenter = createRoomListPresenter(matrixClient) val presenter = createRoomListPresenter(matrixClient)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
@ -123,7 +123,7 @@ class RoomListPresenterTests {
// Room list is loaded with 16 placeholders // Room list is loaded with 16 placeholders
Truth.assertThat(initialState.roomList.size).isEqualTo(16) Truth.assertThat(initialState.roomList.size).isEqualTo(16)
Truth.assertThat(initialState.roomList.all { it.isPlaceholder }).isTrue() Truth.assertThat(initialState.roomList.all { it.isPlaceholder }).isTrue()
roomSummaryDataSource.postAllRooms(listOf(aRoomSummaryFilled())) roomListService.postAllRooms(listOf(aRoomSummaryFilled()))
val withRoomState = consumeItemsUntilPredicate { state -> state.roomList.size == 1 }.last() val withRoomState = consumeItemsUntilPredicate { state -> state.roomList.size == 1 }.last()
Truth.assertThat(withRoomState.roomList.size).isEqualTo(1) Truth.assertThat(withRoomState.roomList.size).isEqualTo(1)
Truth.assertThat(withRoomState.roomList.first()) Truth.assertThat(withRoomState.roomList.first())
@ -133,15 +133,15 @@ class RoomListPresenterTests {
@Test @Test
fun `present - load 1 room with success and filter rooms`() = runTest { fun `present - load 1 room with success and filter rooms`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource() val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource roomListService = roomListService
) )
val presenter = createRoomListPresenter(matrixClient) val presenter = createRoomListPresenter(matrixClient)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
roomSummaryDataSource.postAllRooms(listOf(aRoomSummaryFilled())) roomListService.postAllRooms(listOf(aRoomSummaryFilled()))
val loadedState = consumeItemsUntilPredicate { state -> state.roomList.size == 1 }.last() val loadedState = consumeItemsUntilPredicate { state -> state.roomList.size == 1 }.last()
// Test filtering with result // Test filtering with result
loadedState.eventSink.invoke(RoomListEvents.UpdateFilter(A_ROOM_NAME.substring(0, 3))) loadedState.eventSink.invoke(RoomListEvents.UpdateFilter(A_ROOM_NAME.substring(0, 3)))
@ -160,39 +160,39 @@ class RoomListPresenterTests {
@Test @Test
fun `present - update visible range`() = runTest { fun `present - update visible range`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource() val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource roomListService = roomListService
) )
val presenter = createRoomListPresenter(matrixClient) val presenter = createRoomListPresenter(matrixClient)
moleculeFlow(RecompositionMode.Immediate) { moleculeFlow(RecompositionMode.Immediate) {
presenter.present() presenter.present()
}.test { }.test {
roomSummaryDataSource.postAllRooms(listOf(aRoomSummaryFilled())) roomListService.postAllRooms(listOf(aRoomSummaryFilled()))
val loadedState = awaitItem() val loadedState = awaitItem()
// check initial value // check initial value
Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange).isNull() Truth.assertThat(roomListService.latestSlidingSyncRange).isNull()
// Test empty range // Test empty range
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(1, 0))) loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(1, 0)))
Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange).isNull() Truth.assertThat(roomListService.latestSlidingSyncRange).isNull()
// Update visible range and check that range is transmitted to the SDK after computation // Update visible range and check that range is transmitted to the SDK after computation
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(0, 0))) loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(0, 0)))
Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange) Truth.assertThat(roomListService.latestSlidingSyncRange)
.isEqualTo(IntRange(0, 20)) .isEqualTo(IntRange(0, 20))
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(0, 1))) loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(0, 1)))
Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange) Truth.assertThat(roomListService.latestSlidingSyncRange)
.isEqualTo(IntRange(0, 21)) .isEqualTo(IntRange(0, 21))
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(19, 29))) loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(19, 29)))
Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange) Truth.assertThat(roomListService.latestSlidingSyncRange)
.isEqualTo(IntRange(0, 49)) .isEqualTo(IntRange(0, 49))
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(49, 59))) loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(49, 59)))
Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange) Truth.assertThat(roomListService.latestSlidingSyncRange)
.isEqualTo(IntRange(29, 79)) .isEqualTo(IntRange(29, 79))
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(149, 159))) loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(149, 159)))
Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange) Truth.assertThat(roomListService.latestSlidingSyncRange)
.isEqualTo(IntRange(129, 179)) .isEqualTo(IntRange(129, 179))
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(149, 259))) loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(149, 259)))
Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange) Truth.assertThat(roomListService.latestSlidingSyncRange)
.isEqualTo(IntRange(129, 279)) .isEqualTo(IntRange(129, 279))
cancelAndIgnoreRemainingEvents() cancelAndIgnoreRemainingEvents()
} }
@ -200,9 +200,9 @@ class RoomListPresenterTests {
@Test @Test
fun `present - handle DismissRequestVerificationPrompt`() = runTest { fun `present - handle DismissRequestVerificationPrompt`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource() val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient( val matrixClient = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource roomListService = roomListService
) )
val presenter = createRoomListPresenter( val presenter = createRoomListPresenter(
client = matrixClient, client = matrixClient,
@ -317,7 +317,7 @@ class RoomListPresenterTests {
inviteStateDataSource = inviteStateDataSource, inviteStateDataSource = inviteStateDataSource,
leaveRoomPresenter = leaveRoomPresenter, leaveRoomPresenter = leaveRoomPresenter,
roomListDataSource = RoomListDataSource( roomListDataSource = RoomListDataSource(
client.roomSummaryDataSource, client.roomListService,
lastMessageTimestampFormatter, lastMessageTimestampFormatter,
roomLastMessageFormatter, roomLastMessageFormatter,
coroutineDispatchers = testCoroutineDispatchers() coroutineDispatchers = testCoroutineDispatchers()

View file

@ -25,8 +25,8 @@ import io.element.android.features.roomlist.impl.InvitesState
import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_ROOM_ID_2
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.tests.testutils.testCoroutineDispatchers import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
@ -35,8 +35,8 @@ internal class DefaultInviteStateDataSourceTest {
@Test @Test
fun `emits NoInvites state if invites list is empty`() = runTest { fun `emits NoInvites state if invites list is empty`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource() val roomListService = FakeRoomListService()
val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource) val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore() val seenStore = FakeSeenInvitesStore()
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers()) val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
@ -49,9 +49,9 @@ internal class DefaultInviteStateDataSourceTest {
@Test @Test
fun `emits NewInvites state if unseen invite exists`() = runTest { fun `emits NewInvites state if unseen invite exists`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource() val roomListService = FakeRoomListService()
roomSummaryDataSource.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID))) roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource) val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore() val seenStore = FakeSeenInvitesStore()
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers()) val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
@ -65,9 +65,9 @@ internal class DefaultInviteStateDataSourceTest {
@Test @Test
fun `emits NewInvites state if multiple invites exist and at least one is unseen`() = runTest { fun `emits NewInvites state if multiple invites exist and at least one is unseen`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource() val roomListService = FakeRoomListService()
roomSummaryDataSource.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2))) roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2)))
val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource) val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore() val seenStore = FakeSeenInvitesStore()
seenStore.publishRoomIds(setOf(A_ROOM_ID)) seenStore.publishRoomIds(setOf(A_ROOM_ID))
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers(useUnconfinedTestDispatcher = true)) val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers(useUnconfinedTestDispatcher = true))
@ -82,9 +82,9 @@ internal class DefaultInviteStateDataSourceTest {
@Test @Test
fun `emits SeenInvites state if invite exists in seen store`() = runTest { fun `emits SeenInvites state if invite exists in seen store`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource() val roomListService = FakeRoomListService()
roomSummaryDataSource.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID))) roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource) val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore() val seenStore = FakeSeenInvitesStore()
seenStore.publishRoomIds(setOf(A_ROOM_ID)) seenStore.publishRoomIds(setOf(A_ROOM_ID))
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers(useUnconfinedTestDispatcher = true)) val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers(useUnconfinedTestDispatcher = true))
@ -100,8 +100,8 @@ internal class DefaultInviteStateDataSourceTest {
@Test @Test
fun `emits new state in response to upstream events`() = runTest { fun `emits new state in response to upstream events`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource() val roomListService = FakeRoomListService()
val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource) val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore() val seenStore = FakeSeenInvitesStore()
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers()) val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
@ -112,7 +112,7 @@ internal class DefaultInviteStateDataSourceTest {
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites) Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
// When a single invite is received, state should be NewInvites // When a single invite is received, state should be NewInvites
roomSummaryDataSource.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID))) roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
skipItems(1) skipItems(1)
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites) Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
@ -122,12 +122,12 @@ internal class DefaultInviteStateDataSourceTest {
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.SeenInvites) Truth.assertThat(awaitItem()).isEqualTo(InvitesState.SeenInvites)
// Another new invite resets it to NewInvites // Another new invite resets it to NewInvites
roomSummaryDataSource.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2))) roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2)))
skipItems(1) skipItems(1)
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites) Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
// All of the invites going away reverts to NoInvites // All of the invites going away reverts to NoInvites
roomSummaryDataSource.postInviteRooms(emptyList()) roomListService.postInviteRooms(emptyList())
skipItems(1) skipItems(1)
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites) Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
} }

View file

@ -26,7 +26,7 @@ import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.PushersService
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.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
@ -35,7 +35,7 @@ import java.io.Closeable
interface MatrixClient : Closeable { interface MatrixClient : Closeable {
val sessionId: SessionId val sessionId: SessionId
val roomSummaryDataSource: RoomSummaryDataSource val roomListService: RoomListService
val mediaLoader: MatrixMediaLoader val mediaLoader: MatrixMediaLoader
suspend fun getRoom(roomId: RoomId): MatrixRoom? suspend fun getRoom(roomId: RoomId): MatrixRoom?
suspend fun findDM(userId: UserId): MatrixRoom? suspend fun findDM(userId: UserId): MatrixRoom?

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.libraries.matrix.api.room package io.element.android.libraries.matrix.api.roomlist
import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -23,25 +23,34 @@ import kotlinx.coroutines.withTimeout
import timber.log.Timber import timber.log.Timber
import kotlin.time.Duration import kotlin.time.Duration
interface RoomSummaryDataSource { /**
* Holds some flows related to a specific set of rooms.
* Can be retrieved from [RoomListService] methods.
*/
interface RoomList {
sealed class LoadingState { sealed class LoadingState {
object NotLoaded : LoadingState() object NotLoaded : LoadingState()
data class Loaded(val numberOfRooms: Int) : LoadingState() data class Loaded(val numberOfRooms: Int) : LoadingState()
} }
fun updateAllRoomsVisibleRange(range: IntRange) /**
fun allRoomsLoadingState(): StateFlow<LoadingState> * The list of room summaries as a flow.
fun allRooms(): StateFlow<List<RoomSummary>> */
fun inviteRooms(): StateFlow<List<RoomSummary>> val summaries: StateFlow<List<RoomSummary>>
/**
* The loading state of the room list as a flow.
* This is useful to know if a specific set of rooms is loaded or not.
*/
val loadingState: StateFlow<LoadingState>
} }
suspend fun RoomSummaryDataSource.awaitAllRoomsAreLoaded(timeout: Duration = Duration.INFINITE) { suspend fun RoomList.awaitLoaded(timeout: Duration = Duration.INFINITE) {
try { try {
Timber.d("awaitAllRoomsAreLoaded: wait") Timber.d("awaitAllRoomsAreLoaded: wait")
withTimeout(timeout) { withTimeout(timeout) {
allRoomsLoadingState().firstOrNull { loadingState.firstOrNull {
it is RoomSummaryDataSource.LoadingState.Loaded it is RoomList.LoadingState.Loaded
} }
} }
} catch (timeoutException: TimeoutCancellationException) { } catch (timeoutException: TimeoutCancellationException) {

View file

@ -0,0 +1,56 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.roomlist
import kotlinx.coroutines.flow.StateFlow
/**
* Entry point for the room list api.
* This service will provide different sets of rooms (all, invites, etc.).
* It requires the SyncService to be started to receive updates.
*/
interface RoomListService {
sealed class State {
object Idle : State()
object Running : State()
object Error : State()
object Terminated : State()
}
/**
* returns a [RoomList] object of all rooms we want to display.
* This will exclude some rooms like the invites, or spaces.
*/
fun allRooms(): RoomList
/**
* returns a [RoomList] object of all invites.
*/
fun invites(): RoomList
/**
* Will set the visible range of all rooms.
* This is useful to load more data when the user scrolls down.
*/
fun updateAllRoomsVisibleRange(range: IntRange)
/**
* The state of the service as a flow.
*/
val state: StateFlow<State>
}

View file

@ -14,9 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.libraries.matrix.api.room package io.element.android.libraries.matrix.api.roomlist
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.message.RoomMessage import io.element.android.libraries.matrix.api.room.message.RoomMessage
sealed interface RoomSummary { sealed interface RoomSummary {

View file

@ -32,8 +32,8 @@ import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.PushersService
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.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.room.awaitAllRoomsAreLoaded import io.element.android.libraries.matrix.api.roomlist.awaitLoaded
import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
@ -45,9 +45,8 @@ import io.element.android.libraries.matrix.impl.notification.RustNotificationSer
import io.element.android.libraries.matrix.impl.pushers.RustPushersService import io.element.android.libraries.matrix.impl.pushers.RustPushersService
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
import io.element.android.libraries.matrix.impl.room.RustRoomSummaryDataSource import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
import io.element.android.libraries.matrix.impl.room.roomOrNull import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
import io.element.android.libraries.matrix.impl.room.stateFlow
import io.element.android.libraries.matrix.impl.sync.RustSyncService import io.element.android.libraries.matrix.impl.sync.RustSyncService
import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper import io.element.android.libraries.matrix.impl.usersearch.UserProfileMapper
import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper import io.element.android.libraries.matrix.impl.usersearch.UserSearchResultMapper
@ -90,11 +89,11 @@ class RustMatrixClient constructor(
) : MatrixClient { ) : MatrixClient {
override val sessionId: UserId = UserId(client.userId()) override val sessionId: UserId = UserId(client.userId())
private val roomListService = syncService.roomListService() private val innerRoomListService = syncService.roomListService()
private val sessionDispatcher = dispatchers.io.limitedParallelism(64) private val sessionDispatcher = dispatchers.io.limitedParallelism(64)
private val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-${sessionId}") private val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-${sessionId}")
private val verificationService = RustSessionVerificationService() private val verificationService = RustSessionVerificationService()
private val rustSyncService = RustSyncService(syncService, roomListService.stateFlow(), sessionCoroutineScope) private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope)
private val pushersService = RustPushersService( private val pushersService = RustPushersService(
client = client, client = client,
dispatchers = dispatchers, dispatchers = dispatchers,
@ -122,15 +121,15 @@ class RustMatrixClient constructor(
} }
} }
private val rustRoomSummaryDataSource: RustRoomSummaryDataSource = private val rustRoomListService: RoomListService =
RustRoomSummaryDataSource( RustRoomListService(
roomListService = roomListService, innerRoomListService = innerRoomListService,
sessionCoroutineScope = sessionCoroutineScope, sessionCoroutineScope = sessionCoroutineScope,
dispatcher = sessionDispatcher, dispatcher = sessionDispatcher,
) )
override val roomSummaryDataSource: RoomSummaryDataSource override val roomListService: RoomListService
get() = rustRoomSummaryDataSource get() = rustRoomListService
private val rustMediaLoader = RustMediaLoader(baseCacheDirectory, dispatchers, client) private val rustMediaLoader = RustMediaLoader(baseCacheDirectory, dispatchers, client)
override val mediaLoader: MatrixMediaLoader override val mediaLoader: MatrixMediaLoader
@ -138,7 +137,7 @@ class RustMatrixClient constructor(
private val roomMembershipObserver = RoomMembershipObserver() private val roomMembershipObserver = RoomMembershipObserver()
private val roomContentForwarder = RoomContentForwarder(roomListService) private val roomContentForwarder = RoomContentForwarder(innerRoomListService)
init { init {
client.setDelegate(clientDelegate) client.setDelegate(clientDelegate)
@ -156,7 +155,7 @@ class RustMatrixClient constructor(
var cachedPairOfRoom = pairOfRoom(roomId) var cachedPairOfRoom = pairOfRoom(roomId)
if (cachedPairOfRoom == null) { if (cachedPairOfRoom == null) {
//... otherwise, lets wait for the SS to load all rooms and check again. //... otherwise, lets wait for the SS to load all rooms and check again.
roomSummaryDataSource.awaitAllRoomsAreLoaded() roomListService.allRooms().awaitLoaded()
cachedPairOfRoom = pairOfRoom(roomId) cachedPairOfRoom = pairOfRoom(roomId)
} }
cachedPairOfRoom?.let { (roomListItem, fullRoom) -> cachedPairOfRoom?.let { (roomListItem, fullRoom) ->
@ -174,7 +173,7 @@ class RustMatrixClient constructor(
} }
private fun pairOfRoom(roomId: RoomId): Pair<RoomListItem, Room>? { private fun pairOfRoom(roomId: RoomId): Pair<RoomListItem, Room>? {
val cachedRoomListItem = roomListService.roomOrNull(roomId.value) val cachedRoomListItem = innerRoomListService.roomOrNull(roomId.value)
val fullRoom = cachedRoomListItem?.fullRoom() val fullRoom = cachedRoomListItem?.fullRoom()
return if (cachedRoomListItem == null || fullRoom == null) { return if (cachedRoomListItem == null || fullRoom == null) {
Timber.d("No room cached for $roomId") Timber.d("No room cached for $roomId")
@ -225,7 +224,7 @@ class RustMatrixClient constructor(
// Wait to receive the room back from the sync // Wait to receive the room back from the sync
withTimeout(30_000L) { withTimeout(30_000L) {
roomSummaryDataSource.allRooms() roomListService.allRooms().summaries
.filter { roomSummaries -> .filter { roomSummaries ->
roomSummaries.map { it.identifier() }.contains(roomId.value) roomSummaries.map { it.identifier() }.contains(roomId.value)
} }
@ -273,7 +272,7 @@ class RustMatrixClient constructor(
client.setDelegate(null) client.setDelegate(null)
verificationService.destroy() verificationService.destroy()
syncService.destroy() syncService.destroy()
roomListService.destroy() innerRoomListService.destroy()
notificationClient.destroy() notificationClient.destroy()
client.destroy() client.destroy()
} }

View file

@ -23,7 +23,7 @@ import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@Module @Module
@ -40,8 +40,8 @@ object SessionMatrixModule {
} }
@Provides @Provides
fun provideRoomSummaryDataSource(matrixClient: MatrixClient): RoomSummaryDataSource { fun providesRoomListService(matrixClient: MatrixClient): RoomListService {
return matrixClient.roomSummaryDataSource return matrixClient.roomListService
} }
@Provides @Provides

View file

@ -20,6 +20,7 @@ import io.element.android.libraries.core.coroutine.parallelMap
import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.ForwardEventException import io.element.android.libraries.matrix.api.room.ForwardEventException
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.Room

View file

@ -1,123 +0,0 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.matrix.api.room.RoomSummary
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.matrix.rustcomponents.sdk.RoomList
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListException
import org.matrix.rustcomponents.sdk.RoomListInput
import org.matrix.rustcomponents.sdk.RoomListLoadingState
import org.matrix.rustcomponents.sdk.RoomListRange
import org.matrix.rustcomponents.sdk.RoomListService
import org.matrix.rustcomponents.sdk.RoomListServiceState
import timber.log.Timber
internal class RustRoomSummaryDataSource(
private val roomListService: RoomListService,
private val sessionCoroutineScope: CoroutineScope,
dispatcher: CoroutineDispatcher,
roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
) : RoomSummaryDataSource {
private val allRooms = MutableStateFlow<List<RoomSummary>>(emptyList())
private val inviteRooms = MutableStateFlow<List<RoomSummary>>(emptyList())
private val allRoomsLoadingState: MutableStateFlow<RoomSummaryDataSource.LoadingState> = MutableStateFlow(RoomSummaryDataSource.LoadingState.NotLoaded)
private val allRoomsListProcessor = RoomSummaryListProcessor(allRooms, roomListService, roomSummaryDetailsFactory, shouldFetchFullRoom = false)
private val inviteRoomsListProcessor = RoomSummaryListProcessor(inviteRooms, roomListService, roomSummaryDetailsFactory, shouldFetchFullRoom = true)
init {
sessionCoroutineScope.launch(dispatcher) {
val allRooms = roomListService.allRooms()
allRooms
.observeEntriesWithProcessor(allRoomsListProcessor)
.launchIn(this)
allRooms
.loadingStateFlow()
.map { it.toRoomSummaryDataSourceLoadingState() }
.onEach {
allRoomsLoadingState.value = it
}
.launchIn(this)
launch {
// Wait until running, as invites is only available after that
roomListService.stateFlow().first {
it == RoomListServiceState.RUNNING
}
roomListService.invites()
.observeEntriesWithProcessor(inviteRoomsListProcessor)
.launchIn(this)
}
}
}
override fun allRooms(): StateFlow<List<RoomSummary>> {
return allRooms
}
override fun inviteRooms(): StateFlow<List<RoomSummary>> {
return inviteRooms
}
override fun allRoomsLoadingState(): StateFlow<RoomSummaryDataSource.LoadingState> {
return allRoomsLoadingState
}
override fun updateAllRoomsVisibleRange(range: IntRange) {
Timber.v("setVisibleRange=$range")
sessionCoroutineScope.launch {
try {
val ranges = listOf(RoomListRange(range.first.toUInt(), range.last.toUInt()))
roomListService.applyInput(
RoomListInput.Viewport(ranges)
)
} catch (exception: RoomListException) {
Timber.e(exception, "Failed updating visible range")
}
}
}
}
private fun RoomListLoadingState.toRoomSummaryDataSourceLoadingState(): RoomSummaryDataSource.LoadingState {
return when (this) {
is RoomListLoadingState.Loaded -> RoomSummaryDataSource.LoadingState.Loaded(maximumNumberOfRooms?.toInt() ?: 0)
is RoomListLoadingState.NotLoaded -> RoomSummaryDataSource.LoadingState.NotLoaded
}
}
private fun RoomList.observeEntriesWithProcessor(processor: RoomSummaryListProcessor): Flow<List<RoomListEntriesUpdate>> {
return entriesFlow { roomListEntries ->
processor.postEntries(roomListEntries)
}.onEach { update ->
processor.postUpdate(update)
}
}

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.libraries.matrix.impl.room package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.impl.util.mxCallbackFlow import io.element.android.libraries.matrix.impl.util.mxCallbackFlow

View file

@ -14,10 +14,11 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.libraries.matrix.impl.room package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.matrix.impl.room.RoomMemberMapper
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem import org.matrix.rustcomponents.sdk.RoomListItem

View file

@ -14,10 +14,10 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.libraries.matrix.impl.room package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.core.coroutine.parallelMap import io.element.android.libraries.core.coroutine.parallelMap
import io.element.android.libraries.matrix.api.room.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.flow.StateFlow
/**
* Simple implementation of [RoomList] where state flows are provided through constructor.
*/
class RustRoomList(
override val summaries: StateFlow<List<RoomSummary>>,
override val loadingState: StateFlow<RoomList.LoadingState>
) : RoomList

View file

@ -0,0 +1,151 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.roomlist
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import org.matrix.rustcomponents.sdk.RoomListEntriesUpdate
import org.matrix.rustcomponents.sdk.RoomListException
import org.matrix.rustcomponents.sdk.RoomListInput
import org.matrix.rustcomponents.sdk.RoomListLoadingState
import org.matrix.rustcomponents.sdk.RoomListRange
import org.matrix.rustcomponents.sdk.RoomListServiceState
import timber.log.Timber
import org.matrix.rustcomponents.sdk.RoomListService as InnerRustRoomListService
class RustRoomListService(
private val innerRoomListService: InnerRustRoomListService,
private val sessionCoroutineScope: CoroutineScope,
dispatcher: CoroutineDispatcher,
roomSummaryDetailsFactory: RoomSummaryDetailsFactory = RoomSummaryDetailsFactory(),
) : RoomListService {
private val allRooms = MutableStateFlow<List<RoomSummary>>(emptyList())
private val inviteRooms = MutableStateFlow<List<RoomSummary>>(emptyList())
private val allRoomsLoadingState: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
private val allRoomsListProcessor = RoomSummaryListProcessor(allRooms, innerRoomListService, roomSummaryDetailsFactory, shouldFetchFullRoom = false)
private val invitesLoadingState: MutableStateFlow<RoomList.LoadingState> = MutableStateFlow(RoomList.LoadingState.NotLoaded)
private val inviteRoomsListProcessor = RoomSummaryListProcessor(inviteRooms, innerRoomListService, roomSummaryDetailsFactory, shouldFetchFullRoom = true)
init {
sessionCoroutineScope.launch(dispatcher) {
val allRooms = innerRoomListService.allRooms()
allRooms
.observeEntriesWithProcessor(allRoomsListProcessor)
.launchIn(this)
allRooms
.observeLoadingState(allRoomsLoadingState)
.launchIn(this)
launch {
// Wait until running, as invites is only available after that
innerRoomListService.stateFlow().first {
it == RoomListServiceState.RUNNING
}
val invites = innerRoomListService.invites()
invites
.observeEntriesWithProcessor(inviteRoomsListProcessor)
.launchIn(this)
invites
.observeLoadingState(invitesLoadingState)
.launchIn(this)
}
}
}
override fun allRooms(): RoomList {
return RustRoomList(allRooms, allRoomsLoadingState)
}
override fun invites(): RoomList {
return RustRoomList(inviteRooms, invitesLoadingState)
}
override fun updateAllRoomsVisibleRange(range: IntRange) {
Timber.v("setVisibleRange=$range")
sessionCoroutineScope.launch {
try {
val ranges = listOf(RoomListRange(range.first.toUInt(), range.last.toUInt()))
innerRoomListService.applyInput(
RoomListInput.Viewport(ranges)
)
} catch (exception: RoomListException) {
Timber.e(exception, "Failed updating visible range")
}
}
}
override val state: StateFlow<RoomListService.State> =
innerRoomListService.stateFlow()
.map { it.toRoomListState() }
.onEach { state ->
Timber.d("RoomList state=$state")
}
.distinctUntilChanged()
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, RoomListService.State.Idle)
}
private fun RoomListLoadingState.toLoadingState(): RoomList.LoadingState {
return when (this) {
is RoomListLoadingState.Loaded -> RoomList.LoadingState.Loaded(maximumNumberOfRooms?.toInt() ?: 0)
RoomListLoadingState.NotLoaded -> RoomList.LoadingState.NotLoaded
}
}
private fun RoomListServiceState.toRoomListState(): RoomListService.State {
return when (this) {
RoomListServiceState.INIT,
RoomListServiceState.SETTING_UP -> RoomListService.State.Idle
RoomListServiceState.RUNNING -> RoomListService.State.Running
RoomListServiceState.ERROR -> RoomListService.State.Error
RoomListServiceState.TERMINATED -> RoomListService.State.Terminated
}
}
private fun org.matrix.rustcomponents.sdk.RoomList.observeEntriesWithProcessor(processor: RoomSummaryListProcessor): Flow<List<RoomListEntriesUpdate>> {
return entriesFlow { roomListEntries ->
processor.postEntries(roomListEntries)
}.onEach { update ->
processor.postUpdate(update)
}
}
private fun org.matrix.rustcomponents.sdk.RoomList.observeLoadingState(stateFlow: MutableStateFlow<RoomList.LoadingState>): Flow<RoomList.LoadingState> {
return loadingStateFlow()
.map { it.toLoadingState() }
.onEach {
stateFlow.value = it
}
}

View file

@ -17,19 +17,8 @@
package io.element.android.libraries.matrix.impl.sync package io.element.android.libraries.matrix.impl.sync
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState
import org.matrix.rustcomponents.sdk.RoomListServiceState
import org.matrix.rustcomponents.sdk.SyncServiceState import org.matrix.rustcomponents.sdk.SyncServiceState
internal fun RoomListServiceState.toSyncState(): SyncState {
return when (this) {
RoomListServiceState.INIT,
RoomListServiceState.SETTING_UP -> SyncState.Idle
RoomListServiceState.RUNNING -> SyncState.Running
RoomListServiceState.ERROR -> SyncState.Error
RoomListServiceState.TERMINATED -> SyncState.Terminated
}
}
internal fun SyncServiceState.toSyncState(): SyncState { internal fun SyncServiceState.toSyncState(): SyncState {
return when (this) { return when (this) {
SyncServiceState.IDLE -> SyncState.Idle SyncServiceState.IDLE -> SyncState.Idle

View file

@ -19,36 +19,38 @@ package io.element.android.libraries.matrix.impl.sync
import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.api.sync.SyncState
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import org.matrix.rustcomponents.sdk.RoomListServiceState
import org.matrix.rustcomponents.sdk.SyncServiceInterface import org.matrix.rustcomponents.sdk.SyncServiceInterface
import org.matrix.rustcomponents.sdk.SyncServiceState
import timber.log.Timber import timber.log.Timber
class RustSyncService( class RustSyncService(
private val innerSyncService: SyncServiceInterface, private val innerSyncService: SyncServiceInterface,
roomListStateFlow: Flow<RoomListServiceState>,
sessionCoroutineScope: CoroutineScope sessionCoroutineScope: CoroutineScope
) : SyncService { ) : SyncService {
override suspend fun startSync() = runCatching { override suspend fun startSync() = runCatching {
Timber.i("Start sync") Timber.i("Start sync")
innerSyncService.start() innerSyncService.start()
}.onFailure {
Timber.d("Start sync failed: $it")
} }
override fun stopSync() = runCatching { override fun stopSync() = runCatching {
Timber.i("Stop sync") Timber.i("Stop sync")
innerSyncService.pause() innerSyncService.pause()
}.onFailure {
Timber.d("Stop sync failed: $it")
} }
override val syncState: StateFlow<SyncState> = override val syncState: StateFlow<SyncState> =
roomListStateFlow innerSyncService.stateFlow()
.map(RoomListServiceState::toSyncState) .map(SyncServiceState::toSyncState)
.onEach { state -> .onEach { state ->
Timber.i("Sync state=$state") Timber.i("Sync state=$state")
} }

View file

@ -22,11 +22,11 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.buffer import kotlinx.coroutines.flow.buffer
import org.matrix.rustcomponents.sdk.SyncService import org.matrix.rustcomponents.sdk.SyncServiceInterface
import org.matrix.rustcomponents.sdk.SyncServiceState import org.matrix.rustcomponents.sdk.SyncServiceState
import org.matrix.rustcomponents.sdk.SyncServiceStateObserver import org.matrix.rustcomponents.sdk.SyncServiceStateObserver
fun SyncService.stateFlow(): Flow<SyncServiceState> = fun SyncServiceInterface.stateFlow(): Flow<SyncServiceState> =
mxCallbackFlow { mxCallbackFlow {
val listener = object : SyncServiceStateObserver { val listener = object : SyncServiceStateObserver {
override fun onUpdate(state: SyncServiceState) { override fun onUpdate(state: SyncServiceState) {

View file

@ -27,7 +27,7 @@ import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.PushersService
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.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@ -35,7 +35,7 @@ import io.element.android.libraries.matrix.test.media.FakeMediaLoader
import io.element.android.libraries.matrix.test.notification.FakeNotificationService import io.element.android.libraries.matrix.test.notification.FakeNotificationService
import io.element.android.libraries.matrix.test.pushers.FakePushersService import io.element.android.libraries.matrix.test.pushers.FakePushersService
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.sync.FakeSyncService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.tests.testutils.simulateLongTask import io.element.android.tests.testutils.simulateLongTask
@ -45,7 +45,7 @@ class FakeMatrixClient(
override val sessionId: SessionId = A_SESSION_ID, override val sessionId: SessionId = A_SESSION_ID,
private val userDisplayName: Result<String> = Result.success(A_USER_NAME), private val userDisplayName: Result<String> = Result.success(A_USER_NAME),
private val userAvatarURLString: Result<String> = Result.success(AN_AVATAR_URL), private val userAvatarURLString: Result<String> = Result.success(AN_AVATAR_URL),
override val roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(), override val roomListService: RoomListService = FakeRoomListService(),
override val mediaLoader: MatrixMediaLoader = FakeMediaLoader(), override val mediaLoader: MatrixMediaLoader = FakeMediaLoader(),
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(), private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
private val pushersService: FakePushersService = FakePushersService(), private val pushersService: FakePushersService = FakePushersService(),

View file

@ -20,8 +20,8 @@ import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.core.TransactionId import io.element.android.libraries.matrix.api.core.TransactionId
import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.matrix.api.room.message.RoomMessage import io.element.android.libraries.matrix.api.room.message.RoomMessage
import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent import io.element.android.libraries.matrix.api.timeline.item.event.EventContent

View file

@ -14,18 +14,21 @@
* limitations under the License. * limitations under the License.
*/ */
package io.element.android.libraries.matrix.test.room package io.element.android.libraries.matrix.test.roomlist
import io.element.android.libraries.matrix.api.room.RoomSummary import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
class FakeRoomSummaryDataSource : RoomSummaryDataSource { class FakeRoomListService : RoomListService {
private val allRoomSummariesFlow = MutableStateFlow<List<RoomSummary>>(emptyList()) private val allRoomSummariesFlow = MutableStateFlow<List<RoomSummary>>(emptyList())
private val inviteRoomSummariesFlow = MutableStateFlow<List<RoomSummary>>(emptyList()) private val inviteRoomSummariesFlow = MutableStateFlow<List<RoomSummary>>(emptyList())
private val allRoomsLoadingStateFlow = MutableStateFlow<RoomSummaryDataSource.LoadingState>(RoomSummaryDataSource.LoadingState.NotLoaded) private val allRoomsLoadingStateFlow = MutableStateFlow<RoomList.LoadingState>(RoomList.LoadingState.NotLoaded)
private val inviteRoomsLoadingStateFlow = MutableStateFlow<RoomList.LoadingState>(RoomList.LoadingState.NotLoaded)
private val roomListStateFlow = MutableStateFlow<RoomListService.State>(RoomListService.State.Idle)
suspend fun postAllRooms(roomSummaries: List<RoomSummary>) { suspend fun postAllRooms(roomSummaries: List<RoomSummary>) {
allRoomSummariesFlow.emit(roomSummaries) allRoomSummariesFlow.emit(roomSummaries)
@ -35,20 +38,16 @@ class FakeRoomSummaryDataSource : RoomSummaryDataSource {
inviteRoomSummariesFlow.emit(roomSummaries) inviteRoomSummariesFlow.emit(roomSummaries)
} }
suspend fun postLoadingState(loadingState: RoomSummaryDataSource.LoadingState) { suspend fun postAllRoomsLoadingState(loadingState: RoomList.LoadingState) {
allRoomsLoadingStateFlow.emit(loadingState) allRoomsLoadingStateFlow.emit(loadingState)
} }
override fun allRoomsLoadingState(): StateFlow<RoomSummaryDataSource.LoadingState> { suspend fun postInviteRoomsLoadingState(loadingState: RoomList.LoadingState) {
return allRoomsLoadingStateFlow inviteRoomsLoadingStateFlow.emit(loadingState)
} }
override fun allRooms(): StateFlow<List<RoomSummary>> { suspend fun postState(state: RoomListService.State) {
return allRoomSummariesFlow roomListStateFlow.emit(state)
}
override fun inviteRooms(): StateFlow<List<RoomSummary>> {
return inviteRoomSummariesFlow
} }
var latestSlidingSyncRange: IntRange? = null var latestSlidingSyncRange: IntRange? = null
@ -57,4 +56,20 @@ class FakeRoomSummaryDataSource : RoomSummaryDataSource {
override fun updateAllRoomsVisibleRange(range: IntRange) { override fun updateAllRoomsVisibleRange(range: IntRange) {
latestSlidingSyncRange = range latestSlidingSyncRange = range
} }
override fun allRooms(): RoomList {
return SimpleRoomList(
summaries = allRoomSummariesFlow,
loadingState = allRoomsLoadingStateFlow
)
}
override fun invites(): RoomList {
return SimpleRoomList(
summaries = inviteRoomSummariesFlow,
loadingState = inviteRoomsLoadingStateFlow
)
}
override val state: StateFlow<RoomListService.State> = roomListStateFlow
} }

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.test.roomlist
import io.element.android.libraries.matrix.api.roomlist.RoomList
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.flow.StateFlow
data class SimpleRoomList(
override val summaries: StateFlow<List<RoomSummary>>,
override val loadingState: StateFlow<RoomList.LoadingState>
) : RoomList

View file

@ -46,7 +46,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
@Composable @Composable

View file

@ -68,7 +68,7 @@ class RoomListScreen(
inviteStateDataSource = DefaultInviteStateDataSource(matrixClient, DefaultSeenInvitesStore(context), coroutineDispatchers), inviteStateDataSource = DefaultInviteStateDataSource(matrixClient, DefaultSeenInvitesStore(context), coroutineDispatchers),
leaveRoomPresenter = LeaveRoomPresenterImpl(matrixClient, RoomMembershipObserver(), coroutineDispatchers), leaveRoomPresenter = LeaveRoomPresenterImpl(matrixClient, RoomMembershipObserver(), coroutineDispatchers),
roomListDataSource = RoomListDataSource( roomListDataSource = RoomListDataSource(
roomSummaryDataSource = matrixClient.roomSummaryDataSource, roomListService = matrixClient.roomListService,
lastMessageTimestampFormatter = DefaultLastMessageTimestampFormatter(dateTimeProvider, dateFormatters), lastMessageTimestampFormatter = DefaultLastMessageTimestampFormatter(dateTimeProvider, dateFormatters),
roomLastMessageFormatter = DefaultRoomLastMessageFormatter( roomLastMessageFormatter = DefaultRoomLastMessageFormatter(
sp = stringProvider, sp = stringProvider,