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.turbine)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.features.networkmonitor.test)
testImplementation(projects.tests.testutils)
testImplementation(projects.features.rageshake.test)
testImplementation(projects.features.rageshake.impl)
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.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import timber.log.Timber
@ContributesNode(AppScope::class)
class LoggedInFlowNode @AssistedInject constructor(
@@ -123,7 +126,6 @@ class LoggedInFlowNode @AssistedInject constructor(
override fun onBuilt() {
super.onBuilt()
lifecycle.subscribe(
onCreate = {
plugins<LifecycleCallback>().forEach { it.onFlowCreated(id, inputs.matrixClient) }
@@ -138,12 +140,8 @@ class LoggedInFlowNode @AssistedInject constructor(
backstack.push(NavTarget.Ftue)
}
},
onStart = {
lifecycleScope.launch {
syncService.startSync()
}
},
onStop = {
//Counterpart startSync is done in observeSyncStateAndNetworkStatus method.
syncService.stopSync()
},
onDestroy = {
@@ -153,22 +151,24 @@ class LoggedInFlowNode @AssistedInject constructor(
loggedInFlowProcessor.stopObserving()
}
)
observeSyncStateAndNetworkStatus()
}
@OptIn(FlowPreview::class)
private fun observeSyncStateAndNetworkStatus() {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
repeatOnLifecycle(Lifecycle.State.STARTED) {
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
) { syncState, networkStatus ->
syncState == SyncState.Error && networkStatus == NetworkStatus.Online
Pair(syncState, networkStatus)
}
.distinctUntilChanged()
.collect { restartSync ->
if (restartSync) {
.collect { (syncState, networkStatus) ->
Timber.d("Sync state: $syncState, network status: $networkStatus")
if (syncState != SyncState.Running && networkStatus == NetworkStatus.Online) {
syncService.startSync()
}
}
@@ -351,3 +351,4 @@ class LoggedInFlowNode @AssistedInject constructor(
backstack.push(NavTarget.InviteList)
}
}

View File

@@ -21,16 +21,27 @@ import android.os.Build
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.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.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.noop.NoopPermissionsPresenter
import io.element.android.libraries.push.api.PushService
import kotlinx.coroutines.delay
import javax.inject.Inject
private const val DELAY_BEFORE_SHOWING_SYNC_SPINNER_IN_MILLIS = 1500L
class LoggedInPresenter @Inject constructor(
private val matrixClient: MatrixClient,
private val permissionsPresenterFactory: PermissionsPresenter.Factory,
private val networkMonitor: NetworkMonitor,
private val pushService: PushService,
) : Presenter<LoggedInState> {
@@ -53,18 +64,25 @@ class LoggedInPresenter @Inject constructor(
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()
// fun handleEvents(event: LoggedInEvents) {
// when (event) {
// }
// }
var showSyncSpinner by remember {
mutableStateOf(false)
}
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(
syncState = syncState.value,
showSyncSpinner = showSyncSpinner,
permissionsState = permissionsState,
// eventSink = ::handleEvents
)
}
}

View File

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

View File

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

View File

@@ -47,7 +47,7 @@ fun LoggedInView(
modifier = Modifier
.padding(top = 8.dp)
.align(Alignment.TopCenter),
syncState = state.syncState,
isVisible = state.showSyncSpinner,
)
PermissionsView(
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.Surface
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.ui.strings.CommonStrings
@Composable
fun SyncStateView(
syncState: SyncState,
isVisible: Boolean,
modifier: Modifier = Modifier
) {
val animationSpec = spring<Float>(stiffness = 500F)
AnimatedVisibility(
modifier = modifier,
visible = syncState.mustBeVisible(),
visible = isVisible,
enter = fadeIn(animationSpec = animationSpec),
exit = fadeOut(animationSpec = animationSpec),
) {
@@ -60,15 +59,15 @@ fun SyncStateView(
) {
Row(
modifier = Modifier
.background(color = ElementTheme.colors.bgSubtleSecondary)
.padding(horizontal = 24.dp, vertical = 10.dp),
.background(color = ElementTheme.colors.bgSubtleSecondary)
.padding(horizontal = 24.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
CircularProgressIndicator(
modifier = Modifier
.progressSemantics()
.size(12.dp),
.progressSemantics()
.size(12.dp),
color = ElementTheme.colors.textPrimary,
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
@Composable
internal fun SyncStateViewPreview() = ElementPreview {
// Add a box to see the shadow
Box(modifier = Modifier.padding(24.dp)) {
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.turbine.test
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.roomlist.RoomListService
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.noop.NoopPermissionsPresenter
import io.element.android.libraries.push.api.PushService
import io.element.android.libraries.pushproviders.api.Distributor
import io.element.android.libraries.pushproviders.api.PushProvider
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import kotlinx.coroutines.test.runTest
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(
matrixClient = FakeMatrixClient(),
matrixClient = FakeMatrixClient(roomListService = roomListService),
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
override fun create(permission: String): PermissionsPresenter {
return NoopPermissionsPresenter()
}
},
networkMonitor = FakeNetworkMonitor(networkStatus),
pushService = object : PushService {
override fun notificationStyleChanged() {
}

View File

@@ -18,12 +18,12 @@ package io.element.android.appnav.room
import app.cash.turbine.test
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_SESSION_ID
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.FakeRoomSummaryDataSource
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -47,29 +47,29 @@ class LoadingRoomStateFlowFactoryTest {
@Test
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 roomSummaryDataSource = FakeRoomSummaryDataSource()
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomSummaryDataSource = roomSummaryDataSource)
val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
flowFactory
.create(this, A_ROOM_ID)
.test {
Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
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))
}
}
@Test
fun `flow should emit Loading and then Error when there is no room in cache after SS is loaded`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomSummaryDataSource = roomSummaryDataSource)
val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient(A_SESSION_ID, roomListService = roomListService)
val flowFactory = LoadingRoomStateFlowFactory(matrixClient)
flowFactory
.create(this, A_ROOM_ID)
.test {
Truth.assertThat(awaitItem()).isEqualTo(LoadingRoomState.Loading)
roomSummaryDataSource.postLoadingState(RoomSummaryDataSource.LoadingState.Loaded(1))
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
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.matrix.api.MatrixClient
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.services.analytics.api.AnalyticsService
import io.element.android.services.analytics.api.extensions.toAnalyticsJoinedRoom
@@ -56,8 +56,9 @@ class InviteListPresenter @Inject constructor(
@Composable
override fun present(): InviteListState {
val invites by client
.roomSummaryDataSource
.inviteRooms()
.roomListService
.invites()
.summaries
.collectAsState()
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.room.RoomMember
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.room.RoomSummaryDetails
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
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.A_ROOM_ID
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.FakeMatrixClient
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.test.notifications.FakeNotificationDrawerManager
import io.element.android.services.analytics.api.AnalyticsService
@@ -51,9 +51,9 @@ class InviteListPresenterTests {
@Test
fun `present - starts empty, adds invites when received`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val roomListService = FakeRoomListService()
val presenter = createPresenter(
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
FakeMatrixClient(roomListService = roomListService)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -61,7 +61,7 @@ class InviteListPresenterTests {
val initialState = awaitItem()
Truth.assertThat(initialState.inviteList).isEmpty()
roomSummaryDataSource.postInviteRooms(listOf(aRoomSummary()))
roomListService.postInviteRooms(listOf(aRoomSummary()))
val withInviteState = awaitItem()
Truth.assertThat(withInviteState.inviteList.size).isEqualTo(1)
@@ -72,9 +72,9 @@ class InviteListPresenterTests {
@Test
fun `present - uses user ID and avatar for direct invites`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withDirectChatInvitation()
val roomListService = FakeRoomListService().withDirectChatInvitation()
val presenter = createPresenter(
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
FakeMatrixClient(roomListService = roomListService)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -98,9 +98,9 @@ class InviteListPresenterTests {
@Test
fun `present - includes sender details for room invites`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val roomListService = FakeRoomListService().withRoomInvitation()
val presenter = createPresenter(
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
FakeMatrixClient(roomListService = roomListService)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -122,10 +122,10 @@ class InviteListPresenterTests {
@Test
fun `present - shows confirm dialog for declining direct chat invites`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withDirectChatInvitation()
val roomListService = FakeRoomListService().withDirectChatInvitation()
val presenter = InviteListPresenter(
FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource,
roomListService = roomListService,
),
FakeSeenInvitesStore(),
FakeAnalyticsService(),
@@ -148,9 +148,9 @@ class InviteListPresenterTests {
@Test
fun `present - shows confirm dialog for declining room invites`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val roomListService = FakeRoomListService().withRoomInvitation()
val presenter = createPresenter(
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
FakeMatrixClient(roomListService = roomListService)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -169,9 +169,9 @@ class InviteListPresenterTests {
@Test
fun `present - hides confirm dialog when cancelling`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val roomListService = FakeRoomListService().withRoomInvitation()
val presenter = createPresenter(
FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
FakeMatrixClient(roomListService = roomListService)
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -190,10 +190,10 @@ class InviteListPresenterTests {
@Test
fun `present - declines invite after confirming`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val roomListService = FakeRoomListService().withRoomInvitation()
val fakeNotificationDrawerManager = FakeNotificationDrawerManager()
val client = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource,
roomListService = roomListService,
)
val room = FakeMatrixRoom()
val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager)
@@ -217,9 +217,9 @@ class InviteListPresenterTests {
@Test
fun `present - declines invite after confirming and sets state on error`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val roomListService = FakeRoomListService().withRoomInvitation()
val client = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource,
roomListService = roomListService,
)
val room = FakeMatrixRoom()
val presenter = createPresenter(client)
@@ -247,9 +247,9 @@ class InviteListPresenterTests {
@Test
fun `present - dismisses declining error state`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val roomListService = FakeRoomListService().withRoomInvitation()
val client = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource,
roomListService = roomListService,
)
val room = FakeMatrixRoom()
val presenter = createPresenter(client)
@@ -279,10 +279,10 @@ class InviteListPresenterTests {
@Test
fun `present - accepts invites and sets state on success`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val roomListService = FakeRoomListService().withRoomInvitation()
val fakeNotificationDrawerManager = FakeNotificationDrawerManager()
val client = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource,
roomListService = roomListService,
)
val room = FakeMatrixRoom()
val presenter = createPresenter(client = client, notificationDrawerManager = fakeNotificationDrawerManager)
@@ -303,9 +303,9 @@ class InviteListPresenterTests {
@Test
fun `present - accepts invites and sets state on error`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val roomListService = FakeRoomListService().withRoomInvitation()
val client = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource,
roomListService = roomListService,
)
val room = FakeMatrixRoom()
val presenter = createPresenter(client)
@@ -325,9 +325,9 @@ class InviteListPresenterTests {
@Test
fun `present - dismisses accepting error state`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().withRoomInvitation()
val roomListService = FakeRoomListService().withRoomInvitation()
val client = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource,
roomListService = roomListService,
)
val room = FakeMatrixRoom()
val presenter = createPresenter(client)
@@ -352,11 +352,11 @@ class InviteListPresenterTests {
@Test
fun `present - stores seen invites when received`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val roomListService = FakeRoomListService()
val store = FakeSeenInvitesStore()
val presenter = InviteListPresenter(
FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource,
roomListService = roomListService,
),
store,
FakeAnalyticsService(),
@@ -368,19 +368,19 @@ class InviteListPresenterTests {
awaitItem()
// When one invite is received, that ID is saved
roomSummaryDataSource.postInviteRooms(listOf(aRoomSummary()))
roomListService.postInviteRooms(listOf(aRoomSummary()))
awaitItem()
Truth.assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID))
// 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()
Truth.assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID, A_ROOM_ID_2))
// When they're both dismissed, an empty set is saved
roomSummaryDataSource.postInviteRooms(listOf())
roomListService.postInviteRooms(listOf())
awaitItem()
Truth.assertThat(store.getProvidedRoomIds()).isEmpty()
@@ -389,12 +389,12 @@ class InviteListPresenterTests {
@Test
fun `present - marks invite as new if they're unseen`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val roomListService = FakeRoomListService()
val store = FakeSeenInvitesStore()
store.publishRoomIds(setOf(A_ROOM_ID))
val presenter = InviteListPresenter(
FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource,
roomListService = roomListService,
),
store,
FakeAnalyticsService(),
@@ -405,7 +405,7 @@ class InviteListPresenterTests {
}.test {
awaitItem()
roomSummaryDataSource.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
roomListService.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
skipItems(1)
val withInviteState = awaitItem()
@@ -417,7 +417,7 @@ class InviteListPresenterTests {
}
}
private suspend fun FakeRoomSummaryDataSource.withRoomInvitation(): FakeRoomSummaryDataSource {
private suspend fun FakeRoomListService.withRoomInvitation(): FakeRoomListService {
postInviteRooms(
listOf(
RoomSummary.Filled(
@@ -446,7 +446,7 @@ class InviteListPresenterTests {
return this
}
private suspend fun FakeRoomSummaryDataSource.withDirectChatInvitation(): FakeRoomSummaryDataSource {
private suspend fun FakeRoomListService.withDirectChatInvitation(): FakeRoomListService {
postInviteRooms(
listOf(
RoomSummary.Filled(

View File

@@ -16,7 +16,7 @@
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 {
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.RoomId
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.room.RoomSummaryDetails
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
@@ -65,7 +65,7 @@ class ForwardMessagesPresenter @AssistedInject constructor(
var results: SearchBarResultState<ImmutableList<RoomSummaryDetails>> by remember { mutableStateOf(SearchBarResultState.NotSearching()) }
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) {
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.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
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.matrix.api.core.RoomId
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 kotlinx.collections.immutable.ImmutableList
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.roomListRoomName
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.theme.ElementTheme
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.libraries.designsystem.theme.components.SearchBarResultState
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.FakeMatrixClient
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.roomlist.FakeRoomListService
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.test.runTest
@@ -76,10 +76,10 @@ class ForwardMessagesPresenterTests {
@Test
fun `present - update query`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource().apply {
val roomListService = FakeRoomListService().apply {
postAllRooms(listOf(RoomSummary.Filled(aRoomSummaryDetail())))
}
val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
val client = FakeMatrixClient(roomListService = roomListService)
val presenter = aPresenter(client = client)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@@ -166,11 +166,10 @@ class ForwardMessagesPresenterTests {
}
}
private fun CoroutineScope.aPresenter(
private fun CoroutineScope.aPresenter(
eventId: EventId = AN_EVENT_ID,
fakeMatrixRoom: FakeMatrixRoom = FakeMatrixRoom(),
coroutineScope: CoroutineScope = this,
client: FakeMatrixClient = FakeMatrixClient(),
) = ForwardMessagesPresenter(eventId.value, fakeMatrixRoom, coroutineScope, client)
}

View File

@@ -135,6 +135,6 @@ class RoomListPresenter @Inject constructor(
// Safe to give bigger size than room list
val extendedRangeEnd = range.last + midExtendedRangeSize
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.matrix.api.MatrixClient
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 javax.inject.Inject
@@ -44,8 +44,9 @@ class DefaultInviteStateDataSource @Inject constructor(
@Composable
override fun inviteState(): InvitesState {
val invites by client
.roomSummaryDataSource
.inviteRooms()
.roomListService
.invites()
.summaries
.collectAsState()
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.eventformatter.api.RoomLastMessageFormatter
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.room.RoomSummaryDataSource
import io.element.android.libraries.matrix.api.roomlist.RoomListService
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@@ -44,7 +44,7 @@ import kotlinx.coroutines.withContext
import javax.inject.Inject
class RoomListDataSource @Inject constructor(
private val roomSummaryDataSource: RoomSummaryDataSource,
private val roomListService: RoomListService,
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
private val roomLastMessageFormatter: RoomLastMessageFormatter,
private val coroutineDispatchers: CoroutineDispatchers,
@@ -61,8 +61,9 @@ class RoomListDataSource @Inject constructor(
}
fun launchIn(coroutineScope: CoroutineScope) {
roomSummaryDataSource
roomListService
.allRooms()
.summaries
.onEach { 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_NAME
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.roomlist.FakeRoomListService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.tests.testutils.consumeItemsUntilPredicate
import io.element.android.tests.testutils.testCoroutineDispatchers
@@ -111,9 +111,9 @@ class RoomListPresenterTests {
@Test
fun `present - load 1 room with success`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
roomListService = roomListService
)
val presenter = createRoomListPresenter(matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
@@ -123,7 +123,7 @@ class RoomListPresenterTests {
// Room list is loaded with 16 placeholders
Truth.assertThat(initialState.roomList.size).isEqualTo(16)
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()
Truth.assertThat(withRoomState.roomList.size).isEqualTo(1)
Truth.assertThat(withRoomState.roomList.first())
@@ -133,15 +133,15 @@ class RoomListPresenterTests {
@Test
fun `present - load 1 room with success and filter rooms`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
roomListService = roomListService
)
val presenter = createRoomListPresenter(matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
roomSummaryDataSource.postAllRooms(listOf(aRoomSummaryFilled()))
roomListService.postAllRooms(listOf(aRoomSummaryFilled()))
val loadedState = consumeItemsUntilPredicate { state -> state.roomList.size == 1 }.last()
// Test filtering with result
loadedState.eventSink.invoke(RoomListEvents.UpdateFilter(A_ROOM_NAME.substring(0, 3)))
@@ -160,39 +160,39 @@ class RoomListPresenterTests {
@Test
fun `present - update visible range`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
roomListService = roomListService
)
val presenter = createRoomListPresenter(matrixClient)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
roomSummaryDataSource.postAllRooms(listOf(aRoomSummaryFilled()))
roomListService.postAllRooms(listOf(aRoomSummaryFilled()))
val loadedState = awaitItem()
// check initial value
Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange).isNull()
Truth.assertThat(roomListService.latestSlidingSyncRange).isNull()
// Test empty range
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
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(0, 0)))
Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange)
Truth.assertThat(roomListService.latestSlidingSyncRange)
.isEqualTo(IntRange(0, 20))
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(0, 1)))
Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange)
Truth.assertThat(roomListService.latestSlidingSyncRange)
.isEqualTo(IntRange(0, 21))
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(19, 29)))
Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange)
Truth.assertThat(roomListService.latestSlidingSyncRange)
.isEqualTo(IntRange(0, 49))
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(49, 59)))
Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange)
Truth.assertThat(roomListService.latestSlidingSyncRange)
.isEqualTo(IntRange(29, 79))
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(149, 159)))
Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange)
Truth.assertThat(roomListService.latestSlidingSyncRange)
.isEqualTo(IntRange(129, 179))
loadedState.eventSink.invoke(RoomListEvents.UpdateVisibleRange(IntRange(149, 259)))
Truth.assertThat(roomSummaryDataSource.latestSlidingSyncRange)
Truth.assertThat(roomListService.latestSlidingSyncRange)
.isEqualTo(IntRange(129, 279))
cancelAndIgnoreRemainingEvents()
}
@@ -200,9 +200,9 @@ class RoomListPresenterTests {
@Test
fun `present - handle DismissRequestVerificationPrompt`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val roomListService = FakeRoomListService()
val matrixClient = FakeMatrixClient(
roomSummaryDataSource = roomSummaryDataSource
roomListService = roomListService
)
val presenter = createRoomListPresenter(
client = matrixClient,
@@ -317,7 +317,7 @@ class RoomListPresenterTests {
inviteStateDataSource = inviteStateDataSource,
leaveRoomPresenter = leaveRoomPresenter,
roomListDataSource = RoomListDataSource(
client.roomSummaryDataSource,
client.roomListService,
lastMessageTimestampFormatter,
roomLastMessageFormatter,
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_2
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.roomlist.FakeRoomListService
import io.element.android.tests.testutils.testCoroutineDispatchers
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -35,8 +35,8 @@ internal class DefaultInviteStateDataSourceTest {
@Test
fun `emits NoInvites state if invites list is empty`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
val roomListService = FakeRoomListService()
val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore()
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
@@ -49,9 +49,9 @@ internal class DefaultInviteStateDataSourceTest {
@Test
fun `emits NewInvites state if unseen invite exists`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
roomSummaryDataSource.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
val roomListService = FakeRoomListService()
roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore()
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
@@ -65,9 +65,9 @@ internal class DefaultInviteStateDataSourceTest {
@Test
fun `emits NewInvites state if multiple invites exist and at least one is unseen`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
roomSummaryDataSource.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2)))
val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
val roomListService = FakeRoomListService()
roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2)))
val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore()
seenStore.publishRoomIds(setOf(A_ROOM_ID))
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers(useUnconfinedTestDispatcher = true))
@@ -82,9 +82,9 @@ internal class DefaultInviteStateDataSourceTest {
@Test
fun `emits SeenInvites state if invite exists in seen store`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
roomSummaryDataSource.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
val roomListService = FakeRoomListService()
roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore()
seenStore.publishRoomIds(setOf(A_ROOM_ID))
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers(useUnconfinedTestDispatcher = true))
@@ -100,8 +100,8 @@ internal class DefaultInviteStateDataSourceTest {
@Test
fun `emits new state in response to upstream events`() = runTest {
val roomSummaryDataSource = FakeRoomSummaryDataSource()
val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource)
val roomListService = FakeRoomListService()
val client = FakeMatrixClient(roomListService = roomListService)
val seenStore = FakeSeenInvitesStore()
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
@@ -112,7 +112,7 @@ internal class DefaultInviteStateDataSourceTest {
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
// 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)
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
@@ -122,12 +122,12 @@ internal class DefaultInviteStateDataSourceTest {
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.SeenInvites)
// 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)
Truth.assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
// All of the invites going away reverts to NoInvites
roomSummaryDataSource.postInviteRooms(emptyList())
roomListService.postInviteRooms(emptyList())
skipItems(1)
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.room.MatrixRoom
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.user.MatrixSearchUserResults
import io.element.android.libraries.matrix.api.user.MatrixUser
@@ -35,7 +35,7 @@ import java.io.Closeable
interface MatrixClient : Closeable {
val sessionId: SessionId
val roomSummaryDataSource: RoomSummaryDataSource
val roomListService: RoomListService
val mediaLoader: MatrixMediaLoader
suspend fun getRoom(roomId: RoomId): MatrixRoom?
suspend fun findDM(userId: UserId): MatrixRoom?

View File

@@ -14,7 +14,7 @@
* 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.flow.StateFlow
@@ -23,25 +23,34 @@ import kotlinx.coroutines.withTimeout
import timber.log.Timber
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 {
object NotLoaded : LoadingState()
data class Loaded(val numberOfRooms: Int) : LoadingState()
}
fun updateAllRoomsVisibleRange(range: IntRange)
fun allRoomsLoadingState(): StateFlow<LoadingState>
fun allRooms(): StateFlow<List<RoomSummary>>
fun inviteRooms(): StateFlow<List<RoomSummary>>
/**
* The list of room summaries as a flow.
*/
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 {
Timber.d("awaitAllRoomsAreLoaded: wait")
withTimeout(timeout) {
allRoomsLoadingState().firstOrNull {
it is RoomSummaryDataSource.LoadingState.Loaded
loadingState.firstOrNull {
it is RoomList.LoadingState.Loaded
}
}
} 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.
*/
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.room.RoomMember
import io.element.android.libraries.matrix.api.room.message.RoomMessage
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.room.MatrixRoom
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.room.awaitAllRoomsAreLoaded
import io.element.android.libraries.matrix.api.roomlist.RoomListService
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.SyncState
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.room.RoomContentForwarder
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.room.roomOrNull
import io.element.android.libraries.matrix.impl.room.stateFlow
import io.element.android.libraries.matrix.impl.roomlist.RustRoomListService
import io.element.android.libraries.matrix.impl.roomlist.roomOrNull
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.UserSearchResultMapper
@@ -90,11 +89,11 @@ class RustMatrixClient constructor(
) : MatrixClient {
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 sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-${sessionId}")
private val verificationService = RustSessionVerificationService()
private val rustSyncService = RustSyncService(syncService, roomListService.stateFlow(), sessionCoroutineScope)
private val rustSyncService = RustSyncService(syncService, sessionCoroutineScope)
private val pushersService = RustPushersService(
client = client,
dispatchers = dispatchers,
@@ -122,15 +121,15 @@ class RustMatrixClient constructor(
}
}
private val rustRoomSummaryDataSource: RustRoomSummaryDataSource =
RustRoomSummaryDataSource(
roomListService = roomListService,
private val rustRoomListService: RoomListService =
RustRoomListService(
innerRoomListService = innerRoomListService,
sessionCoroutineScope = sessionCoroutineScope,
dispatcher = sessionDispatcher,
)
override val roomSummaryDataSource: RoomSummaryDataSource
get() = rustRoomSummaryDataSource
override val roomListService: RoomListService
get() = rustRoomListService
private val rustMediaLoader = RustMediaLoader(baseCacheDirectory, dispatchers, client)
override val mediaLoader: MatrixMediaLoader
@@ -138,7 +137,7 @@ class RustMatrixClient constructor(
private val roomMembershipObserver = RoomMembershipObserver()
private val roomContentForwarder = RoomContentForwarder(roomListService)
private val roomContentForwarder = RoomContentForwarder(innerRoomListService)
init {
client.setDelegate(clientDelegate)
@@ -156,7 +155,7 @@ class RustMatrixClient constructor(
var cachedPairOfRoom = pairOfRoom(roomId)
if (cachedPairOfRoom == null) {
//... otherwise, lets wait for the SS to load all rooms and check again.
roomSummaryDataSource.awaitAllRoomsAreLoaded()
roomListService.allRooms().awaitLoaded()
cachedPairOfRoom = pairOfRoom(roomId)
}
cachedPairOfRoom?.let { (roomListItem, fullRoom) ->
@@ -174,7 +173,7 @@ class RustMatrixClient constructor(
}
private fun pairOfRoom(roomId: RoomId): Pair<RoomListItem, Room>? {
val cachedRoomListItem = roomListService.roomOrNull(roomId.value)
val cachedRoomListItem = innerRoomListService.roomOrNull(roomId.value)
val fullRoom = cachedRoomListItem?.fullRoom()
return if (cachedRoomListItem == null || fullRoom == null) {
Timber.d("No room cached for $roomId")
@@ -225,7 +224,7 @@ class RustMatrixClient constructor(
// Wait to receive the room back from the sync
withTimeout(30_000L) {
roomSummaryDataSource.allRooms()
roomListService.allRooms().summaries
.filter { roomSummaries ->
roomSummaries.map { it.identifier() }.contains(roomId.value)
}
@@ -273,7 +272,7 @@ class RustMatrixClient constructor(
client.setDelegate(null)
verificationService.destroy()
syncService.destroy()
roomListService.destroy()
innerRoomListService.destroy()
notificationClient.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.media.MatrixMediaLoader
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
@Module
@@ -40,8 +40,8 @@ object SessionMatrixModule {
}
@Provides
fun provideRoomSummaryDataSource(matrixClient: MatrixClient): RoomSummaryDataSource {
return matrixClient.roomSummaryDataSource
fun providesRoomListService(matrixClient: MatrixClient): RoomListService {
return matrixClient.roomListService
}
@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.RoomId
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.withTimeout
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.
*/
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.matrix.impl.util.mxCallbackFlow

View File

@@ -14,10 +14,11 @@
* 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.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 org.matrix.rustcomponents.sdk.Room
import org.matrix.rustcomponents.sdk.RoomListItem

View File

@@ -14,10 +14,10 @@
* 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.matrix.api.room.RoomSummary
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.flow.MutableStateFlow
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
import io.element.android.libraries.matrix.api.sync.SyncState
import org.matrix.rustcomponents.sdk.RoomListServiceState
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 {
return when (this) {
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.SyncState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import org.matrix.rustcomponents.sdk.RoomListServiceState
import org.matrix.rustcomponents.sdk.SyncServiceInterface
import org.matrix.rustcomponents.sdk.SyncServiceState
import timber.log.Timber
class RustSyncService(
private val innerSyncService: SyncServiceInterface,
roomListStateFlow: Flow<RoomListServiceState>,
sessionCoroutineScope: CoroutineScope
) : SyncService {
override suspend fun startSync() = runCatching {
Timber.i("Start sync")
innerSyncService.start()
}.onFailure {
Timber.d("Start sync failed: $it")
}
override fun stopSync() = runCatching {
Timber.i("Stop sync")
innerSyncService.pause()
}.onFailure {
Timber.d("Stop sync failed: $it")
}
override val syncState: StateFlow<SyncState> =
roomListStateFlow
.map(RoomListServiceState::toSyncState)
innerSyncService.stateFlow()
.map(SyncServiceState::toSyncState)
.onEach { 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.flow.Flow
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.SyncServiceStateObserver
fun SyncService.stateFlow(): Flow<SyncServiceState> =
fun SyncServiceInterface.stateFlow(): Flow<SyncServiceState> =
mxCallbackFlow {
val listener = object : SyncServiceStateObserver {
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.room.MatrixRoom
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.MatrixUser
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.pushers.FakePushersService
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.verification.FakeSessionVerificationService
import io.element.android.tests.testutils.simulateLongTask
@@ -45,7 +45,7 @@ class FakeMatrixClient(
override val sessionId: SessionId = A_SESSION_ID,
private val userDisplayName: Result<String> = Result.success(A_USER_NAME),
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(),
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
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.TransactionId
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.room.RoomSummaryDetails
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
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.timeline.item.TimelineItemDebugInfo
import io.element.android.libraries.matrix.api.timeline.item.event.EventContent

View File

@@ -14,18 +14,21 @@
* 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.room.RoomSummaryDataSource
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.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class FakeRoomSummaryDataSource : RoomSummaryDataSource {
class FakeRoomListService : RoomListService {
private val allRoomSummariesFlow = 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>) {
allRoomSummariesFlow.emit(roomSummaries)
@@ -35,20 +38,16 @@ class FakeRoomSummaryDataSource : RoomSummaryDataSource {
inviteRoomSummariesFlow.emit(roomSummaries)
}
suspend fun postLoadingState(loadingState: RoomSummaryDataSource.LoadingState) {
suspend fun postAllRoomsLoadingState(loadingState: RoomList.LoadingState) {
allRoomsLoadingStateFlow.emit(loadingState)
}
override fun allRoomsLoadingState(): StateFlow<RoomSummaryDataSource.LoadingState> {
return allRoomsLoadingStateFlow
suspend fun postInviteRoomsLoadingState(loadingState: RoomList.LoadingState) {
inviteRoomsLoadingStateFlow.emit(loadingState)
}
override fun allRooms(): StateFlow<List<RoomSummary>> {
return allRoomSummariesFlow
}
override fun inviteRooms(): StateFlow<List<RoomSummary>> {
return inviteRoomSummariesFlow
suspend fun postState(state: RoomListService.State) {
roomListStateFlow.emit(state)
}
var latestSlidingSyncRange: IntRange? = null
@@ -57,4 +56,20 @@ class FakeRoomSummaryDataSource : RoomSummaryDataSource {
override fun updateAllRoomsVisibleRange(range: IntRange) {
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.Text
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
@Composable

View File

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